From bda1605ce708b9f439b856a90732d64270095113 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 17 Jun 2026 15:07:16 +0800 Subject: [PATCH] fix(auth): redirect to splash after logout --- src/app/sidebar/sidebar-screen.tsx | 18 ++++++++++++++---- src/stores/auth/auth-actors.ts | 5 +++++ src/stores/auth/auth-context.tsx | 1 + src/stores/auth/auth-events.ts | 1 + src/stores/auth/auth-machine.ts | 22 ++++++++++++++++++++++ src/stores/user/user-events.ts | 1 + src/stores/user/user-machine.ts | 5 ++++- 7 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/app/sidebar/sidebar-screen.tsx b/src/app/sidebar/sidebar-screen.tsx index b0eb898c..f6735935 100644 --- a/src/app/sidebar/sidebar-screen.tsx +++ b/src/app/sidebar/sidebar-screen.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useRouter } from "next/navigation"; +import { signOut } from "next-auth/react"; import { MobileShell, SettingsSection } from "@/app/_components/core"; import { ROUTES } from "@/router/routes"; @@ -36,15 +37,24 @@ export function SidebarScreen() { const auth = useAuthState(); const authDispatch = useAuthDispatch(); const router = useRouter(); + const isLogoutRedirectPendingRef = useRef(false); useEffect(() => { userDispatch({ type: "UserInit" }); }, [userDispatch]); - const handleLogoutClick = () => { - authDispatch({ type: "AuthReset" }); - userDispatch({ type: "UserLogout" }); + useEffect(() => { + if (!isLogoutRedirectPendingRef.current) return; + if (auth.isLoading || auth.loginStatus !== "notLoggedIn") return; + isLogoutRedirectPendingRef.current = false; router.replace(ROUTES.splash); + }, [auth.isLoading, auth.loginStatus, router]); + + const handleLogoutClick = () => { + isLogoutRedirectPendingRef.current = true; + userDispatch({ type: "UserClearLocal" }); + authDispatch({ type: "AuthLogoutSubmitted" }); + void signOut({ redirect: false }); }; // 状态派生:未登录 / 已登录未 VIP / 已登录 VIP diff --git a/src/stores/auth/auth-actors.ts b/src/stores/auth/auth-actors.ts index 4a7fc050..d6e4d56e 100644 --- a/src/stores/auth/auth-actors.ts +++ b/src/stores/auth/auth-actors.ts @@ -97,6 +97,11 @@ export const oauthSignInActor = fromPromise(async ({ input } } }); +export const logoutActor = fromPromise(async () => { + const result = await authRepo.logout(); + if (Result.isErr(result)) throw result.error; +}); + // ========== OAuth token → 后端业务 token sync actors ========== // 由 在 NextAuth 回调后派发(监听 useSession())。 // 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。 diff --git a/src/stores/auth/auth-context.tsx b/src/stores/auth/auth-context.tsx index abf038ce..89128ec7 100644 --- a/src/stores/auth/auth-context.tsx +++ b/src/stores/auth/auth-context.tsx @@ -67,6 +67,7 @@ export function AuthProvider({ children }: AuthProviderProps) { state.matches("loadingEmailLogin") || state.matches("loadingEmailRegister") || state.matches("loadingOAuth") || + state.matches("loggingOut") || state.matches("syncingGoogleBackend") || state.matches("syncingFacebookBackend") || state.matches("loadingGuestLogin"), diff --git a/src/stores/auth/auth-events.ts b/src/stores/auth/auth-events.ts index 985d6b24..e01d58b9 100644 --- a/src/stores/auth/auth-events.ts +++ b/src/stores/auth/auth-events.ts @@ -13,6 +13,7 @@ export type AuthEvent = | { type: "AuthModeChanged"; mode: AuthMode } | { type: "AuthFormCleared" } | { type: "AuthReset" } + | { type: "AuthLogoutSubmitted" } /** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */ | { type: "AuthInit" } /** 显式游客登录 —— splash 上点"游客模式"按钮触发(不自动) */ diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index 973e262b..56efeb4d 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -15,6 +15,7 @@ import { syncFacebookBackendActor, guestLoginActor, checkAuthStatusActor, + logoutActor, } from "./auth-actors"; // 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变 @@ -46,6 +47,7 @@ export const authMachine = setup({ syncFacebookBackend: syncFacebookBackendActor, guestLogin: guestLoginActor, checkAuthStatus: checkAuthStatusActor, + logout: logoutActor, }, }).createMachine({ /** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgOgJYQBswBiAQQ0wAVkA7MAgWQHsIwBhTWmCAbQAYAuolAAHJrFzpcTGsJAAPRABYA7NgAc6gEzqAjCvUBOLbvUqlWgDQgAnohWnsAZgCsSpXyeHdTgGy6lXwBfIOs0LDxCEnIsZlYOLkh+ISQQMQkpGTlFBF81Y1MzX3UvdT5NaztcrS1sD10tQ1dvPi0+YNCQcJx8IjIKADEmACcAWzYiZGGkwTl0yWlZVJzzbHMarSUvNpctYsrEXd86pT0SvfULXyUQsIpIvpjMACU4MHRkufEFrOXEXT4fGwvkMLicmxUTT4qj4hgOCC2QKUoL4KkaShc5ycty69160QoAEkaJJPql5pklqAcl4nGtDCCtGDdqotCp4YY9MC0Xw3CpfH5Gi4cd0HgSsABxVBwdAAGSYUFwNAAyqgAEajSToGYpUTfSnZRC0+mM5kQtnwlzlDQuQynEpOJz6BoqEV4qL9LAAUVGyFwBHlipV6s16G1vFm5P1i0NCGNkNNrnN7Ns9gx2D2LhUDjcl0BN06ovxnswPr9BFeitg2uGqo1Wp1XwyMb+caaJuMZtZKaquhBQKMsOhHiafY6dwixaeEqYCqIgaVddD4bJeubv2pRvbCc7Se78P0SjqYLaewuTtObsnHqeA2QAGMwGrZwBrBfB+thxtR9dUhRbukdyZPc0R7RAaiPUEnRKREjH0dQrx6G8KFIEQRHnBVFxDBsI11NJow3f8EVqVRfCzJ14L4Qp4UdI83F0Bkz3BJxtEQsUSxnOcwGVGwaHvJccNXfDf1jeMGV3FlQIPDEj1OQITAYjwijYqdBgfJ9Xx4viBK-XCmx+P8aW3cTgMki1UwQBpHFRB1IQMdRMXHXFr0eCgJjAKZKDAGgICVKBXl86Z7w+SM1wM2MLGwQFopimKAnhLQWOwTFNF2JxAWuDwtBUkkpGQAhcAALz84gIBkMA8BoAA3JgXwqotctwfKir8hAlRq+9kEpZIhIpFtNwRAVkt8JkdCaXxWhMeFNHUNZTEMTwXExaEXF0HKFma4qaCgYgwGGYYRmwdCuoAMxGUZsAajaCq2qA2uqphOu6wReoIwzlGubAFoFUw3BMEpQXhSEXDWMiVDSlkJtdQt7gIJhkF87apRld9SvKyqarqy7YfhxGoGR6t33ujqusWHrQuE8LWytIFwT0fxAW0F14SUXQQe+x0HDZRy7TYuGEb8gm5Uwmhdv2w7jvQM6xmxiJ+bxoWifax7SZkcm8L6wicitXRuQYq1-C2cwXBomovrBX7jDaVonNFeW-LLf1UbKugMdq+qcYF7bHYDEXiZV56BFekTqdaPXxs5PJdGj+EyNqJ0z08Cx6b53GHd9J2RbFg7hiOghTvO2WcHt72M99oN-aesmXopzX3oQK0jyolxrkS05JuzS1bWcW1Am8Jw0SdW3Pbxn3K1was9rR13laxu209L8tx8n4ZK9Vmh1f0g1WwsXWLacEcjAPxuD1tfIB-B8FPBUcFsphuWF6gMewCrGts4l-OpcL+evafsvl5rGvQOwcqYDQsLUMwjcBQGxbmBSyjcvqXAmgxTkAQDCp1-gAeSeNPCqs8PYPywU8IB1cg61zehFUEyUDCmD8M6TQ-IErg2PEPCwB9QQmAwXjbBFB3650ltLC6P9uHEOVlXNWNcNYUJ3gyL6-1NCGAMDTPw0097RVWrvCEAQ2KwF4veQWs4oBEAAEIPjqj5XBbs573F0XxAxXFTH3nMRAEhEiyFSJDmArYUU2Y-VMCCTQugDx0OSheEEnIHKeDvhOHAtj9FI0MSYsx3kIB8LzgXGWoo4n2KMWARxzjXEb0kVvfqRF3BHmaBYVEo4TYWQaMUahJRTAGEaIUHRei-J3kfM+Wq+SUmWPwUXbA2TtpdI0r05JPlCmbx-KAspexnANHIiYTkqIBQHiWdgJZ-jszgnWO0uxoz1I9JfH0ixe0c7pK-pkmxHSjndNfGclxYj14zLCtvMBbJgQtzZA5PQ0cmgHiiVsnkrhYHFE5CEToNAWBwDkN0EpWtEAAFp2aGHRRizF6L1C+HhMi4eLkwCIvrpsowlxsxgjRAEUwqjDDOFRBoxKI1ygIXvj0Rqm0-LEtjAEnudo-DjQ8CNVRQJo43zWQKR0pguGC2lITEW3LWy8sqQKvwQqrAWU5vS1QrNEo6BvgS4uj8fbvkVQNZVvdVXXHaBqqo5E5oAl8GDXkvyZWL39AAvaZqiIWv5SCNVNrT4MWcHQ8iQ5mluqgDwrA3qci+q2P661wq6lh30E68pmh3CbGxGy4Zdz8aJLyZMiAsbEDxqteqg8OhajGE5OYWEA8jDQxiXmw5UAxknKeaW3InI+UJsFYGlNgRkqJVtOUJRE1WUhCAA */ @@ -79,6 +81,7 @@ export const authMachine = setup({ AuthReset: { actions: assign(() => initialState), }, + AuthLogoutSubmitted: "loggingOut", // 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 派发 AuthInit: "initializing", @@ -230,6 +233,25 @@ export const authMachine = setup({ }, }, + loggingOut: { + entry: assign({ errorMessage: null, pendingRedirect: false }), + invoke: { + src: "logout", + onDone: { + target: "idle", + actions: assign(() => initialState), + }, + onError: { + target: "idle", + actions: assign(({ event }) => ({ + ...initialState, + errorMessage: + event.error instanceof Error ? event.error.message : String(event.error), + })), + }, + }, + }, + syncingGoogleBackend: { entry: assign({ errorMessage: null }), invoke: { diff --git a/src/stores/user/user-events.ts b/src/stores/user/user-events.ts index af51026b..bbb793d6 100644 --- a/src/stores/user/user-events.ts +++ b/src/stores/user/user-events.ts @@ -9,6 +9,7 @@ export type UserEvent = | { type: "UserUpdate"; user: UserView } | { type: "UserUpdateUsername"; username: string } | { type: "UserUpdatePronouns"; pronouns: string } + | { type: "UserClearLocal" } | { type: "UserLogout" } | { type: "UserDeleteChatHistory" } | { type: "UserDeleteAccount" }; diff --git a/src/stores/user/user-machine.ts b/src/stores/user/user-machine.ts index f36a3a73..fd1b0dcd 100644 --- a/src/stores/user/user-machine.ts +++ b/src/stores/user/user-machine.ts @@ -70,6 +70,9 @@ export const userMachine = setup({ UserUpdatePronouns: { actions: "updatePronouns", }, + UserClearLocal: { + actions: "clearUser", + }, UserLogout: "loggingOut", UserDeleteChatHistory: {}, UserDeleteAccount: {}, @@ -125,4 +128,4 @@ export const userMachine = setup({ }, }); -export type UserMachine = typeof userMachine; \ No newline at end of file +export type UserMachine = typeof userMachine;