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:
@@ -55,11 +55,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
password: state.context.password,
|
||||
username: state.context.username,
|
||||
confirmPassword: state.context.confirmPassword,
|
||||
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)
|
||||
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)/ 显式游客登录
|
||||
isLoading:
|
||||
state.matches("loadingEmailLogin") ||
|
||||
state.matches("loadingEmailRegister") ||
|
||||
state.matches("loadingOAuth"),
|
||||
state.matches("loadingOAuth") ||
|
||||
state.matches("loadingGuestLogin"),
|
||||
errorMessage: state.context.errorMessage,
|
||||
isSuccess: state.matches("success"),
|
||||
loginStatus: state.context.loginStatus,
|
||||
|
||||
@@ -13,8 +13,10 @@ export type AuthEvent =
|
||||
| { type: "AuthModeChanged"; mode: AuthMode }
|
||||
| { type: "AuthFormCleared" }
|
||||
| { type: "AuthReset" }
|
||||
/** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token → 必要时游客登录 */
|
||||
/** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token(**不**自动创建游客账号) */
|
||||
| { type: "AuthStatusCheckSubmitted" }
|
||||
/** 显式游客登录 —— splash 上点"游客模式"按钮触发(**不**自动) */
|
||||
| { type: "AuthGuestLoginSubmitted" }
|
||||
// 业务事件(提交)
|
||||
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
|
||||
| {
|
||||
|
||||
@@ -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 ==========
|
||||
// ========== 显式游客登录 actor(opt-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. 查 loginToken(OAuth / 邮箱登录后端写下的)
|
||||
// 2. 查 loginToken
|
||||
const loginTokenR = await storage.getLoginToken();
|
||||
if (loginTokenR.success && loginTokenR.data) {
|
||||
// token 字符串无法区分 google/facebook/email/apple
|
||||
// 默认 "email" —— 若实际是 OAuth,OAuthSessionSync 后续会通过 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 };
|
||||
},
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
* → 业务 token 写入本地 storage
|
||||
*
|
||||
* 此组件**无可见 UI**(返回 null),仅作为 NextAuth session 与 auth state
|
||||
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后:
|
||||
* <AuthProvider> ← 提供 useAuthState / useAuthDispatch
|
||||
* <OAuthSessionSync /> ← 用上面两个 hook
|
||||
* </AuthProvider>
|
||||
* machine 之间的桥接器。挂在 RootProviders 内、AuthProvider 之后。
|
||||
*
|
||||
* DEBUG:client 端**不**用 Logger(pino 引入会爆 client bundle),改用 console.log
|
||||
* 临时调试,bug 修完即删。
|
||||
*/
|
||||
import { useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
@@ -26,6 +26,22 @@ export function OAuthSessionSync() {
|
||||
const { loginStatus } = useAuthState();
|
||||
|
||||
useEffect(() => {
|
||||
// DEBUG:每条 session 变化都打(区分 OAuth 残留 session 是否触发 sync)
|
||||
console.log("[OAuthSessionSync] session change", {
|
||||
status,
|
||||
hasSession: !!session,
|
||||
provider: session?.provider,
|
||||
idToken: session?.idToken ? `${session.idToken.slice(0, 8)}...` : null,
|
||||
accessToken: session?.accessToken
|
||||
? `${session.accessToken.slice(0, 8)}...`
|
||||
: null,
|
||||
loginStatus,
|
||||
willDispatch:
|
||||
status === "authenticated" &&
|
||||
!!session?.provider &&
|
||||
loginStatus !== session.provider,
|
||||
});
|
||||
|
||||
// 1) NextAuth 还没就绪
|
||||
if (status !== "authenticated" || !session) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user