refactor(splash): convert Skip to explicit guest login flow

- Splash Skip button now dispatches `AuthGuestLoginSubmitted` instead of
  direct routing, keeping guest auth under the state machine
- Update PWA install dialog copy ("Add to Home Screen") and drop favicon
  entry from manifest icons
- Add debug logging and routing sequence docs to splash-button
This commit is contained in:
2026-06-11 15:58:48 +08:00
parent e557f084c4
commit a6bc6941d4
9 changed files with 183 additions and 53 deletions
@@ -9,8 +9,6 @@
*/
import Image from "next/image";
import { AppConstants } from "@/core/constants/app_constants";
import styles from "./pwa-install-dialog.module.css";
export interface PwaInstallDialogProps {
@@ -48,10 +46,10 @@ export function PwaInstallDialog({ onClose, onInstall }: PwaInstallDialogProps)
priority
/>
<h2 id="pwa-dialog-title" className={styles.title}>
Install {AppConstants.appTitle}
Add to Home Screen
</h2>
<p className={styles.content}>
{AppConstants.appTitle} can be installed on your device for a faster, full-screen experience.
Add CozSweet to Home Screen{"\n"}for the best experience
</p>
<div className={styles.actions}>
<button
+31 -12
View File
@@ -3,17 +3,21 @@
* Splash 底部按钮组
*
* 原始 Dart: lib/ui/splash/widgets/splash_button.dart
* `Row(children: [Skip 文本, SizedBox 26, Facebook 登录按钮])`
* `Row(children: [Skip 文本, SizedBox 26, 社交登录按钮])`
*
* 本组件是 splash 鉴权流程的**自治单元**
* - 消费 `useSession()`next-auth/react)监听 NextAuth session
* - 消费 `useAuthState` / `useAuthDispatch`(邮箱登录 + OAuth 流程统一收口到状态机)
* - 消费 `useChatDispatch` / `useUserDispatch`(登录成功后初始化)
* - 自管路由跳转(已登录 / 登录成功 → /chat)
* - Skip 用 `<Link>` 保留 SPA 体验
* - **Skip 按钮 = 显式游客登录入口**(派发 `AuthGuestLoginSubmitted`,不是路由跳转)
* - 社交登录按钮仅派发 `AuthFacebookLoginSubmitted` 事件,由状态机负责调 NextAuth
*
* 跳转时序:
* 1. 派发事件(如 `AuthGuestLoginSubmitted`
* 2. auth machine 进 loading state → API 调用 → onDone → state.isSuccess = true
* 3. useEffect 监听到 isSuccess → 调 chat/user store 初始化 + router.replace("/chat")
*/
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useRef } from "react";
@@ -41,16 +45,28 @@ export function SplashButton() {
const router = useRouter();
const wasSuccess = useRef(false);
// 邮箱登录成功跳 /chat + 通知 chat/user 初始化
// 邮箱/OAuth/游客登录成功跳 /chat + 通知 chat/user 初始化
useEffect(() => {
// DEBUGisSuccess 状态透明化
console.log("[splash-button] useEffect state.isSuccess", {
isSuccess: state.isSuccess,
wasSuccess: wasSuccess.current,
willRedirect: state.isSuccess && !wasSuccess.current,
});
if (state.isSuccess && !wasSuccess.current) {
wasSuccess.current = true;
chatDispatch({ type: "ChatAuthStatusChanged" });
userDispatch({ type: "UserInit" });
console.log("[splash-button] useEffect → router.replace(/chat) [via isSuccess]");
router.replace(ROUTES.chat);
}
}, [state.isSuccess, chatDispatch, userDispatch, router]);
// Skip = 显式游客登录入口("以游客身份继续")
const handleSkip = () => {
authDispatch({ type: "AuthGuestLoginSubmitted" });
};
const handleFacebookLogin = () => {
authDispatch({
type: "AuthFacebookLoginSubmitted",
@@ -60,16 +76,19 @@ export function SplashButton() {
return (
<div className={styles.wrapper}>
{/* Skip 文本链接(next/link 渲染为 <a>,保留 SPA 体验 */}
<Link
href={ROUTES.chat}
{/* Skip 按钮 = 显式游客登录入口(派发 AuthGuestLoginSubmitted**不**做路由跳转 */}
<button
type="button"
onClick={handleSkip}
disabled={isLoading}
className={styles.skip}
aria-disabled={isLoading}
tabIndex={isLoading ? -1 : 0}
style={isLoading ? { pointerEvents: "none", opacity: 0.5 } : undefined}
>
Skip
</Link>
{isLoading ? (
<LoadingIndicator color="#ffffff" />
) : (
"Skip"
)}
</button>
<div className={styles.separator} aria-hidden="true" />
@@ -1,4 +1,11 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { MobileShell } from "@/app/_components/core/mobile-shell";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import { ROUTES } from "@/router/routes";
import { SplashBackground } from "./splash-background";
import { SplashLogo } from "./splash-logo";
@@ -7,6 +14,49 @@ import { SplashButton } from "./splash-button";
import styles from "./splash-screen.module.css";
export function SplashScreen() {
const router = useRouter();
// 启动时检查 localStorage 里的 token
// - loginTokenOAuth/email sync 写下的)→ 视为"已登录"跳 /chat
// - guestToken(用户**显式**走"游客模式"留下的)→ 同样视为"已登录"跳 /chat
// - 都没有 → 留在 splash(**不**自动创建游客账号 —— 见 auth-machine.ts 的 checkAuthStatusActor
//
// proxy 读不到 localStorageEdge runtime),所以这条 fallback 必须在 Client 侧。
//
// DEBUGclient 端**不**用 Loggerpino 引入会爆 client bundle),改用 console.log
// 临时调试,bug 修完即删。
useEffect(() => {
let cancelled = false;
void (async () => {
const storage = AuthStorage.getInstance();
// 用 getXxxToken() 拿 string | null(不是 hasXxxToken() 拿 boolean
const [loginR, guestR] = await Promise.all([
storage.getLoginToken(),
storage.getGuestToken(),
]);
const loginTok = loginR.success ? loginR.data : null;
const guestTok = guestR.success ? guestR.data : null;
const hasAny =
(loginTok != null && loginTok.length > 0) ||
(guestTok != null && guestTok.length > 0);
console.log("[splash] useEffect token check", {
loginToken: loginTok ? `${loginTok.slice(0, 8)}...` : null,
guestToken: guestTok ? `${guestTok.slice(0, 8)}...` : null,
hasAny,
willRedirect: hasAny && !cancelled,
});
if (!cancelled && hasAny) {
console.log("[splash] useEffect → router.replace(/chat)");
router.replace(ROUTES.chat);
}
})();
return () => {
cancelled = true;
};
}, [router]);
return (
<MobileShell background="var(--color-sidebar-background)">
<div className={styles.wrapper}>