refactor(auth): simplify splash guest entry

This commit is contained in:
2026-07-03 15:57:01 +08:00
parent 79324defcf
commit b22f23bcc4
10 changed files with 72 additions and 205 deletions
+9 -14
View File
@@ -7,7 +7,8 @@
import { useEffect, useSyncExternalStore } from "react";
import { MobileShell } from "@/app/_components/core";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useAuthState } from "@/stores/auth/auth-context";
import { isAuthenticatedUser } from "@/router/navigation-resolver";
import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator";
@@ -19,7 +20,6 @@ const log = new Logger("AppAuthAuthScreen");
export function AuthScreen() {
const state = useAuthState();
const authDispatch = useAuthDispatch();
const navigator = useAppNavigator();
const redirectTo = useSyncExternalStore(
subscribeLocationSnapshot,
@@ -28,28 +28,23 @@ export function AuthScreen() {
);
const safeRedirectTo = getSafeRedirectPath(redirectTo) ?? ROUTES.chat;
// 邮箱登录成功 → 跳转;User hydrate 由根级 <UserAuthSync /> 监听 loginStatus 变化处理。
// 用 pendingRedirect flag(不是 useRef transition detection)——
// re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。
// 真实用户登录成功 → 跳转;User hydrate 由根级 <UserAuthSync /> 监听 loginStatus 变化处理。
// 游客态不触发跳转,避免自动游客登录打断 auth 页面。
useEffect(() => {
log.debug("[auth-screen] useEffect (loginStatus + pendingRedirect)", {
pending: state.pendingRedirect,
const shouldRedirect = isAuthenticatedUser(state.loginStatus);
log.debug("[auth-screen] useEffect (loginStatus)", {
loginStatus: state.loginStatus,
willRedirect:
state.pendingRedirect && state.loginStatus !== "notLoggedIn",
willRedirect: shouldRedirect,
});
if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") {
log.debug("[auth-screen] useEffect → navigator.replace [via pendingRedirect flag]", {
if (shouldRedirect) {
log.debug("[auth-screen] useEffect → navigator.replace", {
redirectTo: safeRedirectTo,
});
authDispatch({ type: "AuthClearPendingRedirect" });
navigator.replace(safeRedirectTo);
}
}, [
state.loginStatus,
state.pendingRedirect,
authDispatch,
navigator,
safeRedirectTo,
]);
@@ -5,31 +5,13 @@
justify-content: center;
gap: clamp(var(--spacing-md, 12px), 4.815vw, var(--spacing-26, 26px));
width: 100%;
padding: 0 clamp(var(--spacing-sm, 8px), 2.963vw, var(--spacing-lg, 16px));
z-index: 2;
}
.skip {
font-size: var(--responsive-section-title, var(--font-size-xxl));
font-weight: 700;
font-style: italic;
color: #ffffff;
background: transparent;
border: 0;
cursor: pointer;
padding: 0;
}
.skip:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.separator {
width: clamp(var(--spacing-md, 12px), 4.815vw, var(--spacing-26, 26px));
height: 1px;
}
.facebookButton {
width: 100%;
max-width: 480px;
min-height: var(--responsive-control-height, 48px);
padding: 0 clamp(var(--spacing-lg, 16px), 5.556vw, 30px);
border: 0;
@@ -47,7 +29,7 @@
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
flex: 1 1 auto;
box-shadow: 0 0 10px rgba(248, 89, 168, 0.3);
}
+5 -30
View File
@@ -2,55 +2,30 @@
/**
* Splash 底部按钮组
*/
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useAuthState } from "@/stores/auth/auth-context";
import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
import styles from "./splash-button.module.css";
export interface SplashButtonProps {
onSkip: () => void;
onStartChat: () => void;
}
export function SplashButton({ onSkip }: SplashButtonProps) {
export function SplashButton({ onStartChat }: SplashButtonProps) {
const state = useAuthState();
const authDispatch = useAuthDispatch();
const isLoading = state.isLoading;
const handleFacebookLogin = () => {
authDispatch({
type: "AuthFacebookLoginSubmitted",
provider: "facebook",
});
};
return (
<div className={styles.wrapper}>
{/* Skip 按钮 = 显式游客登录入口(派发事件 + 立即跳 /chat) */}
<button
type="button"
onClick={onSkip}
disabled={isLoading}
className={styles.skip}
>
{isLoading ? (
<LoadingIndicator color="#ffffff" />
) : (
"Skip"
)}
</button>
<div className={styles.separator} aria-hidden="true" />
{/* 社交登录按钮(NextAuth Facebook OAuth */}
<button
type="button"
onClick={handleFacebookLogin}
onClick={onStartChat}
disabled={isLoading}
className={styles.facebookButton}
>
{isLoading ? (
<LoadingIndicator color="#ffffff" />
) : (
<span className={styles.facebookLabel}>Login with Facebook</span>
<span className={styles.facebookLabel}>Start Chatting</span>
)}
</button>
</div>
+10 -70
View File
@@ -3,10 +3,9 @@
import { useEffect } from "react";
import { MobileShell } from "@/app/_components/core";
import { hasBusinessLoginToken } from "@/lib/auth/auth_session";
import { useAppNavigator } from "@/router/use-app-navigator";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { pwaUtil, Logger } from "@/utils";
import { useAuthState } from "@/stores/auth/auth-context";
import { pwaUtil } from "@/utils";
import {
SplashBackground,
@@ -16,7 +15,7 @@ import {
} from "./components";
import styles from "./components/splash-screen.module.css";
const log = new Logger("AppSplashSplashScreen");
const AUTO_OPEN_CHAT_DELAY_MS = 1800;
// 尽量早地缓存 beforeinstallprompt,避免用户停留 splash 时浏览器事件已触发并丢失。
if (typeof window !== "undefined") {
@@ -26,10 +25,8 @@ if (typeof window !== "undefined") {
export function SplashScreen() {
const navigator = useAppNavigator();
const state = useAuthState();
const authDispatch = useAuthDispatch();
const handleSkip = () => {
authDispatch({ type: "AuthGuestLoginSubmitted" });
const handleStartChat = () => {
navigator.openChat({ replace: true });
};
@@ -37,50 +34,13 @@ export function SplashScreen() {
pwaUtil.prepareInstallPrompt();
}, []);
// ─────────────────────────────────────────────────────────────
// useEffect ① 启动时检查 localStorage 里的 loginToken
// - loginTokenOAuth/email sync 写下的)→ 视为"已登录"跳 /chat
// - 不查 guestToken —— 游客用户停留 splash(按你要求)
// - 都没有 → 留 splash
// proxy 读不到 localStorageEdge runtime),所以这条 fallback 必须在 Client 侧。
// ─────────────────────────────────────────────────────────────
useEffect(() => {
let cancelled = false;
void (async () => {
if (!cancelled && (await hasBusinessLoginToken())) {
navigator.openChat({ replace: true });
}
})();
return () => {
cancelled = true;
};
}, [navigator]);
// ─────────────────────────────────────────────────────────────
// useEffect ② 按钮刚派了事件(pendingRedirect=true +
// loginStatus 是非 notLoggedIn → 跳 /chat
//
// 关键:`pendingRedirect` 是 auth context 里的持久状态(不是 ref)——
// re-mount 时不重置,区分"刚按了"和"历史登录"准确无误。
// 跳完调 `AuthClearPendingRedirect` 置 false(一次性信号)。
// ─────────────────────────────────────────────────────────────
useEffect(() => {
// 调试日志(保留,不删 —— 用于排查"re-mount 误跳 /chat"问题)
log.debug("[splash] useEffect ② (loginStatus + pendingRedirect)", {
pending: state.pendingRedirect,
loginStatus: state.loginStatus,
willRedirect:
state.pendingRedirect && state.loginStatus !== "notLoggedIn",
});
if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") {
log.debug(
"[splash] useEffect ② → navigator.openChat [via pendingRedirect flag]",
);
authDispatch({ type: "AuthClearPendingRedirect" });
const timer = window.setTimeout(() => {
navigator.openChat({ replace: true });
}
}, [state.loginStatus, state.pendingRedirect, authDispatch, navigator]);
}, AUTO_OPEN_CHAT_DELAY_MS);
return () => window.clearTimeout(timer);
}, [navigator]);
return (
<MobileShell background="var(--color-sidebar-background)">
@@ -93,28 +53,8 @@ export function SplashScreen() {
<div className={styles.spacer} />
<SplashContent />
<div className={styles.buttonArea}>
<SplashButton onSkip={handleSkip} />
<SplashButton onStartChat={handleStartChat} />
</div>
{/*
* 登录错误提示 —— auth state machine 的 onError 会设 errorMessage
* (如 Facebook 同步后端失败 / 后端返回数据 schema 不匹配等)。
* 之前没显示,导致用户卡在 splash 看不到原因。
*/}
{state.errorMessage ? (
<p
role="alert"
style={{
color: "#c0392b",
fontSize: "0.875rem",
marginTop: "1rem",
textAlign: "center",
maxWidth: "320px",
lineHeight: 1.4,
}}
>
{state.errorMessage}
</p>
) : null}
<p className={styles.bottom}>
Elio Silvestri, Your exclusive AI boyfriend
<br />