/** * Auth 状态机(XState v5) * * 原始 Dart: lib/ui/auth/bloc/auth_bloc.dart + auth_state.dart + auth_event.dart * * 本轮迁移:所有认证登录调用(含 OAuth 社交登录)统一收口到状态机内。 * 业务层只派发事件(`AuthEmailLoginSubmitted` / `AuthGoogleLoginSubmitted` / 等), * 状态机通过 actors 调 `authRepository`(邮箱 / OAuth token sync)或 * `next-auth/react.signIn`(OAuth 跳转)。 */ import { setup, fromPromise, assign } from "xstate"; import { signIn } from "next-auth/react"; import type { AuthProvider } from "@/lib/auth/auth_platform"; import type { LoginStatus } from "@/models/auth/login-status"; import { authRepository } from "@/data/repositories/auth_repository"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { deviceIdentifier } from "@/utils/device_identifier"; import { Result } from "@/utils/result"; import { AuthState, initialState } from "./auth-state"; import type { AuthEvent } from "./auth-events"; // 重新导出 State / Event 类型,保持 machine 文件的公共 API 不变 export type { AuthState } from "./auth-state"; export { initialState } from "./auth-state"; export type { AuthEvent } from "./auth-events"; // ============================================================ // Helpers // ============================================================ async function readGuestId(): Promise { const r = await AuthStorage.getInstance().getDeviceId(); if (r.success && r.data != null) { return r.data; } return undefined; } // ============================================================ // Actors(异步服务) // ============================================================ const emailLoginActor = fromPromise( async ({ input }) => { const guestId = await readGuestId(); const result = await authRepository.emailLogin({ ...input, guestId }); if (Result.isErr(result)) throw result.error; return "email" as LoginStatus; }, ); const emailRegisterThenLoginActor = fromPromise< LoginStatus, { email: string; password: string; username: string; confirmPassword: string } >(async ({ input }) => { const guestId = await readGuestId(); const registerResult = await authRepository.register({ ...input, guestId }); if (Result.isErr(registerResult)) throw registerResult.error; // 注册后自动登录(对齐 Dart 行为) const loginResult = await authRepository.emailLogin({ email: input.email, password: input.password, guestId, }); if (Result.isErr(loginResult)) throw loginResult.error; return "email" as LoginStatus; }); // OAuth 登录 actor:调 next-auth/react 的 signIn(provider) 触发 OAuth 跳转。 // 成功路径下 NextAuth 会重定向离开本应用,actor 通常不会 resolve; // onDone 仍保留以处理 SDK 同步返回(极少见)。 const oauthSignInActor = fromPromise(async ({ input }) => { await signIn(input); }); // ========== OAuth token → 后端业务 token sync actors ========== // 由 在 NextAuth 回调后派发(监听 useSession())。 // 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。 // 后端返回的 LoginResponse 已经被 authRepository._saveLoginData 持久化。 const syncGoogleBackendActor = fromPromise( async ({ input }) => { const guestId = await readGuestId(); const result = await authRepository.googleLogin({ idToken: input.idToken, guestId, }); if (Result.isErr(result)) throw result.error; return "google" as LoginStatus; }, ); const syncFacebookBackendActor = fromPromise< LoginStatus, { accessToken: string } >(async ({ input }) => { const guestId = await readGuestId(); const result = await authRepository.facebookLogin({ accessToken: input.accessToken, guestId, }); if (Result.isErr(result)) throw result.error; return "facebook" as LoginStatus; }); // ========== 显式游客登录 actor(opt-in) ========== // 由 splash UI 派发 `AuthGuestLoginSubmitted` 触发,**不**自动跑。 // 取代了之前 checkAuthStatusActor 的 auto-guest-login 行为 —— 避免每次 // 访 splash 都自动创建游客账号污染后端。 const guestLoginActor = fromPromise(async () => { const deviceId = await deviceIdentifier.getDeviceId(); const result = await authRepository.guestLogin(deviceId); if (Result.isErr(result)) throw result.error; return "guest" as LoginStatus; }); // ========== 启动 / 恢复时检查登录态 actor(只查不创) ========== // 由 在 mount 时派发(一次性)。 // 流程(**纯只读**): // 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘 // (给后续 OAuth/email 登录准备 deviceId,**不**在 splash 首次访问就 auto-create 游客) // 2. AuthStorage.getLoginToken() —— 有 → "email" // 3. AuthStorage.getGuestToken() —— 有 → "guest" // 4. 都没有 → "notLoggedIn"(让用户**显式**选 OAuth / Email / Guest 入口) const checkAuthStatusActor = fromPromise(async () => { // 1. 拿 deviceId(**不**用于 auto-guest-login,仅供后续登录用) await deviceIdentifier.getDeviceId(); const storage = AuthStorage.getInstance(); // 2. 查 loginToken const loginTokenR = await storage.getLoginToken(); if (loginTokenR.success && loginTokenR.data) { return "email" as LoginStatus; } // 3. 查 guestToken const guestTokenR = await storage.getGuestToken(); if (guestTokenR.success && guestTokenR.data) { return "guest" as LoginStatus; } // 4. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口) return "notLoggedIn" as LoginStatus; }); // ============================================================ // Machine // // 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。 // **不**再设 `success` state —— 之前曾用 "success" 当"刚成功"信号, // 但 status check 流也会误入 success,触发 splash-button 误跳 /chat。 // 现在改用 `loginStatus !== "notLoggedIn"` 作为"已登录"信号(见 auth-context.tsx)。 // ============================================================ export const authMachine = setup({ types: { context: {} as AuthState, events: {} as AuthEvent, }, actors: { emailLogin: emailLoginActor, emailRegisterThenLogin: emailRegisterThenLoginActor, oauthSignIn: oauthSignInActor, syncGoogleBackend: syncGoogleBackendActor, syncFacebookBackend: syncFacebookBackendActor, guestLogin: guestLoginActor, checkAuthStatus: checkAuthStatusActor, }, }).createMachine({ id: "auth", initial: "idle", context: initialState, states: { idle: { on: { AuthPanelModeChanged: { actions: assign({ authPanelMode: ({ event }) => event.mode, errorMessage: null, }), }, AuthModeChanged: { actions: assign({ authMode: ({ event }) => event.mode, errorMessage: null, }), }, AuthFormCleared: { actions: assign({ email: "", password: "", username: "", confirmPassword: "", errorMessage: null, }), }, AuthReset: { actions: assign(() => initialState), }, // 启动 / 恢复时检查登录态 —— 由 派发 AuthStatusCheckSubmitted: "checkingAuthStatus", // 显式游客登录 —— 由 splash UI 派发(**不**自动) AuthGuestLoginSubmitted: "loadingGuestLogin", AuthEmailLoginSubmitted: "loadingEmailLogin", AuthEmailRegisterSubmitted: "loadingEmailRegister", AuthGoogleLoginSubmitted: "loadingOAuth", AuthFacebookLoginSubmitted: "loadingOAuth", // OAuth 回调后 sync 入口 —— 由 派发 AuthGoogleSyncSubmitted: "syncingGoogleBackend", AuthFacebookSyncSubmitted: "syncingFacebookBackend", AuthAppleLoginSubmitted: { actions: assign({ errorMessage: "Apple login not implemented", }), }, }, }, checkingAuthStatus: { entry: assign({ errorMessage: null }), invoke: { src: "checkAuthStatus", onDone: { target: "idle", // ← status check 完回 idle(**不**再假冒 success) actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, }), }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, loadingGuestLogin: { entry: assign({ errorMessage: null }), invoke: { src: "guestLogin", onDone: { target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, }), }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, loadingEmailLogin: { invoke: { src: "emailLogin", input: ({ event }) => { if (event.type !== "AuthEmailLoginSubmitted") return { email: "", password: "" }; return { email: event.email, password: event.password }; }, onDone: { target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, }), }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, loadingEmailRegister: { invoke: { src: "emailRegisterThenLogin", input: ({ event }) => { if (event.type !== "AuthEmailRegisterSubmitted") return { email: "", password: "", username: "", confirmPassword: "" }; return { email: event.email, password: event.password, username: event.username, confirmPassword: event.confirmPassword, }; }, onDone: { target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, }), }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, loadingOAuth: { entry: assign({ errorMessage: null }), invoke: { src: "oauthSignIn", input: ({ event }) => { if (event.type === "AuthGoogleLoginSubmitted") return event.provider; if (event.type === "AuthFacebookLoginSubmitted") return event.provider; return "google" as AuthProvider; }, onDone: { target: "idle", }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, syncingGoogleBackend: { entry: assign({ errorMessage: null }), invoke: { src: "syncGoogleBackend", input: ({ event }) => { if (event.type !== "AuthGoogleSyncSubmitted") return { idToken: "" }; return { idToken: event.idToken }; }, onDone: { target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, }), }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, syncingFacebookBackend: { entry: assign({ errorMessage: null }), invoke: { src: "syncFacebookBackend", input: ({ event }) => { if (event.type !== "AuthFacebookSyncSubmitted") return { accessToken: "" }; return { accessToken: event.accessToken }; }, onDone: { target: "idle", // ← 改自 "success" actions: assign({ loginStatus: ({ event }) => event.output, errorMessage: null, }), }, onError: { target: "idle", actions: assign({ errorMessage: ({ event }) => event.error instanceof Error ? event.error.message : String(event.error), }), }, }, }, }, }); export type AuthMachine = typeof authMachine;