fix(auth): defer guest login until chat entry

This commit is contained in:
2026-07-07 14:11:36 +08:00
parent fd96cdb3d6
commit 2aaf60a497
8 changed files with 84 additions and 29 deletions
+19 -1
View File
@@ -95,13 +95,31 @@ describe("authMachine", () => {
actor.stop();
});
it("automatically guest logs in when initialization finds no login state", async () => {
it("initializes as notLoggedIn without automatically guest logging in", async () => {
const guestLoginSpy = vi.fn<() => void>();
const actor = createActor(createTestAuthMachine({ guestLoginSpy })).start();
actor.send({ type: "AuthInit" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
const context = actor.getSnapshot().context;
expect(context.loginStatus).toBe("notLoggedIn");
expect(context.hasInitialized).toBe(true);
expect(guestLoginSpy).not.toHaveBeenCalled();
actor.stop();
});
it("guest logs in only after explicit guest login submission", async () => {
const guestLoginSpy = vi.fn<() => void>();
const actor = createActor(createTestAuthMachine({ guestLoginSpy })).start();
actor.send({ type: "AuthInit" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
actor.send({ type: "AuthGuestLoginSubmitted" });
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
const context = actor.getSnapshot().context;
expect(context.loginStatus).toBe("guest");
expect(context.hasInitialized).toBe(true);
+1 -1
View File
@@ -58,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") ||
+1
View File
@@ -15,6 +15,7 @@ export type AuthEvent =
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
| { type: "AuthInit" }
// 业务事件(提交)
| { type: "AuthGuestLoginSubmitted" }
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
| {
type: "AuthEmailRegisterSubmitted";
+24 -22
View File
@@ -32,7 +32,7 @@ const toAuthErrorMessage = (error: unknown): string =>
//
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
//
// 未找到任何登录态时,初始化流程会自动进入 guest login,补齐游客会话
// 初始化只恢复已有登录态;游客登录必须由用户明确操作触发
// ============================================================
export const authMachine = setup({
types: {
@@ -49,6 +49,13 @@ export const authMachine = setup({
checkAuthStatus: checkAuthStatusActor,
logout: logoutActor,
},
guards: {
canSubmitGuestLogin: ({ context }) =>
context.loginStatus === "notLoggedIn",
canLogoutToGuest: ({ context }) =>
context.loginStatus !== "notLoggedIn" &&
context.loginStatus !== "guest",
},
}).createMachine({
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgOgJYQBswBiAQQ0wAVkA7MAgWQHsIwBhTWmCAbQAYAuolAAHJrFzpcTGsJAAPRABYA7NgAc6gEzqAjCvUBOLbvUqlWgDQgAnohWnsAZgCsSpXyeHdTgGy6lXwBfIOs0LDxCEnIsZlYOLkh+ISQQMQkpGTlFBF81Y1MzX3UvdT5NaztcrS1sD10tQ1dvPi0+YNCQcJx8IjIKADEmACcAWzYiZGGkwTl0yWlZVJzzbHMarSUvNpctYsrEXd86pT0SvfULXyUQsIpIvpjMACU4MHRkufEFrOXEXT4fGwvkMLicmxUTT4qj4hgOCC2QKUoL4KkaShc5ycty69160QoAEkaJJPql5pklqAcl4nGtDCCtGDdqotCp4YY9MC0Xw3CpfH5Gi4cd0HgSsABxVBwdAAGSYUFwNAAyqgAEajSToGYpUTfSnZRC0+mM5kQtnwlzlDQuQynEpOJz6BoqEV4qL9LAAUVGyFwBHlipV6s16G1vFm5P1i0NCGNkNNrnN7Ns9gx2D2LhUDjcl0BN06ovxnswPr9BFeitg2uGqo1Wp1XwyMb+caaJuMZtZKaquhBQKMsOhHiafY6dwixaeEqYCqIgaVddD4bJeubv2pRvbCc7Se78P0SjqYLaewuTtObsnHqeA2QAGMwGrZwBrBfB+thxtR9dUhRbukdyZPc0R7RAaiPUEnRKREjH0dQrx6G8KFIEQRHnBVFxDBsI11NJow3f8EVqVRfCzJ14L4Qp4UdI83F0Bkz3BJxtEQsUSxnOcwGVGwaHvJccNXfDf1jeMGV3FlQIPDEj1OQITAYjwijYqdBgfJ9Xx4viBK-XCmx+P8aW3cTgMki1UwQBpHFRB1IQMdRMXHXFr0eCgJjAKZKDAGgICVKBXl86Z7w+SM1wM2MLGwQFopimKAnhLQWOwTFNF2JxAWuDwtBUkkpGQAhcAALz84gIBkMA8BoAA3JgXwqotctwfKir8hAlRq+9kEpZIhIpFtNwRAVkt8JkdCaXxWhMeFNHUNZTEMTwXExaEXF0HKFma4qaCgYgwGGYYRmwdCuoAMxGUZsAajaCq2qA2uqphOu6wReoIwzlGubAFoFUw3BMEpQXhSEXDWMiVDSlkJtdQt7gIJhkF87apRld9SvKyqarqy7YfhxGoGR6t33ujqusWHrQuE8LWytIFwT0fxAW0F14SUXQQe+x0HDZRy7TYuGEb8gm5Uwmhdv2w7jvQM6xmxiJ+bxoWifax7SZkcm8L6wicitXRuQYq1-C2cwXBomovrBX7jDaVonNFeW-LLf1UbKugMdq+qcYF7bHYDEXiZV56BFekTqdaPXxs5PJdGj+EyNqJ0z08Cx6b53GHd9J2RbFg7hiOghTvO2WcHt72M99oN-aesmXopzX3oQK0jyolxrkS05JuzS1bWcW1Am8Jw0SdW3Pbxn3K1was9rR13laxu209L8tx8n4ZK9Vmh1f0g1WwsXWLacEcjAPxuD1tfIB-B8FPBUcFsphuWF6gMewCrGts4l-OpcL+evafsvl5rGvQOwcqYDQsLUMwjcBQGxbmBSyjcvqXAmgxTkAQDCp1-gAeSeNPCqs8PYPywU8IB1cg61zehFUEyUDCmD8M6TQ-IErg2PEPCwB9QQmAwXjbBFB3650ltLC6P9uHEOVlXNWNcNYUJ3gyL6-1NCGAMDTPw0097RVWrvCEAQ2KwF4veQWs4oBEAAEIPjqj5XBbs573F0XxAxXFTH3nMRAEhEiyFSJDmArYUU2Y-VMCCTQugDx0OSheEEnIHKeDvhOHAtj9FI0MSYsx3kIB8LzgXGWoo4n2KMWARxzjXEb0kVvfqRF3BHmaBYVEo4TYWQaMUahJRTAGEaIUHRei-J3kfM+Wq+SUmWPwUXbA2TtpdI0r05JPlCmbx-KAspexnANHIiYTkqIBQHiWdgJZ-jszgnWO0uxoz1I9JfH0ixe0c7pK-pkmxHSjndNfGclxYj14zLCtvMBbJgQtzZA5PQ0cmgHiiVsnkrhYHFE5CEToNAWBwDkN0EpWtEAAFp2aGHRRizF6L1C+HhMi4eLkwCIvrpsowlxsxgjRAEUwqjDDOFRBoxKI1ygIXvj0Rqm0-LEtjAEnudo-DjQ8CNVRQJo43zWQKR0pguGC2lITEW3LWy8sqQKvwQqrAWU5vS1QrNEo6BvgS4uj8fbvkVQNZVvdVXXHaBqqo5E5oAl8GDXkvyZWL39AAvaZqiIWv5SCNVNrT4MWcHQ8iQ5mluqgDwrA3qci+q2P661wq6lh30E68pmh3CbGxGy4Zdz8aJLyZMiAsbEDxqteqg8OhajGE5OYWEA8jDQxiXmw5UAxknKeaW3InI+UJsFYGlNgRkqJVtOUJRE1WUhCAA */
id: "auth",
@@ -78,10 +85,17 @@ export const authMachine = setup({
errorMessage: null,
}),
},
AuthLogoutSubmitted: "loggingOut",
AuthLogoutSubmitted: {
guard: "canLogoutToGuest",
target: "loggingOut",
},
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 <AuthStatusChecker /> 派发
AuthInit: "initializing",
AuthGuestLoginSubmitted: {
guard: "canSubmitGuestLogin",
target: "loadingGuestLogin",
},
AuthEmailLoginSubmitted: "loadingEmailLogin",
AuthEmailRegisterSubmitted: "loadingEmailRegister",
AuthGoogleLoginSubmitted: "loadingOAuth",
@@ -102,26 +116,14 @@ export const authMachine = setup({
entry: assign({ errorMessage: null }),
invoke: {
src: "checkAuthStatus",
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,
}),
},
],
onDone: {
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
actions: assign({
loginStatus: ({ event }) => event.output,
hasInitialized: true,
errorMessage: null,
}),
},
onError: {
target: "idle",
actions: assign({