Merge branch 'dev' into test
This commit is contained in:
@@ -51,12 +51,10 @@ export function SidebarScreen() {
|
|||||||
const isNowLoggedOut = user.currentUser == null;
|
const isNowLoggedOut = user.currentUser == null;
|
||||||
prevUserRef.current = user.currentUser;
|
prevUserRef.current = user.currentUser;
|
||||||
if (wasLoggedIn && isNowLoggedOut) {
|
if (wasLoggedIn && isNowLoggedOut) {
|
||||||
// 1) AuthReset:清场(清 email/password/username 等敏感字段)
|
// AuthReset:清场(context 回到 initialState,loginStatus="notLoggedIn")
|
||||||
|
// 不需要再 dispatch AuthInit 校验 storage —— userLogoutActor 已清空 storage,
|
||||||
|
// AuthReset 直接写回 initialState 已与 storage 对齐。
|
||||||
authDispatch({ type: "AuthReset" });
|
authDispatch({ type: "AuthReset" });
|
||||||
// 2) AuthStatusCheckSubmitted:通过 storage 重新验证实际 auth 状态
|
|
||||||
// (userLogoutActor 已清空 userStorage,checkAuthStatusActor 会返 "notLoggedIn")
|
|
||||||
// 这一步确保 redirect 到 /chat 后,chat-screen 看到的是真实 storage 状态而非 AuthReset 直接置的 initialState
|
|
||||||
authDispatch({ type: "AuthStatusCheckSubmitted" });
|
|
||||||
router.replace(ROUTES.chat);
|
router.replace(ROUTES.chat);
|
||||||
}
|
}
|
||||||
}, [user.currentUser, authDispatch, router]);
|
}, [user.currentUser, authDispatch, router]);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*
|
*
|
||||||
* 把所有功能 Provider 串起来:
|
* 把所有功能 Provider 串起来:
|
||||||
* AuthProvider → UserProvider → SidebarProvider → ChatProvider
|
* AuthProvider → UserProvider → SidebarProvider → ChatProvider
|
||||||
* + AuthStatusChecker(启动时一次:派发 AuthStatusCheckSubmitted)
|
* + AuthStatusChecker(启动时一次:派发 AuthInit)
|
||||||
* + OAuthSessionSync (持续监听 NextAuth session → auth machine)
|
* + OAuthSessionSync (持续监听 NextAuth session → auth machine)
|
||||||
*
|
*
|
||||||
* 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可,
|
* 它们始终挂载,保证各页面直接 `use*State()` / `use*Dispatch()` 即可,
|
||||||
|
|||||||
@@ -61,11 +61,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||||||
password: state.context.password,
|
password: state.context.password,
|
||||||
username: state.context.username,
|
username: state.context.username,
|
||||||
confirmPassword: state.context.confirmPassword,
|
confirmPassword: state.context.confirmPassword,
|
||||||
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)/ 显式游客登录
|
// isLoading 覆盖邮箱登录 / 邮箱注册 / OAuth 跳转(NextAuth 重定向期间)/
|
||||||
|
// OAuth 回调后端 sync / 显式游客登录
|
||||||
isLoading:
|
isLoading:
|
||||||
state.matches("loadingEmailLogin") ||
|
state.matches("loadingEmailLogin") ||
|
||||||
state.matches("loadingEmailRegister") ||
|
state.matches("loadingEmailRegister") ||
|
||||||
state.matches("loadingOAuth") ||
|
state.matches("loadingOAuth") ||
|
||||||
|
state.matches("syncingGoogleBackend") ||
|
||||||
|
state.matches("syncingFacebookBackend") ||
|
||||||
state.matches("loadingGuestLogin"),
|
state.matches("loadingGuestLogin"),
|
||||||
errorMessage: state.context.errorMessage,
|
errorMessage: state.context.errorMessage,
|
||||||
loginStatus: state.context.loginStatus,
|
loginStatus: state.context.loginStatus,
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ export type AuthEvent =
|
|||||||
| { type: "AuthModeChanged"; mode: AuthMode }
|
| { type: "AuthModeChanged"; mode: AuthMode }
|
||||||
| { type: "AuthFormCleared" }
|
| { type: "AuthFormCleared" }
|
||||||
| { type: "AuthReset" }
|
| { type: "AuthReset" }
|
||||||
/** 启动 / 恢复时检查登录态 —— 拿 deviceId → 查 token(不自动创建游客账号) */
|
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
|
||||||
| { type: "AuthStatusCheckSubmitted" }
|
| { type: "AuthInit" }
|
||||||
/** 显式游客登录 —— splash 上点"游客模式"按钮触发(不自动) */
|
/** 显式游客登录 —— splash 上点"游客模式"按钮触发(不自动) */
|
||||||
| { type: "AuthGuestLoginSubmitted" }
|
| { type: "AuthGuestLoginSubmitted" }
|
||||||
// 业务事件(提交)
|
// 业务事件(提交)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export type { AuthEvent } from "./auth-events";
|
|||||||
// 在登录成功的 onDone 里自动置 true:
|
// 在登录成功的 onDone 里自动置 true:
|
||||||
// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
|
// splash / auth screen 看到 `pendingRedirect && loginStatus !== "notLoggedIn"` 跳 /chat
|
||||||
// 跳完派 `AuthClearPendingRedirect` 置 false
|
// 跳完派 `AuthClearPendingRedirect` 置 false
|
||||||
// checkingAuthStatus 不设 flag —— 被动查 token 不算"用户意图"。
|
// `initializing` 不设 flag —— 启动 init 是被动读 token,不算"用户意图"。
|
||||||
// ============================================================
|
// ============================================================
|
||||||
export const authMachine = setup({
|
export const authMachine = setup({
|
||||||
types: {
|
types: {
|
||||||
@@ -48,6 +48,7 @@ export const authMachine = setup({
|
|||||||
checkAuthStatus: checkAuthStatusActor,
|
checkAuthStatus: checkAuthStatusActor,
|
||||||
},
|
},
|
||||||
}).createMachine({
|
}).createMachine({
|
||||||
|
/** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgOgJYQBswBiAQQ0wAVkA7MAgWQHsIwBhTWmCAbQAYAuolAAHJrFzpcTGsJAAPRABYAHNgDMAVgCMmzepUB2AGwAmLUu1KANCACeiXYe0bthlQE4PxlUp2GlAF9A2zQsPEIScixmVg4uSH4hJBAxCSkZOUUEH2w+dQ8+E2Ntb3VjTVNbBwQrQ01scw9DQs0ldXVdYNCKCKIyCgAxJgAnAFs2ImQRxME5NMlpWRTsw3Vsbz4PUxN6v0rDasd1FuxtEp8rPlNtbVNTbpAwnHx+6MwAJTgwdCT58UWmRWiC8GxuRm82i27iKR1qphU6z4iM0hnuxi20Mezz6UQoAGV0Mh0KhYBwwABjADW+NQACMxpJ0LNkqIARllqBskpChoEXdTB5bkolHwlMY4R4VKZsG0fFpNMY-IY0djeq88VgAOKoODoAAyTCguBotIZTJZ-3SSyyyl56n59yFVlF4rhmi2jXU7TaKuM2xumjV4Q1AywAFExshcARDcbTfTGehmbw5ikFhzbQgeXw+SoBc6RWKJfZEAEGp4+JoPP4ijzDMGXpEw5hI9GCF9jbBmSMzUmU390+ybcDs-bHYLha6SzVzttZbo9MYAhjqw2Qk91c33lqmEaiHGTX2LanWalh0CuXbcw7806p8W4W4lLL3HdysirPcVI3cS3BsgFJgHSe5UoeCbmsmlpDtal4KNeeYFg+bqlgg9wNEUd5WNW9YPBuOKhu8pAiCIB5GkeiYnoObKwZy8HZjKlTepUIraFonRPtoRh5FW4qdKUhhFHhPQhtuFC7vuYD4nYNAUseUGnlagJ0dy453pOLqPqh2EVtWnTGFo9wOuov6EUMgHAaB0myfJA5pjRylZjmiH3ppKGzjcHiNPkRTVpovhiuuIlNm8FCTGA0yUGANAQCaUBfLFMwUr89nnrRTlVNpmJgms+jNCoHpKEFm7hBSmCUlScXvISxKksQEAyGAeA0AAbkwVJNTiZUVVVBJEiSsAICabUUsSSxJNRaWOaOeiGNgLQ+Gs+TGP6qJwionjYOKRhfh4HSih4v7ddSvVYDVA3EGAIwjKM2CkcSABmoxjNgXXlSdNBQNV-WkkNrVMKNHITalGYjleCB6GoSp8CU3imDo5iHKhC0aK0e3XDcxa-gQTDILFn06nq4H1Y1zVtR1r29DjeNxYT3bgX9I1jTIwNnqDcHZHpYJcQZ7QOtcSM1JYxhnIq5haFWPj+djuP41AdMGuRNCXddt33egT3jJT4TU3LCsM8NAPMzQrNKZmM0eC+JiKlC1znOosKocZeRvgK3oipbxgyzTn1tjGxMNXQZPtZ1VOy3FfuxkrjNG0DgiTezKmIH5Gww+YYpbP6yJwhUMp3kVBheDo3re3LkfE1dN0jHdBCPc92s4LrEdRv70eG4D43xyDF5JxD3jzTyzT+lKtwbe61bzd6hlmNKKraKXzftp2uDdldJNB4bFM4k3vstx2YBdj2McdyzXdsz3WY6CLfC6GYbGWyqNjadWXntAEFgBNKJf4WHPtQJHy9V4jBVlXGudctbb3DrvJeB8V5H3bsbU2MFprgyvtgAqe1lxSn5plWcHoRZaD2l+fI-liqQL-gAeXeOvJqm9Q46ygVAKhFBj6ILPmbMG9F-JqClO0REBQ0TnBnIgHYDRUT5lKJ0ZElR54-wYZQ6hlc1a1w1vXchctmFYFYXHAQCcL4zU2tKO8XgCpKncogfMso+DWOuB0LOOFfywBkhSWme4oBEAAEKAQ6jFGhwct69CcbJVxkkvHUmihAbRnddHd3SjNE4Gg9JGFYgYRUT5ygyldjWZEWxvCOOcSE9xYAwk+IgCA5R4CXo4iCS4gmbjPHeIiVE0+MTz5xNQQYbynRvTXBHicJ8ph-TzR8msFQ0JqxezkTgGpcUAJARAu1EpES-F0IbtgGZn05mWUWY0mKzSTbsOQebVBNwNgnHwYqEo9R1CcUGWcUwPI5Q7R8EEKZ6yCmbIsgsqkSzfFKOrurTWVTAkfKgFs75vzIkIJ0Xo9pXD7jzTvPoXQ4p8g3Kyh0PI219q6D2nhDcNAWBwDkM8DhHNEAAFphEIApQ0Lw9KGUMqVEGN5GoyW9zuC+c4oo2gGDFOcfMT5lwygKBiZE0pUTCRKjgY6lVPrfVqvAI5nDVhjNcA8wohd7ginWiqRoqITC6HKN6KV6jaa6npkrdlWYYTqpzFqh5T8hb5A2H4BEdYVA+C9QvaBrd4zWtHLazoGr8ieG1U6xA0NsAYkXJUfyWI3k73-nvQBPYA3gyDZyzVYbHVPlIY0QZV9+GijIb-DR7x030UzSGh1OrtImC2vyN1BwOimpBcEupoTdkQEraqm8WbQ0BjrbOCWr4BZKgKKiPJbyNlgq+aBSFvayxquDfanNw7HC3HWEKUUH5lSVGCMEIAA */
|
||||||
id: "auth",
|
id: "auth",
|
||||||
initial: "idle",
|
initial: "idle",
|
||||||
context: initialState,
|
context: initialState,
|
||||||
@@ -78,8 +79,8 @@ export const authMachine = setup({
|
|||||||
AuthReset: {
|
AuthReset: {
|
||||||
actions: assign(() => initialState),
|
actions: assign(() => initialState),
|
||||||
},
|
},
|
||||||
// 启动 / 恢复时检查登录态 —— 由 <AuthStatusChecker /> 派发
|
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 <AuthStatusChecker /> 派发
|
||||||
AuthStatusCheckSubmitted: "checkingAuthStatus",
|
AuthInit: "initializing",
|
||||||
|
|
||||||
AuthGuestLoginSubmitted: "loadingGuestLogin",
|
AuthGuestLoginSubmitted: "loadingGuestLogin",
|
||||||
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
||||||
@@ -102,12 +103,12 @@ export const authMachine = setup({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
checkingAuthStatus: {
|
initializing: {
|
||||||
entry: assign({ errorMessage: null }),
|
entry: assign({ errorMessage: null }),
|
||||||
invoke: {
|
invoke: {
|
||||||
src: "checkAuthStatus",
|
src: "checkAuthStatus",
|
||||||
onDone: {
|
onDone: {
|
||||||
target: "idle", // ← status check 完回 idle(不再假冒 success)
|
target: "idle", // ← init 完回 idle(从 storage 拿到 loginStatus 后落地)
|
||||||
actions: assign({
|
actions: assign({
|
||||||
loginStatus: ({ event }) => event.output,
|
loginStatus: ({ event }) => event.output,
|
||||||
// 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
|
// 不置 pendingRedirect —— 状态查询是被动读 token,不算"用户意图"
|
||||||
|
|||||||
@@ -1,74 +1,31 @@
|
|||||||
"use client";
|
"use client";
|
||||||
/**
|
/**
|
||||||
* AuthStatusChecker 启动 + 状态变化检查器
|
* AuthInit 派发器 —— App 启动时读 storage 同步 loginStatus 到状态机
|
||||||
*
|
*
|
||||||
* 触发时机:
|
* 派发时机:仅 mount 时一次(`useEffect` deps = `[dispatch]`,永远不重跑)。
|
||||||
* 1. App 启动时(mount)派发一次 `AuthStatusCheckSubmitted`
|
|
||||||
* 2. loginStatus 每次变化时(外部触发)也派发一次 —— 重新读 storage 校准
|
|
||||||
* 例:OAuth sync 完成 → loginStatus 变 "facebook" → re-check 一次确认 storage 一致
|
|
||||||
* 邮箱登录成功 → loginStatus 变 "email" → re-check 确认 token 已落盘
|
|
||||||
*
|
*
|
||||||
* 死循环防护(关键 —— 不加会无限递归):
|
* 与原 `AuthStatusCheckSubmitted` 的区别:
|
||||||
* - 状态机收 `AuthStatusCheckSubmitted` → 进 `checkingAuthStatus` → 读 storage → onDone 写回 loginStatus
|
* - 原版本有两个 useEffect:① mount + ② loginStatus 变化 re-check
|
||||||
* - 如果 useEffect 直接依赖 `loginStatus`,写回本身触发 effect → 再 dispatch → 死循环
|
* - re-check 是 dead code —— 状态机 onDone 已经把 loginStatus 写对,
|
||||||
* - 用 `skipNextChangeRef` 抑制一次:
|
* 再读 storage 只会拿到相同值(no-op),徒增死循环防护复杂度
|
||||||
* 每次我们自己 dispatch 前把 flag 置 true;
|
* - 新版本只保留 ①,命名 `AuthInit` 与 `UserInit` 对齐(user-machine 的同模式)
|
||||||
* effect 见到 loginStatus 变化且 flag 为 true 时跳过并清 flag。
|
|
||||||
*
|
*
|
||||||
* 与 <OAuthSessionSync /> 平级:
|
* 与 <OAuthSessionSync /> 平级:
|
||||||
* - AuthStatusChecker: 启动 + 状态变化时 re-check("自己也能感知")
|
* - AuthStatusChecker: 启动时一次性 init("自己启动时同步")
|
||||||
* - OAuthSessionSync: 持续监听 NextAuth session("别人也在写")
|
* - OAuthSessionSync: 持续监听 NextAuth session("别人在写")
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import type { LoginStatus } from "@/models/auth/login-status";
|
import { useAuthDispatch } from "./auth-context";
|
||||||
|
|
||||||
import { useAuthDispatch, useAuthState } from "./auth-context";
|
|
||||||
|
|
||||||
export function AuthStatusChecker() {
|
export function AuthStatusChecker() {
|
||||||
const dispatch = useAuthDispatch();
|
const dispatch = useAuthDispatch();
|
||||||
const loginStatus = useAuthState().loginStatus;
|
|
||||||
|
|
||||||
// 上一次的 loginStatus(首次 mount 留 null —— 用来跳过首次 effect 跑)
|
|
||||||
const prevLoginStatusRef = useRef<LoginStatus | null>(null);
|
|
||||||
// 抑制下一次 loginStatus 变化触发的 effect(自己 dispatch 引起的写回要跳过)
|
|
||||||
const skipNextChangeRef = useRef(false);
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// ① App 启动时:派发一次
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[auth-status-checker] mount → dispatch AuthStatusCheckSubmitted");
|
console.log("[auth-init] mount → dispatch AuthInit");
|
||||||
skipNextChangeRef.current = true; // 抑制启动 check 写回 loginStatus 引起的 effect
|
dispatch({ type: "AuthInit" });
|
||||||
dispatch({ type: "AuthStatusCheckSubmitted" });
|
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// ② loginStatus 变化时:派发一次(但跳过我们自己 check 写回的那次)
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
useEffect(() => {
|
|
||||||
const prev = prevLoginStatusRef.current;
|
|
||||||
prevLoginStatusRef.current = loginStatus;
|
|
||||||
if (prev === null) return; // 首次 mount —— 启动那次由 ① 处理
|
|
||||||
if (prev === loginStatus) return; // loginStatus 没变 —— 不触发
|
|
||||||
if (skipNextChangeRef.current) {
|
|
||||||
// 这是我们自己 check 写回的 —— 抑制本次
|
|
||||||
skipNextChangeRef.current = false;
|
|
||||||
console.log(
|
|
||||||
"[auth-status-checker] loginStatus change suppressed (self-triggered)",
|
|
||||||
{ from: prev, to: loginStatus },
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// 外部触发(OAuth sync / 邮箱登录 / 登出 等)的 loginStatus 变化 → 再 check 一次校准
|
|
||||||
skipNextChangeRef.current = true;
|
|
||||||
console.log(
|
|
||||||
"[auth-status-checker] loginStatus changed externally → re-check",
|
|
||||||
{ from: prev, to: loginStatus },
|
|
||||||
);
|
|
||||||
dispatch({ type: "AuthStatusCheckSubmitted" });
|
|
||||||
}, [loginStatus, dispatch]);
|
|
||||||
|
|
||||||
// 无 UI
|
// 无 UI
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user