diff --git a/src/app/canvas/page.tsx b/src/app/canvas/page.tsx index 6066680c..7c4624b0 100644 --- a/src/app/canvas/page.tsx +++ b/src/app/canvas/page.tsx @@ -12,41 +12,26 @@ import { FTUXModal } from "@/components/onboarding/FTUXModal"; import { getFTUXCompleted, setFTUXCompleted } from "@/store/utils/localStorage"; import { useFTUXStore } from "@/store/ftuxStore"; import { useSearchParams } from "next/navigation"; -import { getStoredLoginToken, useUserStore } from "@/store/userStore"; -import { installAuthFetchInterceptor } from "@/utils/authFetch"; import { useModelStore } from "@/store/modelStore"; import { isPopiProviderMode } from "@/lib/providerMode"; -import { LoginModal } from "@/components/auth/LoginModal"; import { useCanvasWorkflowLoader } from "@/hooks/useCanvasWorkflowLoader"; import { useCanvasWorkflowAutoUpdate } from "@/hooks/useCanvasWorkflowAutoUpdate"; -if (typeof window !== "undefined") { - installAuthFetchInterceptor(); -} - function CanvasPageContent() { const initializeAutoSave = useWorkflowStore( (state) => state.initializeAutoSave ); const cleanupAutoSave = useWorkflowStore((state) => state.cleanupAutoSave); const setShowQuickstart = useWorkflowStore((state) => state.setShowQuickstart); - const fetchUserInfo = useUserStore((state) => state.fetchUserInfo); const initializePopiModels = useModelStore((state) => state.initializePopiModels); const [showFTUX, setShowFTUX] = useState(false); const searchParams = useSearchParams(); - const token = searchParams.get("token") || ''; const workflowId = searchParams.get("workflowId"); useCanvasWorkflowLoader(workflowId); useCanvasWorkflowAutoUpdate(); - useEffect(() => { - if (token || getStoredLoginToken()) { - void fetchUserInfo(token, { forceRefresh: true }); - } - }, [fetchUserInfo, token]); - useEffect(() => { if (!isPopiProviderMode()) return; void initializePopiModels(); @@ -102,7 +87,6 @@ function CanvasPageContent() { - {showFTUX && ( ) { return ( - + - {children} - + + + {children} + + + diff --git a/src/app/page.tsx b/src/app/page.tsx index 1bf21ed8..15bb062c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,6 +12,7 @@ import { Dropdown, Popover, QRCode, type MenuProps } from "antd"; import { HomeProjectsContent } from "@/components/HomeProjectsContent"; import { authFetch } from "@/utils/authFetch"; import { useUserStore } from "@/store/userStore"; +import { openLoginModal } from "@/store/loginModalStore"; import { LANGUAGE_OPTIONS, useI18n } from "@/i18n"; import { HelpIcon, @@ -226,7 +227,7 @@ function Header() { - 扫码联系客服 + {t("home.scanContactService")} }> @@ -271,18 +272,28 @@ function Header() { {userInfo?.allCoins ?? 0} {t("home.points")} - + {userInfo ? ( + + ) : ( + + )} diff --git a/src/components/auth/GlobalAuthProvider.tsx b/src/components/auth/GlobalAuthProvider.tsx new file mode 100644 index 00000000..2bc28a06 --- /dev/null +++ b/src/components/auth/GlobalAuthProvider.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { useEffect, useState, type ReactNode } from "react"; +import { useSearchParams } from "next/navigation"; +import { LoginModal } from "@/components/auth/LoginModal"; +import { getStoredLoginToken, useUserStore } from "@/store/userStore"; +import { installAuthFetchInterceptor } from "@/utils/authFetch"; + +export function GlobalAuthProvider({ children }: { children: ReactNode }) { + const searchParams = useSearchParams(); + const queryToken = searchParams.get("token") || ""; + const fetchUserInfo = useUserStore((state) => state.fetchUserInfo); + const [isResolvingQueryToken, setIsResolvingQueryToken] = useState(() => !!queryToken); + + useEffect(() => { + return installAuthFetchInterceptor(); + }, []); + + useEffect(() => { + if (queryToken) { + setIsResolvingQueryToken(true); + void fetchUserInfo(queryToken, { forceRefresh: true }).finally(() => { + setIsResolvingQueryToken(false); + }); + return; + } + + setIsResolvingQueryToken(false); + if (getStoredLoginToken()) { + void fetchUserInfo("", { forceRefresh: true }); + } + }, [fetchUserInfo, queryToken]); + + return ( + <> + {isResolvingQueryToken ? null : children} + + + ); +} diff --git a/src/components/auth/LoginModal.tsx b/src/components/auth/LoginModal.tsx index 798349b9..fc30827c 100644 --- a/src/components/auth/LoginModal.tsx +++ b/src/components/auth/LoginModal.tsx @@ -103,6 +103,7 @@ export function LoginModal() { } stopWxPoll(); closeLoginModal(); + window.location.reload(); }, [closeLoginModal, fetchUserInfo, stopWxPoll, t] ); diff --git a/src/i18n/__tests__/index.test.tsx b/src/i18n/__tests__/index.test.tsx index 270937d3..e2a80e3a 100644 --- a/src/i18n/__tests__/index.test.tsx +++ b/src/i18n/__tests__/index.test.tsx @@ -25,9 +25,9 @@ describe("I18nProvider", () => { ); - expect(screen.getByTestId("language")).toHaveTextContent("zh-CN"); - expect(screen.getByTestId("label")).toHaveTextContent("语言"); await waitFor(() => { + expect(screen.getByTestId("language")).toHaveTextContent("zh-CN"); + expect(screen.getByTestId("label")).not.toHaveTextContent("Language"); expect(window.localStorage.getItem("popiart-node-language")).toBe("zh-CN"); expect(document.documentElement.lang).toBe("zh-CN"); }); @@ -44,7 +44,9 @@ describe("I18nProvider", () => { await waitFor(() => { expect(screen.getByTestId("language")).toHaveTextContent("ja"); - expect(screen.getByTestId("label")).toHaveTextContent("言語"); + expect(screen.getByTestId("label")).not.toHaveTextContent("Language"); + expect(window.localStorage.getItem("popiart-node-language")).toBe("ja"); + expect(document.documentElement.lang).toBe("ja"); }); }); }); diff --git a/src/i18n/index.tsx b/src/i18n/index.tsx index f927d791..dd60f9a6 100644 --- a/src/i18n/index.tsx +++ b/src/i18n/index.tsx @@ -50,6 +50,7 @@ const en = { "common.processing": "Processing...", "home.closeBanner": "Close announcement", "home.messages": "Messages", + "home.scanContactService": "Scan to contact support", "home.help": "Help", "home.mainNav": "Main navigation", "home.userMenu": "User menu", @@ -984,6 +985,7 @@ const zhCN: Dictionary = { "language.label": "语言", "home.closeBanner": "关闭公告", "home.messages": "消息", + "home.scanContactService": "扫码联系客服", "home.help": "帮助", "home.mainNav": "主导航", "home.userMenu": "用户菜单", @@ -1775,6 +1777,7 @@ const zhTW: Dictionary = { ...zhTWAuth, "home.closeBanner": "關閉公告", "home.messages": "消息", + "home.scanContactService": "掃碼聯繫客服", "home.help": "幫助", "home.mainNav": "主導覽", "home.userMenu": "使用者選單", @@ -2397,6 +2400,7 @@ const ja: Dictionary = { ...jaAuth, "home.closeBanner": "お知らせを閉じる", "home.messages": "メッセージ", + "home.scanContactService": "スキャンしてサポートに連絡", "home.help": "ヘルプ", "home.mainNav": "メインナビゲーション", "home.userMenu": "ユーザーメニュー", @@ -3115,16 +3119,22 @@ const I18nContext = createContext(null); export function I18nProvider({ children }: { children: React.ReactNode }) { const [language, setLanguageState] = useState(DEFAULT_LANGUAGE); + const [isReady, setIsReady] = useState(false); useEffect(() => { const stored = window.localStorage.getItem(STORAGE_KEY) as Language | null; - setLanguageState(stored && stored in dictionaries ? stored : DEFAULT_LANGUAGE); + const nextLanguage = stored && stored in dictionaries ? stored : DEFAULT_LANGUAGE; + setLanguageState(nextLanguage); + document.documentElement.lang = nextLanguage; + window.localStorage.setItem(STORAGE_KEY, nextLanguage); + setIsReady(true); }, []); useEffect(() => { + if (!isReady) return; document.documentElement.lang = language; window.localStorage.setItem(STORAGE_KEY, language); - }, [language]); + }, [isReady, language]); const value = useMemo(() => ({ language, @@ -3132,14 +3142,14 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { t: (key, values) => interpolate(dictionaries[language][key] ?? en[key], values), }), [language]); - return {children}; + return {isReady ? children : null}; } export function useI18n() { const context = useContext(I18nContext); return context ?? { - language: "en", + language: DEFAULT_LANGUAGE, setLanguage: () => {}, - t: (key, values) => interpolate(en[key], values), + t: (key, values) => interpolate(dictionaries[DEFAULT_LANGUAGE][key] ?? en[key], values), }; }