refactor(auth): funnel OAuth signIn into auth state machine

Move new AuthPlatform(provider).signIn() out of UI components and into
the auth state machine. UI now dispatches AuthGoogle/FacebookLoginSubmitted
events; a new oauthSignIn actor inside the machine calls next-auth/react's
signIn(provider). Errors and the OAuth redirect window are exposed via
the existing state.isLoading / state.errorMessage surface, removing the
local busy / error state in auth-facebook-panel.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kanban
2026-06-11 10:05:14 +08:00
committed by chenhang
parent d4cb40a74b
commit e2a57386e7
6 changed files with 274 additions and 54 deletions
+193
View File
@@ -388,3 +388,196 @@ export default function RootLayout({ children }: { children: React.ReactNode })
7. **新建 global.d.ts**:声明 `window.google.accounts.id.*` 类型 7. **新建 global.d.ts**:声明 `window.google.accounts.id.*` 类型
8. **依次新建** `auth_event_bridge.ts``initialize_*.ts``auth_platform.ts``web_auth_platform.ts``mock_auth_platform.ts``auth_bridge_component.tsx` 8. **依次新建** `auth_event_bridge.ts``initialize_*.ts``auth_platform.ts``web_auth_platform.ts``mock_auth_platform.ts``auth_bridge_component.tsx`
9. **验证**`npx tsc --noEmit``pnpm lint``pnpm build` 9. **验证**`npx tsc --noEmit``pnpm lint``pnpm build`
---
# 收口 OAuth 登录调用到 Auth 状态机
## [Overview]
`new AuthPlatform(provider).signIn()` 的直接调用从 UI 组件移到 Auth 状态机里。改完后所有 OAuth 入口都通过 `useAuthDispatch()` 派发 `AuthGoogleLoginSubmitted` / `AuthFacebookLoginSubmitted` 事件,状态机自己用 `next-auth/react``signIn(provider)` 触发流程。NextAuth 重定向期间用一个 `loadingOAuth` 子状态表示"正在跳走",期间 `isLoading` 为 trueUI 复用现有的 `state.isLoading` 控制按钮 disabled;失败把 `errorMessage` 写入 context。
## [Goals]
- UI 组件不再 `import { AuthPlatform }`,仅派发事件
- 状态机里承载 OAuth 流程(actor 调 `signIn`、成功/失败分支)
- 复用既有 `isLoading` / `errorMessage` 形态,UI 侧零概念变化
- 行为对齐:成功 = NextAuth 重定向到 `callbackUrl`(默认 `/chat`),所以状态机不需要"成功"分支——和邮箱登录的 `success` 终态语义不同。OAuth 的"完成"由 NextAuth 回调 + `SessionProvider` 处理
## [Non-Goals]
- 不动邮箱登录的 actor / 事件
- 不动 `Logout` 类(实际未在 UI 直接 `new Logout()`,由 `userMachine``authRepository.logout` 走,OAuth 注销是另一条线)
- 不在状态机里尝试"成功"——NextAuth 跳转后页面会被 Next.js 接管,state 在那时已被销毁,无需 `success` 终态
- 不引入新的 busy 字段
## [Files]
### 修改
| 路径 | 变更 |
|---|---|
| `src/stores/auth/machine/auth-events.ts` | `AuthGoogleLoginSubmitted` / `AuthFacebookLoginSubmitted` 携带 `provider: AuthProvider` 字段(或保留无 payload + 状态机内部按事件类型路由) |
| `src/stores/auth/machine/auth-machine.ts` | 新增 `oauthSignIn: fromPromise<void, AuthProvider>` actor`idle` 中两个事件指向新状态 `loadingOAuth``loadingOAuth.invoke` 调 actor`onError` 把错误写入 `errorMessage` 并回到 `idle``onDone` 也回到 `idle`(NextAuth 一旦调起就会重定向,`onDone` 极少触发;但状态机结构上仍要正确处理以防 SDK 同步返回) |
| `src/app/splash/components/splash-button.tsx` | 移除 `import { AuthPlatform }`,改用 `useAuthDispatch` 派发 `AuthFacebookLoginSubmitted`;移除 `handleFacebookLogin` 的 try/catch(错误走状态机) |
| `src/app/auth/components/auth-facebook-panel.tsx` | 移除 `import { AuthPlatform }`、移除本地 `busy` 状态(用 `state.isLoading` 替代) / `error` 状态(用 `state.errorMessage` 替代);改用 `useAuthDispatch` 派发对应事件;`disabled``isLoading`;按钮 spinner 用 `isLoading` 判断(不能再区分是 Facebook 还是 Google 在加载——OAuth 跳转期间整个 UI 都该锁住) |
| `src/lib/auth/nextauth.ts` | 仍 re-export `AuthPlatform` 类型(给 `authRepository` 内部用),但**删除**面向 UI 的 export 仅保留内部使用;或将 export 改为 `internal` barrel 命名(不强制,看用户偏好) |
### 不变
- `src/lib/auth/auth_platform.ts` 保持原样(其他模块可能用)
- `src/stores/auth/machine/auth-context.tsx` 不用动(`isLoading` / `errorMessage` 已暴露)
- `src/stores/auth/auth-state.ts` / `auth-types.ts` / `index.ts` 不用动
## [State Machine Diff]
```ts
// auth-events.ts —— OAuth 事件保留无 payloadprovider 由事件类型决定
export type AuthEvent =
| { type: "AuthPanelModeChanged"; mode: AuthPanelMode }
| { type: "AuthModeChanged"; mode: AuthMode }
| { type: "AuthFormCleared" }
| { type: "AuthReset" }
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
| { type: "AuthEmailRegisterSubmitted"; email: string; password: string; username: string; confirmPassword: string }
| { type: "AuthGoogleLoginSubmitted" }
| { type: "AuthFacebookLoginSubmitted" }
| { type: "AuthAppleLoginSubmitted" };
```
```ts
// auth-machine.ts —— 新增 actor + 状态
import { signIn } from "next-auth/react";
const oauthSignInActor = fromPromise<void, "google" | "facebook">(async ({ input }) => {
await signIn(input);
});
export const authMachine = setup({
types: { context: {} as AuthState, events: {} as AuthEvent },
actors: {
emailLogin: emailLoginActor,
emailRegisterThenLogin: emailRegisterThenLoginActor,
oauthSignIn: oauthSignInActor,
},
}).createMachine({
id: "auth",
initial: "idle",
context: initialState,
states: {
idle: {
on: {
// ... 既有事件保持 ...
AuthGoogleLoginSubmitted: { target: "loadingOAuth" },
AuthFacebookLoginSubmitted: { target: "loadingOAuth" },
AuthAppleLoginSubmitted: {
actions: assign({ errorMessage: "Apple login not implemented" }),
},
},
},
loadingOAuth: {
// 记录是哪个 provider 出错
entry: assign({ errorMessage: null }),
invoke: {
src: "oauthSignIn",
input: ({ context }) => context.pendingOAuthProvider ?? "google",
onDone: { target: "idle" }, // 罕见分支:NextAuth 同步 resolve(不应发生)
onError: {
target: "idle",
actions: assign({
errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error),
}),
},
},
on: {
AuthGoogleLoginSubmitted: { /* 同 provider 重新触发:重新进入状态即可;XState v5 默认会重 invoke */ target: "loadingOAuth", reenter: true },
AuthFacebookLoginSubmitted: { target: "loadingOAuth", reenter: true },
},
},
// loadingEmailLogin / loadingEmailRegister / success 不变
},
});
```
**注意**:把 provider 传给 actor 需要从事件读取。两种实现:
1. **input 走 event(推荐)**:在 `idle` 的 transition 上用 `actions: assign({ pendingOAuthProvider: ... })` + 用 `target: "loadingOAuth"`。但 XState v5 在 `invoke.input` 只能拿 `event`(触发 transition 的那个),且 `loadingOAuth` 没有 transition 是用 `event` 进入的(是 target 跳转),所以更稳的做法是给事件本身带 `provider`
```ts
// 推荐方案:事件携带 provider
| { type: "AuthGoogleLoginSubmitted"; provider: "google" }
| { type: "AuthFacebookLoginSubmitted"; provider: "facebook" }
```
2. 状态机入口用 `actions: assign({ pendingOAuthProvider: ... })` 写入 context,再从 `context` 读。
**采用方案 1**——更显式、无需 context 字段。详见 [Implementation Order] 第 3 步。
## [AuthState 增量]
不需要新字段。如果采用"从 context 读 provider"才需要 `pendingOAuthProvider`,但方案 1 不用。
## [UI Diff]
### `splash-button.tsx`
```tsx
// 旧
const handleFacebookLogin = async () => {
try { await new AuthPlatform("facebook").signIn(); }
catch (e) { console.error("[splash-button] Facebook login failed", e); }
};
// 新
const authDispatch = useAuthDispatch();
const handleFacebookLogin = () => authDispatch({ type: "AuthFacebookLoginSubmitted" });
```
### `auth-facebook-panel.tsx`
```tsx
// 旧
const [showOptions, setShowOptions] = useState(false);
const [busy, setBusy] = useState<null | "facebook" | "google">(null);
const [error, setError] = useState<string | null>(null);
const handleFacebook = async () => { /* setBusy + try new AuthPlatform().signIn() */ };
const handleGoogle = async () => { /* setBusy + try new AuthPlatform().signIn() */ };
// 新
const { isLoading, errorMessage } = useAuthState();
const authDispatch = useAuthDispatch();
const handleFacebook = () => authDispatch({ type: "AuthFacebookLoginSubmitted" });
const handleGoogle = () => authDispatch({ type: "AuthGoogleLoginSubmitted" });
// 渲染:disabled={isLoading}spinner 用 isLoadingerror 用 errorMessage
```
**取舍**
- **去掉了"哪个按钮在转"区分**——OAuth 跳转期间 `isLoading` 一直 true,两个按钮都禁用并显示 spinner。这是预期行为(避免重复触发),且和 splash 一致
- **去掉了本地 error 状态**——错误统一到 `state.errorMessage`,与邮箱登录同源
## [Trade-offs]
- ✅ 调用方零概念:UI 只 dispatch,不知道有 `next-auth/react`
- ✅ 错误处理统一:`state.errorMessage` 涵盖邮箱 + OAuth
-`isLoading` 统一:UI 不用为 OAuth 单写 busy
- ⚠️ `signIn` 副作用(重定向)从 React 组件挪到 `fromPromise` actorXState v5 支持 promise actor 在浏览器环境跑
- ⚠️ `auth-facebook-panel` 失去"区分两个 spinner"的能力(已知取舍)
## [Testing]
- `npx tsc --noEmit`0 错误
- `pnpm lint`:通过
- 手动 smoke:点 Facebook / Google 按钮 → 状态机进入 `loadingOAuth` → NextAuth 重定向到 provider
- 检查:刷新 / 失败场景下 `errorMessage` 正确显示在 `<AuthErrorMessage />`
## [Implementation Order]
1. **`auth-events.ts`**OAuth 事件携带 `provider: "google" | "facebook"`(如果用方案 1
2. **`auth-machine.ts`**:新增 `oauthSignInActor` + `loadingOAuth` 状态 + idle transition 指向它;移除 `// 社交登录已迁移到 NextAuth + AuthPlatform 类` 注释
3. **`splash-button.tsx`**:替换 `handleFacebookLogin` 为 dispatch;移除 `AuthPlatform` import
4. **`auth-facebook-panel.tsx`**:替换两个 handler;移除本地 `busy` / `error`;按钮 `disabled``isLoading`spinner 用 `isLoading``<AuthErrorMessage message={error} />` 改为 `message={errorMessage}`(来自 `useAuthState`
5. **`auth-context.tsx`**:检查 `isLoading` 表达式是否覆盖 `loadingOAuth`——需要把 `state.matches("loadingOAuth")` 加进去
6. **验证**`npx tsc --noEmit` + `pnpm lint` + dev server smoke
7. **可选清理**`src/lib/auth/nextauth.ts``export { AuthPlatform, type AuthProvider }` 改为仅在 auth_repository / 其他内部消费者使用;如果没有别的 import,可保留
+23 -31
View File
@@ -7,14 +7,16 @@
* 视觉规格(与 Dart 对齐): * 视觉规格(与 Dart 对齐):
* - Spacer → logo120h)→ Spacer → Facebook 主按钮 → 链接 → Spacer → 法律 * - Spacer → logo120h)→ Spacer → Facebook 主按钮 → 链接 → Spacer → 法律
* - Facebook 主按钮:46px 白胶囊 + Facebook "f" 图标(#1877f2+ "Login with Facebook" * - Facebook 主按钮:46px 白胶囊 + Facebook "f" 图标(#1877f2+ "Login with Facebook"
* - 社交登录走 NextAuth`new AuthPlatform("facebook").signIn()` / `new AuthPlatform("google").signIn()` * - 社交登录统一收口到 Auth 状态机:派发 `AuthFacebookLoginSubmitted` /
* `AuthGoogleLoginSubmitted` 事件,状态机内部调 next-auth/react 的 signIn(provider)
* - busy / error 状态来自 `useAuthState()`(与邮箱登录同源)
* - "Other Sign-in Options" 链接打开底部弹层 * - "Other Sign-in Options" 链接打开底部弹层
* - 弹层 Email 按钮 → 触发 `onSwitchToEmail`(父级派发 `AuthPanelModeChanged` * - 弹层 Email 按钮 → 触发 `onSwitchToEmail`(父级派发 `AuthPanelModeChanged`
*/ */
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import { AuthPlatform } from "@/lib/auth/nextauth"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { AuthErrorMessage } from "./auth-error-message"; import { AuthErrorMessage } from "./auth-error-message";
import { AuthLegalText } from "./auth-legal-text"; import { AuthLegalText } from "./auth-legal-text";
@@ -27,31 +29,21 @@ export interface AuthFacebookPanelProps {
export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) { export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
const [showOptions, setShowOptions] = useState(false); const [showOptions, setShowOptions] = useState(false);
const [busy, setBusy] = useState<null | "facebook" | "google">(null); const { isLoading, errorMessage } = useAuthState();
const [error, setError] = useState<string | null>(null); const authDispatch = useAuthDispatch();
const handleFacebook = async () => { const handleFacebook = () => {
setBusy("facebook"); authDispatch({
setError(null); type: "AuthFacebookLoginSubmitted",
try { provider: "facebook",
await new AuthPlatform("facebook").signIn(); });
} catch (e) {
setError(e instanceof Error ? e.message : "Facebook login failed");
} finally {
setBusy(null);
}
}; };
const handleGoogle = async () => { const handleGoogle = () => {
setBusy("google"); authDispatch({
setError(null); type: "AuthGoogleLoginSubmitted",
try { provider: "google",
await new AuthPlatform("google").signIn(); });
} catch (e) {
setError(e instanceof Error ? e.message : "Google login failed");
} finally {
setBusy(null);
}
}; };
return ( return (
@@ -70,10 +62,10 @@ export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
<button <button
type="button" type="button"
className={styles.facebookButton} className={styles.facebookButton}
onClick={() => void handleFacebook()} onClick={() => handleFacebook()}
disabled={busy !== null} disabled={isLoading}
> >
{busy === "facebook" ? ( {isLoading ? (
<span className={styles.spinner} aria-hidden="true" /> <span className={styles.spinner} aria-hidden="true" />
) : ( ) : (
<span className={styles.facebookIcon} aria-hidden="true"> <span className={styles.facebookIcon} aria-hidden="true">
@@ -83,7 +75,7 @@ export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
</span> </span>
)} )}
<span className={styles.facebookLabel}> <span className={styles.facebookLabel}>
{busy === "facebook" ? "Logging in..." : "Login with Facebook"} {isLoading ? "Logging in..." : "Login with Facebook"}
</span> </span>
</button> </button>
@@ -97,15 +89,15 @@ export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
<div className={styles.spacer} /> <div className={styles.spacer} />
<AuthErrorMessage message={error} /> <AuthErrorMessage message={errorMessage} />
<AuthLegalText /> <AuthLegalText />
<AuthOtherOptionsDialog <AuthOtherOptionsDialog
open={showOptions} open={showOptions}
onClose={() => setShowOptions(false)} onClose={() => setShowOptions(false)}
mode="facebook" mode="facebook"
onFacebook={() => void handleFacebook()} onFacebook={() => handleFacebook()}
onGoogle={() => void handleGoogle()} onGoogle={() => handleGoogle()}
onEmail={onSwitchToEmail} onEmail={onSwitchToEmail}
/> />
</div> </div>
+9 -10
View File
@@ -7,20 +7,19 @@
* *
* 本组件是 splash 鉴权流程的**自治单元** * 本组件是 splash 鉴权流程的**自治单元**
* - 消费 `useSession()`next-auth/react)监听 NextAuth session * - 消费 `useSession()`next-auth/react)监听 NextAuth session
* - 消费 `useAuthState` / `useAuthDispatch`(邮箱登录流程 * - 消费 `useAuthState` / `useAuthDispatch`(邮箱登录 + OAuth 流程统一收口到状态机
* - 消费 `useChatDispatch` / `useUserDispatch`(登录成功后初始化) * - 消费 `useChatDispatch` / `useUserDispatch`(登录成功后初始化)
* - 自管路由跳转(已登录 / 登录成功 → /chat) * - 自管路由跳转(已登录 / 登录成功 → /chat)
* - Skip 用 `<Link>` 保留 SPA 体验 * - Skip 用 `<Link>` 保留 SPA 体验
* - 社交登录按钮走 `new AuthPlatform("facebook").signIn()`NextAuth 流程) * - 社交登录按钮仅派发 `AuthFacebookLoginSubmitted` 事件,由状态机负责调 NextAuth
*/ */
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useAuthState } from "@/stores/auth/auth-context"; import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useChatDispatch } from "@/stores/chat/chat-context"; import { useChatDispatch } from "@/stores/chat/chat-context";
import { useUserDispatch } from "@/stores/user/user-context"; import { useUserDispatch } from "@/stores/user/user-context";
import { AuthPlatform } from "@/lib/auth/nextauth";
import { ROUTES } from "@/router/routes"; import { ROUTES } from "@/router/routes";
import { LoadingIndicator } from "@/app/_components/core/loading-indicator"; import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
import styles from "./splash-button.module.css"; import styles from "./splash-button.module.css";
@@ -33,6 +32,7 @@ export function SplashButton() {
// 不是鉴权判断;proxy 会让通过因为 cookie 已设) // 不是鉴权判断;proxy 会让通过因为 cookie 已设)
const state = useAuthState(); const state = useAuthState();
const authDispatch = useAuthDispatch();
const isLoading = state.isLoading; const isLoading = state.isLoading;
// ===== 跨 store 初始化(登录成功后触发) ===== // ===== 跨 store 初始化(登录成功后触发) =====
@@ -51,12 +51,11 @@ export function SplashButton() {
} }
}, [state.isSuccess, chatDispatch, userDispatch, router]); }, [state.isSuccess, chatDispatch, userDispatch, router]);
const handleFacebookLogin = async () => { const handleFacebookLogin = () => {
try { authDispatch({
await new AuthPlatform("facebook").signIn(); type: "AuthFacebookLoginSubmitted",
} catch (e) { provider: "facebook",
console.error("[splash-button] Facebook login failed", e); });
}
}; };
return ( return (
+5 -3
View File
@@ -55,9 +55,11 @@ export function AuthProvider({ children }: AuthProviderProps) {
password: state.context.password, password: state.context.password,
username: state.context.username, username: state.context.username,
confirmPassword: state.context.confirmPassword, confirmPassword: state.context.confirmPassword,
// isLoading 仅来自邮箱登录 actor(社交登录改由 NextAuth 处理,UI 自行管 busy 状态 // isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间
isLoading: state.matches("loadingEmailLogin") || isLoading:
state.matches("loadingEmailRegister"), state.matches("loadingEmailLogin") ||
state.matches("loadingEmailRegister") ||
state.matches("loadingOAuth"),
errorMessage: state.context.errorMessage, errorMessage: state.context.errorMessage,
isSuccess: state.matches("success"), isSuccess: state.matches("success"),
loginType: state.context.loginType, loginType: state.context.loginType,
+4 -3
View File
@@ -3,6 +3,7 @@
* *
* 事件名与原 Dart AuthEvent 对齐。 * 事件名与原 Dart AuthEvent 对齐。
*/ */
import type { AuthProvider } from "@/lib/auth/auth_platform";
import type { AuthMode } from "@/models/auth/auth-mode"; import type { AuthMode } from "@/models/auth/auth-mode";
import type { AuthPanelMode } from "@/models/auth/auth-panel-mode"; import type { AuthPanelMode } from "@/models/auth/auth-panel-mode";
@@ -21,7 +22,7 @@ export type AuthEvent =
username: string; username: string;
confirmPassword: string; confirmPassword: string;
} }
// 社交登录(已迁移到 NextAuth)— 业务层直接 await nextauth.googleLogin() 即可 // 社交登录 —— 状态机内部调 next-auth/react 的 signIn(provider)
| { type: "AuthGoogleLoginSubmitted" } | { type: "AuthGoogleLoginSubmitted"; provider: AuthProvider }
| { type: "AuthFacebookLoginSubmitted" } | { type: "AuthFacebookLoginSubmitted"; provider: AuthProvider }
| { type: "AuthAppleLoginSubmitted" }; | { type: "AuthAppleLoginSubmitted" };
+40 -7
View File
@@ -3,12 +3,14 @@
* *
* 原始 Dart: lib/ui/auth/bloc/auth_bloc.dart + auth_state.dart + auth_event.dart * 原始 Dart: lib/ui/auth/bloc/auth_bloc.dart + auth_state.dart + auth_event.dart
* *
* 本轮迁移:社交登录改由 NextAuth 处理(`@/lib/auth/nextauth`),状态机仅保留 * 本轮迁移:所有认证登录调用(含 OAuth 社交登录)统一收口到状态机内。
* 邮箱注册/登录 + 通用 reset 事件。社交登录走 `new AuthPlatform("google").signIn()` / * 业务层只派发事件(`AuthEmailLoginSubmitted` / `AuthGoogleLoginSubmitted` / 等),
* `new AuthPlatform("facebook").signIn()` * 状态机通过 actors 调 `authRepository`(邮箱)或 `next-auth/react.signIn`OAuth
*/ */
import { setup, fromPromise, assign } from "xstate"; import { setup, fromPromise, assign } from "xstate";
import { signIn } from "next-auth/react";
import type { AuthProvider } from "@/lib/auth/auth_platform";
import type { LoginType } from "@/models/auth/login-type"; import type { LoginType } from "@/models/auth/login-type";
import { authRepository } from "@/data/repositories/auth_repository"; import { authRepository } from "@/data/repositories/auth_repository";
import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage";
@@ -68,6 +70,13 @@ const emailRegisterThenLoginActor = fromPromise<
return "email" as LoginType; return "email" as LoginType;
}); });
// OAuth 登录 actor:调 next-auth/react 的 signIn(provider) 触发 OAuth 跳转。
// 成功路径下 NextAuth 会重定向离开本应用,actor 通常不会 resolve
// onDone 仍保留以处理 SDK 同步返回(极少见)。
const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
await signIn(input);
});
// ============================================================ // ============================================================
// Machine // Machine
// ============================================================ // ============================================================
@@ -79,6 +88,7 @@ export const authMachine = setup({
actors: { actors: {
emailLogin: emailLoginActor, emailLogin: emailLoginActor,
emailRegisterThenLogin: emailRegisterThenLoginActor, emailRegisterThenLogin: emailRegisterThenLoginActor,
oauthSignIn: oauthSignInActor,
}, },
}).createMachine({ }).createMachine({
id: "auth", id: "auth",
@@ -113,10 +123,8 @@ export const authMachine = setup({
}, },
AuthEmailLoginSubmitted: "loadingEmailLogin", AuthEmailLoginSubmitted: "loadingEmailLogin",
AuthEmailRegisterSubmitted: "loadingEmailRegister", AuthEmailRegisterSubmitted: "loadingEmailRegister",
// 社交登录已迁移到 NextAuth + AuthPlatform 类 AuthGoogleLoginSubmitted: "loadingOAuth",
// 状态机保留事件以兼容旧调用方(实际不再触发) AuthFacebookLoginSubmitted: "loadingOAuth",
AuthGoogleLoginSubmitted: {},
AuthFacebookLoginSubmitted: {},
AuthAppleLoginSubmitted: { AuthAppleLoginSubmitted: {
actions: assign({ actions: assign({
errorMessage: "Apple login not implemented", errorMessage: "Apple login not implemented",
@@ -180,6 +188,31 @@ export const authMachine = setup({
}, },
}, },
loadingOAuth: {
entry: assign({ errorMessage: null }),
invoke: {
src: "oauthSignIn",
input: ({ event }) => {
// event 来自 idle 跳转(AuthGoogle/FacebookLoginSubmitted
if (event.type === "AuthGoogleLoginSubmitted") return event.provider;
if (event.type === "AuthFacebookLoginSubmitted") return event.provider;
return "google" as AuthProvider;
},
onDone: {
// 罕见分支:NextAuth 同步 resolve(实际几乎不会发生,
// OAuth 流程会通过重定向离开本应用)。
target: "idle",
},
onError: {
target: "idle",
actions: assign({
errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error),
}),
},
},
},
success: { success: {
// 终态:业务层通过 state.matches("success") 判断 // 终态:业务层通过 state.matches("success") 判断
}, },