diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index 6e235053..f592f12a 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -19,12 +19,14 @@ */ import { useEffect, useRef } from "react"; import Image from "next/image"; +import { useRouter } from "next/navigation"; import { useAuthState } from "@/stores/auth/auth-context"; import type { LoginStatus } from "@/data/dto/auth"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; import { AppEnvUtil } from "@/utils/app-env"; +import { ROUTES } from "@/router/routes"; import { MobileShell } from "@/app/_components/core"; @@ -75,6 +77,7 @@ function dispatchAuthLifecycle( } export function ChatScreen() { + const router = useRouter(); const state = useChatState(); const authState = useAuthState(); const chatDispatch = useChatDispatch(); @@ -91,37 +94,36 @@ export function ChatScreen() { const showExhaustedBanner = isGuest && state.quotaLoaded && state.quotaExceededTrigger > 0; - - // ───────────────────────────────────────────────────────────── - // 屏幕生命周期 + 派初次鉴权生命周期事件 - // ───────────────────────────────────────────────────────────── - useEffect(() => { - dispatchAuthLifecycle(authState.loginStatus, chatDispatch); - return () => { - }; - // 注:mount useEffect 故意不依赖 loginStatus —— 首次 mount 时机是固定的 - // ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。 - // loginStatus 后续变化由下方"副作用 ②" 统一处理。 - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chatDispatch]); - - // ───────────────────────────────────────────────────────────── - // 副作用 ②:loginStatus 变化 → 派对应鉴权生命周期事件 + // loginStatus 变化 → 派对应鉴权生命周期事件 // // 规则: - // - 首次 mount(prev === null)→ 由 mount useEffect 统一处理 - // - "notLoggedIn" → 派 ChatLogout + // - AuthInit 未完成 → 等,避免初始 notLoggedIn 把 /chat 变成死状态 + // - AuthInit 完成后仍 "notLoggedIn" → 清 chat 并回 splash // - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin // ───────────────────────────────────────────────────────────── const prevLoginStatusRef = useRef(null); useEffect(() => { + if (!authState.hasInitialized || authState.isLoading) return; + const prev = prevLoginStatusRef.current; - prevLoginStatusRef.current = authState.loginStatus; - if (prev === null) return; // 首次 mount 由 mount useEffect 处理 if (prev === authState.loginStatus) return; // 登录态没变 + prevLoginStatusRef.current = authState.loginStatus; + + if (authState.loginStatus === "notLoggedIn") { + chatDispatch({ type: "ChatLogout" }); + router.replace(ROUTES.splash); + return; + } + dispatchAuthLifecycle(authState.loginStatus, chatDispatch); - }, [authState.loginStatus, chatDispatch]); + }, [ + authState.hasInitialized, + authState.isLoading, + authState.loginStatus, + chatDispatch, + router, + ]); return ( diff --git a/src/stores/auth/auth-context.tsx b/src/stores/auth/auth-context.tsx index 89128ec7..88b85f5f 100644 --- a/src/stores/auth/auth-context.tsx +++ b/src/stores/auth/auth-context.tsx @@ -34,6 +34,8 @@ interface AuthState { errorMessage: string | null; /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ loginStatus: MachineContext["loginStatus"]; + /** 启动时的本地登录态恢复是否已经完成 */ + hasInitialized: MachineContext["hasInitialized"]; /** * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true, * splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` @@ -67,12 +69,14 @@ export function AuthProvider({ children }: AuthProviderProps) { state.matches("loadingEmailLogin") || state.matches("loadingEmailRegister") || state.matches("loadingOAuth") || + state.matches("initializing") || state.matches("loggingOut") || state.matches("syncingGoogleBackend") || state.matches("syncingFacebookBackend") || state.matches("loadingGuestLogin"), errorMessage: state.context.errorMessage, loginStatus: state.context.loginStatus, + hasInitialized: state.context.hasInitialized, pendingRedirect: state.context.pendingRedirect, }), [state], diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 9501502b..147d9df1 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -111,6 +111,7 @@ export const authMachine = setup({ target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地) actions: assign({ loginStatus: ({ event }) => event.output, + hasInitialized: true, // 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图" errorMessage: null, }), @@ -120,6 +121,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -137,6 +139,7 @@ export const authMachine = setup({ // Skip 点击后已在 splash 里立即跳 /chat;guest login 不再依赖 pendingRedirect。 pendingRedirect: false, errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -144,6 +147,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -164,6 +168,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← 邮箱登录成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -171,6 +176,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -196,6 +202,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← 注册成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -203,6 +210,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -236,12 +244,16 @@ export const authMachine = setup({ src: "logout", onDone: { target: "idle", - actions: assign(() => initialState), + actions: assign(() => ({ + ...initialState, + hasInitialized: true, + })), }, onError: { target: "idle", actions: assign(({ event }) => ({ ...initialState, + hasInitialized: true, errorMessage: event.error instanceof Error ? event.error.message : String(event.error), })), @@ -263,6 +275,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← OAuth sync 成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -270,6 +283,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, @@ -289,6 +303,7 @@ export const authMachine = setup({ loginStatus: ({ event }) => event.output, pendingRedirect: true, // ← OAuth sync 成功 → 跳 errorMessage: null, + hasInitialized: true, }), }, onError: { @@ -296,6 +311,7 @@ export const authMachine = setup({ actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), + hasInitialized: true, }), }, }, diff --git a/src/stores/auth/auth-state.ts b/src/stores/auth/auth-state.ts index 80eb8471..84397bc1 100644 --- a/src/stores/auth/auth-state.ts +++ b/src/stores/auth/auth-state.ts @@ -15,6 +15,8 @@ export interface AuthState { errorMessage: string | null; /** 当前登录状态(未登录 / 游客 / OAuth / 邮箱) */ loginStatus: LoginStatus; + /** 启动时的本地登录态恢复是否已经完成 */ + hasInitialized: boolean; /** * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true * → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转 @@ -35,5 +37,6 @@ export const initialState: AuthState = { confirmPassword: "", errorMessage: null, loginStatus: "notLoggedIn", + hasInitialized: false, pendingRedirect: false, };