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:
@@ -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` 为 true,UI 复用现有的 `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 事件保留无 payload;provider 由事件类型决定
|
||||||
|
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 用 isLoading;error 用 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` actor;XState 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,可保留
|
||||||
|
|||||||
@@ -7,14 +7,16 @@
|
|||||||
* 视觉规格(与 Dart 对齐):
|
* 视觉规格(与 Dart 对齐):
|
||||||
* - Spacer → logo(120h)→ Spacer → Facebook 主按钮 → 链接 → Spacer → 法律
|
* - Spacer → logo(120h)→ 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>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" };
|
||||||
|
|||||||
@@ -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") 判断
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user