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:
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
// DEBUG:isSuccess 状态透明化
|
||||
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:
|
||||
// - loginToken(OAuth/email sync 写下的)→ 视为"已登录"跳 /chat
|
||||
// - guestToken(用户**显式**走"游客模式"留下的)→ 同样视为"已登录"跳 /chat
|
||||
// - 都没有 → 留在 splash(**不**自动创建游客账号 —— 见 auth-machine.ts 的 checkAuthStatusActor)
|
||||
//
|
||||
// proxy 读不到 localStorage(Edge runtime),所以这条 fallback 必须在 Client 侧。
|
||||
//
|
||||
// DEBUG:client 端**不**用 Logger(pino 引入会爆 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}>
|
||||
|
||||
Reference in New Issue
Block a user