fix(auth): defer guest login until chat entry
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
||||||
import { useUserState } from "@/stores/user/user-context";
|
import { useUserState } from "@/stores/user/user-context";
|
||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
@@ -42,6 +42,7 @@ export function ChatScreen() {
|
|||||||
const state = useChatState();
|
const state = useChatState();
|
||||||
const chatDispatch = useChatDispatch();
|
const chatDispatch = useChatDispatch();
|
||||||
const authState = useAuthState();
|
const authState = useAuthState();
|
||||||
|
const authDispatch = useAuthDispatch();
|
||||||
const userState = useUserState();
|
const userState = useUserState();
|
||||||
const navigator = useAppNavigator();
|
const navigator = useAppNavigator();
|
||||||
const [showExternalBrowserDialog, setShowExternalBrowserDialog] =
|
const [showExternalBrowserDialog, setShowExternalBrowserDialog] =
|
||||||
@@ -70,6 +71,25 @@ export function ChatScreen() {
|
|||||||
const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10;
|
const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10;
|
||||||
|
|
||||||
const externalBrowserPromptShownRef = useRef(false);
|
const externalBrowserPromptShownRef = useRef(false);
|
||||||
|
const guestLoginRequestedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authState.hasInitialized || authState.isLoading) return;
|
||||||
|
|
||||||
|
if (authState.loginStatus !== "notLoggedIn") {
|
||||||
|
guestLoginRequestedRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guestLoginRequestedRef.current) return;
|
||||||
|
guestLoginRequestedRef.current = true;
|
||||||
|
authDispatch({ type: "AuthGuestLoginSubmitted" });
|
||||||
|
}, [
|
||||||
|
authDispatch,
|
||||||
|
authState.hasInitialized,
|
||||||
|
authState.isLoading,
|
||||||
|
authState.loginStatus,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface SplashButtonProps {
|
|||||||
|
|
||||||
export function SplashButton({ onStartChat }: SplashButtonProps) {
|
export function SplashButton({ onStartChat }: SplashButtonProps) {
|
||||||
const state = useAuthState();
|
const state = useAuthState();
|
||||||
const isLoading = state.isLoading;
|
const isLoading = !state.hasInitialized || state.isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
|
|||||||
@@ -33,12 +33,21 @@ describe("navigation resolver", () => {
|
|||||||
).toBe("/subscription?type=topup");
|
).toBe("/subscription?type=topup");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("redirects not logged in session routes to splash", () => {
|
it("allows not logged in users to enter chat for guest bootstrap", () => {
|
||||||
expect(
|
expect(
|
||||||
resolveRouteGuardRedirect({
|
resolveRouteGuardRedirect({
|
||||||
loginStatus: "notLoggedIn",
|
loginStatus: "notLoggedIn",
|
||||||
pathname: ROUTES.chat,
|
pathname: ROUTES.chat,
|
||||||
}),
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("redirects not logged in session routes except chat to splash", () => {
|
||||||
|
expect(
|
||||||
|
resolveRouteGuardRedirect({
|
||||||
|
loginStatus: "notLoggedIn",
|
||||||
|
pathname: ROUTES.sidebar,
|
||||||
|
}),
|
||||||
).toBe(ROUTES.splash);
|
).toBe(ROUTES.splash);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { ROUTES } from "./routes";
|
import { ROUTES } from "./routes";
|
||||||
|
|
||||||
export type RouteAccess = "public" | "session" | "realUser" | "authOnly";
|
export type RouteAccess =
|
||||||
|
| "public"
|
||||||
|
| "guestEntry"
|
||||||
|
| "session"
|
||||||
|
| "realUser"
|
||||||
|
| "authOnly";
|
||||||
|
|
||||||
const STATIC_ROUTE_ACCESS: Partial<Record<string, RouteAccess>> = {
|
const STATIC_ROUTE_ACCESS: Partial<Record<string, RouteAccess>> = {
|
||||||
[ROUTES.root]: "public",
|
[ROUTES.root]: "public",
|
||||||
[ROUTES.splash]: "authOnly",
|
[ROUTES.splash]: "authOnly",
|
||||||
[ROUTES.auth]: "authOnly",
|
[ROUTES.auth]: "authOnly",
|
||||||
[ROUTES.chat]: "session",
|
[ROUTES.chat]: "guestEntry",
|
||||||
[ROUTES.sidebar]: "session",
|
[ROUTES.sidebar]: "session",
|
||||||
[ROUTES.subscription]: "realUser",
|
[ROUTES.subscription]: "realUser",
|
||||||
[ROUTES.coinsRules]: "public",
|
[ROUTES.coinsRules]: "public",
|
||||||
|
|||||||
@@ -95,13 +95,31 @@ describe("authMachine", () => {
|
|||||||
actor.stop();
|
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 guestLoginSpy = vi.fn<() => void>();
|
||||||
const actor = createActor(createTestAuthMachine({ guestLoginSpy })).start();
|
const actor = createActor(createTestAuthMachine({ guestLoginSpy })).start();
|
||||||
|
|
||||||
actor.send({ type: "AuthInit" });
|
actor.send({ type: "AuthInit" });
|
||||||
await waitFor(actor, (snapshot) => snapshot.matches("idle"));
|
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;
|
const context = actor.getSnapshot().context;
|
||||||
expect(context.loginStatus).toBe("guest");
|
expect(context.loginStatus).toBe("guest");
|
||||||
expect(context.hasInitialized).toBe(true);
|
expect(context.hasInitialized).toBe(true);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
username: state.context.username,
|
username: state.context.username,
|
||||||
confirmPassword: state.context.confirmPassword,
|
confirmPassword: state.context.confirmPassword,
|
||||||
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)/
|
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)/
|
||||||
// OAuth 回调后端 sync / 自动游客登录
|
// OAuth 回调后端 sync / 显式游客登录
|
||||||
isLoading:
|
isLoading:
|
||||||
state.matches("loadingEmailLogin") ||
|
state.matches("loadingEmailLogin") ||
|
||||||
state.matches("loadingEmailRegister") ||
|
state.matches("loadingEmailRegister") ||
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type AuthEvent =
|
|||||||
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
|
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
|
||||||
| { type: "AuthInit" }
|
| { type: "AuthInit" }
|
||||||
// 业务事件(提交)
|
// 业务事件(提交)
|
||||||
|
| { type: "AuthGuestLoginSubmitted" }
|
||||||
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
|
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
|
||||||
| {
|
| {
|
||||||
type: "AuthEmailRegisterSubmitted";
|
type: "AuthEmailRegisterSubmitted";
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const toAuthErrorMessage = (error: unknown): string =>
|
|||||||
//
|
//
|
||||||
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
// 设计:所有登录/同步流跑完都回 `idle`(带正确 `loginStatus`)。
|
||||||
//
|
//
|
||||||
// 未找到任何登录态时,初始化流程会自动进入 guest login,补齐游客会话。
|
// 初始化只恢复已有登录态;游客登录必须由用户明确操作触发。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const authMachine = setup({
|
export const authMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
@@ -49,6 +49,13 @@ export const authMachine = setup({
|
|||||||
checkAuthStatus: checkAuthStatusActor,
|
checkAuthStatus: checkAuthStatusActor,
|
||||||
logout: logoutActor,
|
logout: logoutActor,
|
||||||
},
|
},
|
||||||
|
guards: {
|
||||||
|
canSubmitGuestLogin: ({ context }) =>
|
||||||
|
context.loginStatus === "notLoggedIn",
|
||||||
|
canLogoutToGuest: ({ context }) =>
|
||||||
|
context.loginStatus !== "notLoggedIn" &&
|
||||||
|
context.loginStatus !== "guest",
|
||||||
|
},
|
||||||
}).createMachine({
|
}).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 */
|
/** @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",
|
id: "auth",
|
||||||
@@ -78,10 +85,17 @@ export const authMachine = setup({
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
AuthLogoutSubmitted: "loggingOut",
|
AuthLogoutSubmitted: {
|
||||||
|
guard: "canLogoutToGuest",
|
||||||
|
target: "loggingOut",
|
||||||
|
},
|
||||||
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 <AuthStatusChecker /> 派发
|
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 <AuthStatusChecker /> 派发
|
||||||
AuthInit: "initializing",
|
AuthInit: "initializing",
|
||||||
|
|
||||||
|
AuthGuestLoginSubmitted: {
|
||||||
|
guard: "canSubmitGuestLogin",
|
||||||
|
target: "loadingGuestLogin",
|
||||||
|
},
|
||||||
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
||||||
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
||||||
AuthGoogleLoginSubmitted: "loadingOAuth",
|
AuthGoogleLoginSubmitted: "loadingOAuth",
|
||||||
@@ -102,26 +116,14 @@ export const authMachine = setup({
|
|||||||
entry: assign({ errorMessage: null }),
|
entry: assign({ errorMessage: null }),
|
||||||
invoke: {
|
invoke: {
|
||||||
src: "checkAuthStatus",
|
src: "checkAuthStatus",
|
||||||
onDone: [
|
onDone: {
|
||||||
{
|
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
|
||||||
guard: ({ event }) => event.output === "notLoggedIn",
|
actions: assign({
|
||||||
target: "loadingGuestLogin",
|
loginStatus: ({ event }) => event.output,
|
||||||
actions: assign({
|
hasInitialized: true,
|
||||||
loginStatus: "notLoggedIn",
|
errorMessage: null,
|
||||||
// 自动游客登录仍属于初始化流程,完成前不标记 initialized。
|
}),
|
||||||
hasInitialized: false,
|
},
|
||||||
errorMessage: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
|
|
||||||
actions: assign({
|
|
||||||
loginStatus: ({ event }) => event.output,
|
|
||||||
hasInitialized: true,
|
|
||||||
errorMessage: null,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onError: {
|
onError: {
|
||||||
target: "idle",
|
target: "idle",
|
||||||
actions: assign({
|
actions: assign({
|
||||||
|
|||||||
Reference in New Issue
Block a user