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
+23 -31
View File
@@ -7,14 +7,16 @@
* 视觉规格(与 Dart 对齐):
* - Spacer → logo120h)→ Spacer → Facebook 主按钮 → 链接 → Spacer → 法律
* - 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" 链接打开底部弹层
* - 弹层 Email 按钮 → 触发 `onSwitchToEmail`(父级派发 `AuthPanelModeChanged`
*/
import Image from "next/image";
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 { AuthLegalText } from "./auth-legal-text";
@@ -27,31 +29,21 @@ export interface AuthFacebookPanelProps {
export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
const [showOptions, setShowOptions] = useState(false);
const [busy, setBusy] = useState<null | "facebook" | "google">(null);
const [error, setError] = useState<string | null>(null);
const { isLoading, errorMessage } = useAuthState();
const authDispatch = useAuthDispatch();
const handleFacebook = async () => {
setBusy("facebook");
setError(null);
try {
await new AuthPlatform("facebook").signIn();
} catch (e) {
setError(e instanceof Error ? e.message : "Facebook login failed");
} finally {
setBusy(null);
}
const handleFacebook = () => {
authDispatch({
type: "AuthFacebookLoginSubmitted",
provider: "facebook",
});
};
const handleGoogle = async () => {
setBusy("google");
setError(null);
try {
await new AuthPlatform("google").signIn();
} catch (e) {
setError(e instanceof Error ? e.message : "Google login failed");
} finally {
setBusy(null);
}
const handleGoogle = () => {
authDispatch({
type: "AuthGoogleLoginSubmitted",
provider: "google",
});
};
return (
@@ -70,10 +62,10 @@ export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
<button
type="button"
className={styles.facebookButton}
onClick={() => void handleFacebook()}
disabled={busy !== null}
onClick={() => handleFacebook()}
disabled={isLoading}
>
{busy === "facebook" ? (
{isLoading ? (
<span className={styles.spinner} aria-hidden="true" />
) : (
<span className={styles.facebookIcon} aria-hidden="true">
@@ -83,7 +75,7 @@ export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
</span>
)}
<span className={styles.facebookLabel}>
{busy === "facebook" ? "Logging in..." : "Login with Facebook"}
{isLoading ? "Logging in..." : "Login with Facebook"}
</span>
</button>
@@ -97,15 +89,15 @@ export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
<div className={styles.spacer} />
<AuthErrorMessage message={error} />
<AuthErrorMessage message={errorMessage} />
<AuthLegalText />
<AuthOtherOptionsDialog
open={showOptions}
onClose={() => setShowOptions(false)}
mode="facebook"
onFacebook={() => void handleFacebook()}
onGoogle={() => void handleGoogle()}
onFacebook={() => handleFacebook()}
onGoogle={() => handleGoogle()}
onEmail={onSwitchToEmail}
/>
</div>