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),
};
}