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
+47 -22
View File
@@ -40,9 +40,6 @@ async function readGuestId(): Promise<string | undefined> {
// ============================================================
// Actors(异步服务)
// ============================================================
// 邮箱登录 actor 仅调 authRepository.emailLogin 拿业务 token。
// 本轮起不再调 /api/auth/marker 写 cookie(统一迁到 NextAuth session
// cookie 检测,邮箱登录 proxy 行为在后续轮恢复)。
const emailLoginActor = fromPromise<LoginStatus, { email: string; password: string }>(
async ({ input }) => {
@@ -79,7 +76,7 @@ const oauthSignInActor = fromPromise<void, AuthProvider>(async ({ input }) => {
await signIn(input);
});
// ========== 新增:OAuth token → 后端业务 token sync actors ==========
// ========== OAuth token → 后端业务 token sync actors ==========
// 由 <OAuthSessionSync /> 在 NextAuth 回调后派发(监听 useSession())。
// 用 OAuth provider 的 idToken / accessToken 调后端换业务 token + user。
// 后端返回的 LoginResponse 已经被 authRepository._saveLoginData 持久化。
@@ -109,24 +106,35 @@ const syncFacebookBackendActor = fromPromise<
return "facebook" as LoginStatus;
});
// ========== 新增:启动 / 恢复时检查登录 actor ==========
// ========== 显式游客登录 actoropt-in ==========
// 由 splash UI 派发 `AuthGuestLoginSubmitted` 触发,**不**自动跑。
// 取代了之前 checkAuthStatusActor 的 auto-guest-login 行为 —— 避免每次
// 访 splash 都自动创建游客账号污染后端。
const guestLoginActor = fromPromise<LoginStatus, void>(async () => {
const deviceId = await deviceIdentifier.getDeviceId();
const result = await authRepository.guestLogin(deviceId);
if (Result.isErr(result)) throw result.error;
return "guest" as LoginStatus;
});
// ========== 启动 / 恢复时检查登录态 actor(只查不创) ==========
// 由 <AuthStatusChecker /> 在 mount 时派发(一次性)。
// 流程:
// 流程**纯只读**
// 1. deviceIdentifier.getDeviceId() —— 无则通过 fingerprintjs 生成 + 落盘
// 2. AuthStorage.getLoginToken() —— 有 → "email"(具体 provider 由 OAuthSessionSync 后续覆盖
// (给后续 OAuth/email 登录准备 deviceId**不**在 splash 首次访问就 auto-create 游客
// 2. AuthStorage.getLoginToken() —— 有 → "email"
// 3. AuthStorage.getGuestToken() —— 有 → "guest"
// 4. 都没有 → authRepository.guestLogin(deviceId) 内部自动 setGuestToken + setDeviceId + setUserId
// 4. 都没有 → "notLoggedIn"(让用户**显式**选 OAuth / Email / Guest 入口)
const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
// 1. 拿 deviceId
const deviceId = await deviceIdentifier.getDeviceId();
// 1. 拿 deviceId**不**用于 auto-guest-login,仅供后续登录用)
await deviceIdentifier.getDeviceId();
const storage = AuthStorage.getInstance();
// 2. 查 loginTokenOAuth / 邮箱登录后端写下的)
// 2. 查 loginToken
const loginTokenR = await storage.getLoginToken();
if (loginTokenR.success && loginTokenR.data) {
// token 字符串无法区分 google/facebook/email/apple
// 默认 "email" —— 若实际是 OAuthOAuthSessionSync 后续会通过 AuthGoogle/FacebookSyncSubmitted 覆盖
return "email" as LoginStatus;
}
@@ -136,10 +144,8 @@ const checkAuthStatusActor = fromPromise<LoginStatus, void>(async () => {
return "guest" as LoginStatus;
}
// 4. 都没有 —— 调游客登录接口(内部自动 setGuestToken + setDeviceId + setUserId
const result = await authRepository.guestLogin(deviceId);
if (Result.isErr(result)) throw result.error;
return "guest" as LoginStatus;
// 4. 都没有 → "notLoggedIn"(保持 splash 页面 + 登录入口
return "notLoggedIn" as LoginStatus;
});
// ============================================================
@@ -156,6 +162,7 @@ export const authMachine = setup({
oauthSignIn: oauthSignInActor,
syncGoogleBackend: syncGoogleBackendActor,
syncFacebookBackend: syncFacebookBackendActor,
guestLogin: guestLoginActor,
checkAuthStatus: checkAuthStatusActor,
},
}).createMachine({
@@ -191,6 +198,8 @@ export const authMachine = setup({
},
// 启动 / 恢复时检查登录态 —— 由 <AuthStatusChecker /> 派发
AuthStatusCheckSubmitted: "checkingAuthStatus",
// 显式游客登录 —— 由 splash UI 派发(**不**自动)
AuthGuestLoginSubmitted: "loadingGuestLogin",
AuthEmailLoginSubmitted: "loadingEmailLogin",
AuthEmailRegisterSubmitted: "loadingEmailRegister",
AuthGoogleLoginSubmitted: "loadingOAuth",
@@ -227,6 +236,27 @@ export const authMachine = setup({
},
},
loadingGuestLogin: {
entry: assign({ errorMessage: null }),
invoke: {
src: "guestLogin",
onDone: {
target: "success",
actions: assign({
loginStatus: ({ event }) => event.output,
errorMessage: null,
}),
},
onError: {
target: "idle",
actions: assign({
errorMessage: ({ event }) =>
event.error instanceof Error ? event.error.message : String(event.error),
}),
},
},
},
loadingEmailLogin: {
invoke: {
src: "emailLogin",
@@ -287,14 +317,11 @@ export const authMachine = setup({
invoke: {
src: "oauthSignIn",
input: ({ event }) => {
// event 来自 idle 跳转(AuthGoogle/FacebookLoginSubmitted
if (event.type === "AuthGoogleLoginSubmitted") return event.provider;
if (event.type === "AuthFacebookLoginSubmitted") return event.provider;
return "google" as AuthProvider;
},
onDone: {
// 罕见分支:NextAuth 同步 resolve(实际几乎不会发生,
// OAuth 流程会通过重定向离开本应用)。
target: "idle",
},
onError: {
@@ -307,13 +334,11 @@ export const authMachine = setup({
},
},
// ========== 新增:OAuth token → 后端 sync 状态 ==========
syncingGoogleBackend: {
entry: assign({ errorMessage: null }),
invoke: {
src: "syncGoogleBackend",
input: ({ event }) => {
// event 来自 idle 跳转(AuthGoogleSyncSubmitted
if (event.type !== "AuthGoogleSyncSubmitted") return { idToken: "" };
return { idToken: event.idToken };
},