refactor(auth): migrate social login to next-auth

This commit is contained in:
2026-06-10 10:26:45 +08:00
parent 109a3e3855
commit a4b902893e
34 changed files with 572 additions and 1421 deletions
+27 -27
View File
@@ -6,54 +6,54 @@
*
* 行为:
* - 默认显示「Continue with Facebook」+ 「Other sign-in options」入口
* - 真实 Facebook 登录在「Continue with Facebook」按钮中触发
* - 社交登录直接调 `nextauth.facebookLogin()` / `nextauth.googleLogin()`NextAuth 流程)
*/
import { useState } from "react";
import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
import { useAuthState } from "@/stores/auth/auth-context";
import { facebookLogin, googleLogin } from "@/lib/auth/nextauth";
import { AuthSocialButtons } from "./auth-social-buttons";
import { AuthDivider } from "./auth-divider";
import { AuthPrimaryButton } from "./auth-primary-button";
import { AuthErrorMessage } from "./auth-error-message";
import { AuthOtherOptionsDialog } from "./auth-other-options-dialog";
import { GoogleSignInButton } from "./google-sign-in-button";
import { AuthLegalText } from "./auth-legal-text";
import styles from "./auth-facebook-panel.module.css";
export interface AuthFacebookPanelProps {
onSwitchToEmail: () => void;
onGoogleSuccess: (idToken: string, email: string) => void;
}
export function AuthFacebookPanel({
onSwitchToEmail,
onGoogleSuccess,
}: AuthFacebookPanelProps) {
export function AuthFacebookPanel({ onSwitchToEmail }: AuthFacebookPanelProps) {
const state = useAuthState();
const dispatch = useAuthDispatch();
const [showOptions, setShowOptions] = useState(false);
const [busy, setBusy] = useState<null | "facebook" | "google">(null);
const handleFacebook = async () => {
setBusy("facebook");
const r = await facebookLogin();
setBusy(null);
if (!r.success) console.error("[auth-facebook-panel]", r.error);
};
const handleGoogle = async () => {
setBusy("google");
const r = await googleLogin();
setBusy(null);
if (!r.success) console.error("[auth-facebook-panel]", r.error);
};
return (
<div className={styles.panel}>
<AuthErrorMessage message={state.errorMessage} />
<AuthSocialButtons
loadingProvider={state.isLoading ? "facebook" : null}
onFacebook={() => dispatch({ type: "AuthFacebookLoginSubmitted" })}
onGoogle={() => dispatch({ type: "AuthGoogleLoginSubmitted" })}
googleSlot={
<GoogleSignInButton
onSuccess={(s) =>
onGoogleSuccess(s.idToken, s.email)
}
/>
}
loadingProvider={busy ?? (state.isLoading ? "facebook" : null)}
onFacebook={handleFacebook}
onGoogle={handleGoogle}
/>
<AuthDivider label="or" />
<AuthPrimaryButton
onClick={onSwitchToEmail}
variant="primary"
>
<AuthPrimaryButton onClick={onSwitchToEmail} variant="primary">
Continue with Email
</AuthPrimaryButton>
<button
@@ -69,21 +69,21 @@ export function AuthFacebookPanel({
onClose={() => setShowOptions(false)}
onFacebook={() => {
setShowOptions(false);
dispatch({ type: "AuthFacebookLoginSubmitted" });
void handleFacebook();
}}
onGoogle={() => {
setShowOptions(false);
dispatch({ type: "AuthGoogleLoginSubmitted" });
void handleGoogle();
}}
onApple={() => {
setShowOptions(false);
dispatch({ type: "AuthAppleLoginSubmitted" });
console.warn("Apple login not implemented");
}}
onEmail={() => {
setShowOptions(false);
onSwitchToEmail();
}}
loadingProvider={state.isLoading ? "facebook" : null}
loadingProvider={busy ?? (state.isLoading ? "facebook" : null)}
showApple={false}
/>
</div>
+1 -6
View File
@@ -10,11 +10,7 @@ import { AuthFacebookPanel } from "./auth-facebook-panel";
import { AuthEmailPanel } from "./auth-email-panel";
import styles from "./auth-panel.module.css";
export interface AuthPanelProps {
onGoogleSuccess: (idToken: string, email: string) => void;
}
export function AuthPanel({ onGoogleSuccess }: AuthPanelProps) {
export function AuthPanel() {
const state = useAuthState();
return (
<div className={styles.panel}>
@@ -26,7 +22,6 @@ export function AuthPanel({ onGoogleSuccess }: AuthPanelProps) {
// 但我们目前 AuthFacebookPanel 内部 click "Continue with Email" 直接调用 onSwitchToEmail
// 实际上本组件直接渲染对应 panel
}}
onGoogleSuccess={onGoogleSuccess}
/>
) : (
<AuthEmailPanel onBack={() => undefined} />
+3 -7
View File
@@ -16,7 +16,7 @@ import { useRouter } from "next/navigation";
import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
import { useChatDispatch } from "@/stores/chat/chat-context";
import { useUserDispatch } from "@/stores/user/user-context";
import { useAuthGate } from "@/lib/auth/use-auth-gate";
import { useAuthGate } from "@/lib/auth/nextauth";
import { ROUTES } from "@/lib/routes";
import { MobileShell } from "@/app/_components/core/mobile-shell";
import { AuthPanel } from "@/app/auth/components/auth-panel";
@@ -42,7 +42,7 @@ export function AuthScreen({
if (isAuthed) router.replace(ROUTES.chat);
}, [isAuthed, router]);
// 登录成功 → 通知 Chat + User + 跳转
// 邮箱登录成功 → 通知 Chat + User + 跳转
useEffect(() => {
if (state.isSuccess && !wasSuccess.current) {
wasSuccess.current = true;
@@ -63,11 +63,7 @@ export function AuthScreen({
padding: "var(--spacing-lg)",
}}
>
<AuthPanel
onGoogleSuccess={(idToken, email) => {
authDispatch({ type: "AuthWebGoogleLoginSuccess", idToken, email });
}}
/>
<AuthPanel />
</div>
</MobileShell>
);
@@ -1,50 +0,0 @@
"use client";
/**
* Google 登录按钮(GIS 渲染)
*
* 原始 Dart: lib/ui/auth/widgets/google_sign_in_web.dart
*
* 通过 `google.accounts.id.renderButton` 把 GIS 官方按钮渲染到指定 div。
* 登录成功由 `setOnGoogleSignInSuccess` 设置的回调处理。
*/
import { useEffect, useRef } from "react";
import {
renderGoogleButton,
setOnGoogleSignInSuccess,
type GoogleSignInSuccess,
} from "@/integrations/google-identity";
import styles from "./google-sign-in-button.module.css";
export interface GoogleSignInButtonProps {
onSuccess: (success: GoogleSignInSuccess) => void;
}
export function GoogleSignInButton({ onSuccess }: GoogleSignInButtonProps) {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
setOnGoogleSignInSuccess(onSuccess);
return () => setOnGoogleSignInSuccess(null);
}, [onSuccess]);
useEffect(() => {
if (!ref.current) return;
let cancelled = false;
// GIS 异步加载,轮询直到可用
const tryRender = () => {
if (cancelled) return;
if (window.google?.accounts?.id) {
renderGoogleButton(ref.current!, { theme: "outline", size: "large" });
} else {
setTimeout(tryRender, 100);
}
};
tryRender();
return () => {
cancelled = true;
};
}, []);
return <div ref={ref} className={styles.slot} />;
}
-1
View File
@@ -17,5 +17,4 @@ export * from "./auth-text-field";
export * from "./auth-validators";
export * from "./email-login-form";
export * from "./email-register-form";
export * from "./google-sign-in-button";
export * from "./__tests__/auth-validators.test";