c07a10ee80
Previously, the Skip button (guest login entry) relied on the auth state machine's `pendingRedirect` flag to trigger the navigation to /chat after guest login completed. That introduced a non-obvious coupling: the button dispatched an event, the machine ran an actor, the auth screen (or splash useEffect) saw the flag and redirected. This refactor moves the redirect into the splash screen itself: - `SplashButton` now takes an `onSkip` prop and no longer knows about auth dispatch. Pure presentation component. - `SplashScreen` provides `handleSkip` that dispatches the `AuthGuestLoginSubmitted` event AND immediately calls `router.replace(/chat)` for snappier perceived navigation. - `auth-machine.ts`: `loadingGuestLogin.onDone` no longer sets `pendingRedirect: true` (splash already navigated). Sets it to `false` explicitly so other screens (auth, sidebar) that also react to `pendingRedirect` don't double-navigate if the user triggers guest login from a non-splash surface in the future. No behavior change for the happy path: Skip → /chat works the same. The refactor is purely about responsibility allocation and component decoupling.
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
|
||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||
import { useUserDispatch } from "@/stores/user/user-context";
|
||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||
import { ROUTES } from "@/router/routes";
|
||
|
||
import { SplashBackground } from "./splash-background";
|
||
import { SplashLogo } from "./splash-logo";
|
||
import { SplashContent } from "./splash-content";
|
||
import { SplashButton } from "./splash-button";
|
||
import styles from "./splash-screen.module.css";
|
||
|
||
export function SplashScreen() {
|
||
const router = useRouter();
|
||
const state = useAuthState();
|
||
const authDispatch = useAuthDispatch();
|
||
const userDispatch = useUserDispatch();
|
||
|
||
const handleSkip = () => {
|
||
authDispatch({ type: "AuthGuestLoginSubmitted" });
|
||
router.replace(ROUTES.chat);
|
||
};
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// useEffect ① 启动时检查 localStorage 里的 loginToken:
|
||
// - loginToken(OAuth/email sync 写下的)→ 视为"已登录"跳 /chat
|
||
// - 不查 guestToken —— 游客用户停留 splash(按你要求)
|
||
// - 都没有 → 留 splash
|
||
// proxy 读不到 localStorage(Edge runtime),所以这条 fallback 必须在 Client 侧。
|
||
// ─────────────────────────────────────────────────────────────
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
void (async () => {
|
||
// 用 getLoginToken() 拿 string | null(不是 hasLoginToken() 拿 boolean)
|
||
const r = await AuthStorage.getInstance().getLoginToken();
|
||
if (!cancelled && r.success && r.data) {
|
||
router.replace(ROUTES.chat);
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [router]);
|
||
|
||
// ─────────────────────────────────────────────────────────────
|
||
// useEffect ② 按钮刚派了事件(pendingRedirect=true) +
|
||
// loginStatus 是非 notLoggedIn → 跳 /chat
|
||
//
|
||
// 关键:`pendingRedirect` 是 auth context 里的持久状态(不是 ref)——
|
||
// re-mount 时不重置,区分"刚按了"和"历史登录"准确无误。
|
||
// 跳完调 `AuthClearPendingRedirect` 置 false(一次性信号)。
|
||
// ─────────────────────────────────────────────────────────────
|
||
useEffect(() => {
|
||
// 调试日志(保留,不删 —— 用于排查"re-mount 误跳 /chat"问题)
|
||
console.log("[splash] useEffect ② (loginStatus + pendingRedirect)", {
|
||
pending: state.pendingRedirect,
|
||
loginStatus: state.loginStatus,
|
||
willRedirect:
|
||
state.pendingRedirect && state.loginStatus !== "notLoggedIn",
|
||
});
|
||
|
||
if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") {
|
||
console.log(
|
||
"[splash] useEffect ② → router.replace(/chat) [via pendingRedirect flag]",
|
||
);
|
||
authDispatch({ type: "AuthClearPendingRedirect" });
|
||
userDispatch({ type: "UserInit" });
|
||
router.replace(ROUTES.chat);
|
||
}
|
||
}, [
|
||
state.loginStatus,
|
||
state.pendingRedirect,
|
||
authDispatch,
|
||
userDispatch,
|
||
router,
|
||
]);
|
||
|
||
return (
|
||
<MobileShell background="var(--color-sidebar-background)">
|
||
<div className={styles.wrapper}>
|
||
<SplashBackground />
|
||
{/* 渐变叠加层:accent → transparent (bottom-left → center-right) */}
|
||
<div className={styles.gradientOverlay} aria-hidden="true" />
|
||
<div className={styles.content}>
|
||
<SplashLogo />
|
||
<div className={styles.spacer} />
|
||
<SplashContent />
|
||
<div className={styles.buttonArea}>
|
||
<SplashButton onSkip={handleSkip} />
|
||
</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 />
|
||
24/7 online | Chat | Companion | Heal | Sweet moments
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</MobileShell>
|
||
);
|
||
}
|