diff --git a/src/app/auth/auth-screen.tsx b/src/app/auth/auth-screen.tsx
index 62031029..e83bb5f5 100644
--- a/src/app/auth/auth-screen.tsx
+++ b/src/app/auth/auth-screen.tsx
@@ -7,7 +7,8 @@
import { useEffect, useSyncExternalStore } from "react";
import { MobileShell } from "@/app/_components/core";
-import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
+import { useAuthState } from "@/stores/auth/auth-context";
+import { isAuthenticatedUser } from "@/router/navigation-resolver";
import { ROUTES } from "@/router/routes";
import { useAppNavigator } from "@/router/use-app-navigator";
@@ -19,7 +20,6 @@ const log = new Logger("AppAuthAuthScreen");
export function AuthScreen() {
const state = useAuthState();
- const authDispatch = useAuthDispatch();
const navigator = useAppNavigator();
const redirectTo = useSyncExternalStore(
subscribeLocationSnapshot,
@@ -28,28 +28,23 @@ export function AuthScreen() {
);
const safeRedirectTo = getSafeRedirectPath(redirectTo) ?? ROUTES.chat;
- // 邮箱登录成功 → 跳转;User hydrate 由根级 监听 loginStatus 变化处理。
- // 用 pendingRedirect flag(不是 useRef transition detection)——
- // re-mount 时 flag 在 auth context 里持久,区分"刚按了"和"re-visit"准确无误。
+ // 真实用户登录成功 → 跳转;User hydrate 由根级 监听 loginStatus 变化处理。
+ // 游客态不触发跳转,避免自动游客登录打断 auth 页面。
useEffect(() => {
- log.debug("[auth-screen] useEffect (loginStatus + pendingRedirect)", {
- pending: state.pendingRedirect,
+ const shouldRedirect = isAuthenticatedUser(state.loginStatus);
+ log.debug("[auth-screen] useEffect (loginStatus)", {
loginStatus: state.loginStatus,
- willRedirect:
- state.pendingRedirect && state.loginStatus !== "notLoggedIn",
+ willRedirect: shouldRedirect,
});
- if (state.pendingRedirect && state.loginStatus !== "notLoggedIn") {
- log.debug("[auth-screen] useEffect → navigator.replace [via pendingRedirect flag]", {
+ if (shouldRedirect) {
+ log.debug("[auth-screen] useEffect → navigator.replace", {
redirectTo: safeRedirectTo,
});
- authDispatch({ type: "AuthClearPendingRedirect" });
navigator.replace(safeRedirectTo);
}
}, [
state.loginStatus,
- state.pendingRedirect,
- authDispatch,
navigator,
safeRedirectTo,
]);
diff --git a/src/app/splash/components/splash-button.module.css b/src/app/splash/components/splash-button.module.css
index 19016031..d0842164 100644
--- a/src/app/splash/components/splash-button.module.css
+++ b/src/app/splash/components/splash-button.module.css
@@ -5,31 +5,13 @@
justify-content: center;
gap: clamp(var(--spacing-md, 12px), 4.815vw, var(--spacing-26, 26px));
width: 100%;
+ padding: 0 clamp(var(--spacing-sm, 8px), 2.963vw, var(--spacing-lg, 16px));
z-index: 2;
}
-.skip {
- font-size: var(--responsive-section-title, var(--font-size-xxl));
- font-weight: 700;
- font-style: italic;
- color: #ffffff;
- background: transparent;
- border: 0;
- cursor: pointer;
- padding: 0;
-}
-
-.skip:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.separator {
- width: clamp(var(--spacing-md, 12px), 4.815vw, var(--spacing-26, 26px));
- height: 1px;
-}
-
.facebookButton {
+ width: 100%;
+ max-width: 480px;
min-height: var(--responsive-control-height, 48px);
padding: 0 clamp(var(--spacing-lg, 16px), 5.556vw, 30px);
border: 0;
@@ -47,7 +29,7 @@
display: inline-flex;
align-items: center;
justify-content: center;
- flex: 0 0 auto;
+ flex: 1 1 auto;
box-shadow: 0 0 10px rgba(248, 89, 168, 0.3);
}
diff --git a/src/app/splash/components/splash-button.tsx b/src/app/splash/components/splash-button.tsx
index 6f46414e..6af03253 100644
--- a/src/app/splash/components/splash-button.tsx
+++ b/src/app/splash/components/splash-button.tsx
@@ -2,55 +2,30 @@
/**
* Splash 底部按钮组
*/
-import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
+import { useAuthState } from "@/stores/auth/auth-context";
import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
import styles from "./splash-button.module.css";
export interface SplashButtonProps {
- onSkip: () => void;
+ onStartChat: () => void;
}
-export function SplashButton({ onSkip }: SplashButtonProps) {
+export function SplashButton({ onStartChat }: SplashButtonProps) {
const state = useAuthState();
- const authDispatch = useAuthDispatch();
const isLoading = state.isLoading;
- const handleFacebookLogin = () => {
- authDispatch({
- type: "AuthFacebookLoginSubmitted",
- provider: "facebook",
- });
- };
-
return (
@@ -93,28 +53,8 @@ export function SplashScreen() {
-
+
- {/*
- * 登录错误提示 —— auth state machine 的 onError 会设 errorMessage
- * (如 Facebook 同步后端失败 / 后端返回数据 schema 不匹配等)。
- * 之前没显示,导致用户卡在 splash 看不到原因。
- */}
- {state.errorMessage ? (
-
- {state.errorMessage}
-
- ) : null}
Elio Silvestri, Your exclusive AI boyfriend
diff --git a/src/stores/auth/__tests__/auth-machine.test.ts b/src/stores/auth/__tests__/auth-machine.test.ts
index e8e496cc..473d3cc0 100644
--- a/src/stores/auth/__tests__/auth-machine.test.ts
+++ b/src/stores/auth/__tests__/auth-machine.test.ts
@@ -9,11 +9,13 @@ function createTestAuthMachine(
overrides: Partial<{
checkAuthStatus: LoginStatus;
guestLogin: LoginStatus;
+ guestLoginSpy: () => void;
emailLogin: LoginStatus;
logoutSpy: () => void;
}> = {},
) {
const logoutSpy = overrides.logoutSpy ?? vi.fn<() => void>();
+ const guestLoginSpy = overrides.guestLoginSpy ?? vi.fn<() => void>();
return authMachine.provide({
actors: {
@@ -21,7 +23,10 @@ function createTestAuthMachine(
async () => overrides.checkAuthStatus ?? "notLoggedIn",
),
guestLogin: fromPromise(
- async () => overrides.guestLogin ?? "guest",
+ async () => {
+ guestLoginSpy();
+ return overrides.guestLogin ?? "guest";
+ },
),
emailLogin: fromPromise<
LoginStatus,
@@ -72,9 +77,10 @@ describe("authMachine", () => {
actor.stop();
});
- it("initializes from storage without triggering a pending redirect", async () => {
+ it("initializes from storage without triggering guest login", async () => {
+ const guestLoginSpy = vi.fn<() => void>();
const actor = createActor(
- createTestAuthMachine({ checkAuthStatus: "facebook" }),
+ createTestAuthMachine({ checkAuthStatus: "facebook", guestLoginSpy }),
).start();
actor.send({ type: "AuthInit" });
@@ -83,26 +89,27 @@ describe("authMachine", () => {
const context = actor.getSnapshot().context;
expect(context.loginStatus).toBe("facebook");
expect(context.hasInitialized).toBe(true);
- expect(context.pendingRedirect).toBe(false);
+ expect(guestLoginSpy).not.toHaveBeenCalled();
actor.stop();
});
- it("guest login marks initialized but does not set pending redirect", async () => {
- const actor = createActor(createTestAuthMachine()).start();
+ it("automatically guest logs in when initialization finds no login state", async () => {
+ const guestLoginSpy = vi.fn<() => void>();
+ const actor = createActor(createTestAuthMachine({ guestLoginSpy })).start();
- actor.send({ type: "AuthGuestLoginSubmitted" });
+ actor.send({ type: "AuthInit" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
const context = actor.getSnapshot().context;
expect(context.loginStatus).toBe("guest");
expect(context.hasInitialized).toBe(true);
- expect(context.pendingRedirect).toBe(false);
+ expect(guestLoginSpy).toHaveBeenCalledOnce();
actor.stop();
});
- it("email login sets pending redirect until it is cleared", async () => {
+ it("email login updates login status", async () => {
const actor = createActor(createTestAuthMachine()).start();
actor.send({
@@ -113,10 +120,7 @@ describe("authMachine", () => {
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
expect(actor.getSnapshot().context.loginStatus).toBe("email");
- expect(actor.getSnapshot().context.pendingRedirect).toBe(true);
-
- actor.send({ type: "AuthClearPendingRedirect" });
- expect(actor.getSnapshot().context.pendingRedirect).toBe(false);
+ expect(actor.getSnapshot().context.hasInitialized).toBe(true);
actor.stop();
});
@@ -138,7 +142,6 @@ describe("authMachine", () => {
const context = actor.getSnapshot().context;
expect(logoutSpy).toHaveBeenCalledOnce();
expect(context.loginStatus).toBe("notLoggedIn");
- expect(context.pendingRedirect).toBe(false);
expect(context.hasInitialized).toBe(true);
actor.stop();
diff --git a/src/stores/auth/auth-actors.ts b/src/stores/auth/auth-actors.ts
index ee2e209d..8d9fc7b4 100644
--- a/src/stores/auth/auth-actors.ts
+++ b/src/stores/auth/auth-actors.ts
@@ -193,13 +193,13 @@ async function persistFacebookProfile(accessToken: string): Promise {
}
}
-// ========== 显式游客登录 actor(opt-in) ==========
-// 由 splash UI 派发 `AuthGuestLoginSubmitted` 触发,不自动跑。
+// ========== 自动游客登录 actor ==========
+// AuthInit 检查不到任何本地登录态时触发,用于自动补齐游客会话。
export const guestLoginActor = fromPromise(async () => {
log.debug("[auth-machine] guestLoginActor ENTRY");
- // 1. 先查本地 guestToken —— 已有就直接进 idle(带 pendingRedirect)
+ // 1. 先查本地 guestToken —— 已有就直接进入游客态
const hasTokenR = await AuthStorage.getInstance().hasGuestToken();
log.debug("[auth-machine] guestLoginActor hasGuestToken", {
success: hasTokenR.success,
diff --git a/src/stores/auth/auth-context.tsx b/src/stores/auth/auth-context.tsx
index 88b85f5f..ac70c41f 100644
--- a/src/stores/auth/auth-context.tsx
+++ b/src/stores/auth/auth-context.tsx
@@ -36,12 +36,6 @@ interface AuthState {
loginStatus: MachineContext["loginStatus"];
/** 启动时的本地登录态恢复是否已经完成 */
hasInitialized: MachineContext["hasInitialized"];
- /**
- * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮派完业务事件后置 true,
- * splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"`
- * 触发跳转 + 调 `AuthClearPendingRedirect` 置 false。
- */
- pendingRedirect: boolean;
}
const AuthStateCtx = createContext(null);
@@ -64,7 +58,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
username: state.context.username,
confirmPassword: state.context.confirmPassword,
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)/
- // OAuth 回调后端 sync / 显式游客登录
+ // OAuth 回调后端 sync / 自动游客登录
isLoading:
state.matches("loadingEmailLogin") ||
state.matches("loadingEmailRegister") ||
@@ -77,7 +71,6 @@ export function AuthProvider({ children }: AuthProviderProps) {
errorMessage: state.context.errorMessage,
loginStatus: state.context.loginStatus,
hasInitialized: state.context.hasInitialized,
- pendingRedirect: state.context.pendingRedirect,
}),
[state],
);
diff --git a/src/stores/auth/auth-events.ts b/src/stores/auth/auth-events.ts
index 4eabf42f..d77a1b7a 100644
--- a/src/stores/auth/auth-events.ts
+++ b/src/stores/auth/auth-events.ts
@@ -14,8 +14,6 @@ export type AuthEvent =
| { type: "AuthLogoutSubmitted" }
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
| { type: "AuthInit" }
- /** 显式游客登录 —— splash 上点"游客模式"按钮触发(不自动) */
- | { type: "AuthGuestLoginSubmitted" }
// 业务事件(提交)
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
| {
@@ -32,9 +30,4 @@ export type AuthEvent =
// OAuth 回调后:把 OAuth provider token 转交后端换业务 token
// 由 监听 useSession() 派发
| { type: "AuthGoogleSyncSubmitted"; idToken: string }
- | { type: "AuthFacebookSyncSubmitted"; accessToken: string }
- // ──────────────────────────────────────────────────────────────────────
- // splash / auth screen 跳完 /chat 后清"刚按了"信号(一次性):
- // `pendingRedirect` 在 login onDone action 里自动置 true(不是按钮派的)
- // —— 因此只需要 `AuthClearPendingRedirect` 一个事件
- | { type: "AuthClearPendingRedirect" };
+ | { type: "AuthFacebookSyncSubmitted"; accessToken: string };
diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts
index 8ba1eb7f..8af4f397 100644
--- a/src/stores/auth/auth-machine.ts
+++ b/src/stores/auth/auth-machine.ts
@@ -32,11 +32,7 @@ const toAuthErrorMessage = (error: unknown): string =>
//
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
//
-// `pendingRedirect: boolean`("用户显式触发了登录"信号)——
-// 在登录成功的 onDone 里自动置 true:
-// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
-// 跳完派 `AuthClearPendingRedirect` 置 false
-// `initializing` 不设 flag —— 启动 init 是被动读 token,不算"用户意图"。
+// 未找到任何登录态时,初始化流程会自动进入 guest login,补齐游客会话。
// ============================================================
export const authMachine = setup({
types: {
@@ -86,7 +82,6 @@ export const authMachine = setup({
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 派发
AuthInit: "initializing",
- AuthGuestLoginSubmitted: "loadingGuestLogin",
AuthEmailLoginSubmitted: "loadingEmailLogin",
AuthEmailRegisterSubmitted: "loadingEmailRegister",
AuthGoogleLoginSubmitted: "loadingOAuth",
@@ -100,10 +95,6 @@ export const authMachine = setup({
// OAuth 回调后 sync 入口 —— 由 派发
AuthGoogleSyncSubmitted: "syncingGoogleBackend",
AuthFacebookSyncSubmitted: "syncingFacebookBackend",
- // splash / auth screen 跳完 /chat 后清flag
- AuthClearPendingRedirect: {
- actions: assign({ pendingRedirect: false }),
- },
},
},
@@ -111,15 +102,26 @@ export const authMachine = setup({
entry: assign({ errorMessage: null }),
invoke: {
src: "checkAuthStatus",
- onDone: {
- target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
- actions: assign({
- loginStatus: ({ event }) => event.output,
- hasInitialized: true,
- // 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
- errorMessage: null,
- }),
- },
+ onDone: [
+ {
+ guard: ({ event }) => event.output === "notLoggedIn",
+ target: "loadingGuestLogin",
+ actions: assign({
+ loginStatus: "notLoggedIn",
+ // 自动游客登录仍属于初始化流程,完成前不标记 initialized。
+ hasInitialized: false,
+ errorMessage: null,
+ }),
+ },
+ {
+ target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
+ actions: assign({
+ loginStatus: ({ event }) => event.output,
+ hasInitialized: true,
+ errorMessage: null,
+ }),
+ },
+ ],
onError: {
target: "idle",
actions: assign({
@@ -139,8 +141,6 @@ export const authMachine = setup({
// 内联 assign —— XState v5 type inference 保留 loadingGuestLogin state 类型
actions: assign({
loginStatus: ({ event }) => event.output,
- // Skip 点击后已在 splash 里立即跳 /chat;guest login 不再依赖 pendingRedirect。
- pendingRedirect: false,
errorMessage: null,
hasInitialized: true,
}),
@@ -168,7 +168,6 @@ export const authMachine = setup({
target: "idle",
actions: assign({
loginStatus: ({ event }) => event.output,
- pendingRedirect: true, // ← 邮箱登录成功 → 跳
errorMessage: null,
hasInitialized: true,
}),
@@ -201,7 +200,6 @@ export const authMachine = setup({
target: "idle",
actions: assign({
loginStatus: ({ event }) => event.output,
- pendingRedirect: true, // ← 注册成功 → 跳
errorMessage: null,
hasInitialized: true,
}),
@@ -238,7 +236,7 @@ export const authMachine = setup({
},
loggingOut: {
- entry: assign({ errorMessage: null, pendingRedirect: false }),
+ entry: assign({ errorMessage: null }),
invoke: {
src: "logout",
onDone: {
@@ -271,7 +269,6 @@ export const authMachine = setup({
target: "idle",
actions: assign({
loginStatus: ({ event }) => event.output,
- pendingRedirect: true, // ← OAuth sync 成功 → 跳
errorMessage: null,
hasInitialized: true,
}),
@@ -298,7 +295,6 @@ export const authMachine = setup({
target: "idle",
actions: assign({
loginStatus: ({ event }) => event.output,
- pendingRedirect: true, // ← OAuth sync 成功 → 跳
errorMessage: null,
hasInitialized: true,
}),
diff --git a/src/stores/auth/auth-state.ts b/src/stores/auth/auth-state.ts
index 6a9bda0c..d71dd3a1 100644
--- a/src/stores/auth/auth-state.ts
+++ b/src/stores/auth/auth-state.ts
@@ -17,15 +17,6 @@ export interface AuthState {
loginStatus: LoginStatus;
/** 启动时的本地登录态恢复是否已经完成 */
hasInitialized: boolean;
- /**
- * 一次性的"刚按了"信号 —— Skip / Facebook / 邮箱登录按钮刚派事件 → 置 true
- * → splash-screen / auth-screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 触发跳转
- * → 跳完置 false
- *
- * 历史:之前用 useRef 做 transition detection,但 re-mount 时 ref 重置,bug 复发。
- * 现在 auth state 里持久(machine 不重置)—— re-mount 时也能区分"刚按"和"历史"。
- */
- pendingRedirect: boolean;
}
export const initialState: AuthState = {
@@ -38,5 +29,4 @@ export const initialState: AuthState = {
errorMessage: null,
loginStatus: "notLoggedIn",
hasInitialized: false,
- pendingRedirect: false,
};