refactor: migrate state imports from contexts to stores directory
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
/**
|
||||
* AuthContext:基于 XState v5 的 React Context Provider
|
||||
*
|
||||
* 业务事件(`Auth*Submitted`)通过 `useMachine(authMachine).send()` 派发。
|
||||
* XState 自动处理异步副作用(invoke + fromPromise actors)。
|
||||
*
|
||||
* 兼容性:保持原 `useAuthState()` / `useAuthDispatch()` API,
|
||||
* 内部将 XState 状态机快照映射回原 `AuthState` 形状。
|
||||
*/
|
||||
import {
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useMachine } from "@xstate/react";
|
||||
|
||||
import { authMachine } from "./auth-machine";
|
||||
import type { AuthEvent, AuthContext as MachineContext } from "./auth-machine";
|
||||
|
||||
/**
|
||||
* 对外暴露的 State 形状(保持与原 AuthState 兼容)
|
||||
*/
|
||||
export interface AuthState {
|
||||
authPanelMode: MachineContext["authPanelMode"];
|
||||
authMode: MachineContext["authMode"];
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
confirmPassword: string;
|
||||
isLoading: boolean;
|
||||
errorMessage: string | null;
|
||||
isSuccess: boolean;
|
||||
loginType: MachineContext["loginType"];
|
||||
}
|
||||
|
||||
const AuthStateCtx = createContext<AuthState | null>(null);
|
||||
const AuthDispatchCtx = createContext<Dispatch<AuthEvent> | null>(null);
|
||||
|
||||
export interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: AuthProviderProps) {
|
||||
const [state, send] = useMachine(authMachine);
|
||||
|
||||
// 映射 XState 状态机快照 → 原 AuthState 形状
|
||||
const authState = useMemo<AuthState>(
|
||||
() => ({
|
||||
authPanelMode: state.context.authPanelMode,
|
||||
authMode: state.context.authMode,
|
||||
email: state.context.email,
|
||||
password: state.context.password,
|
||||
username: state.context.username,
|
||||
confirmPassword: state.context.confirmPassword,
|
||||
isLoading: state.matches("loadingEmailLogin") ||
|
||||
state.matches("loadingEmailRegister") ||
|
||||
state.matches("loadingFacebookLogin") ||
|
||||
state.matches("loadingWebGoogleLogin"),
|
||||
errorMessage: state.context.errorMessage,
|
||||
isSuccess: state.matches("success"),
|
||||
loginType: state.context.loginType,
|
||||
}),
|
||||
[state],
|
||||
);
|
||||
|
||||
return (
|
||||
<AuthStateCtx.Provider value={authState}>
|
||||
<AuthDispatchCtx.Provider value={send}>{children}</AuthDispatchCtx.Provider>
|
||||
</AuthStateCtx.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthState(): AuthState {
|
||||
const ctx = useContext(AuthStateCtx);
|
||||
if (!ctx) throw new Error("useAuthState must be used inside <AuthProvider>");
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export function useAuthDispatch(): Dispatch<AuthEvent> {
|
||||
const ctx = useContext(AuthDispatchCtx);
|
||||
if (!ctx)
|
||||
throw new Error("useAuthDispatch must be used inside <AuthProvider>");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
/**
|
||||
* Auth 状态机(XState v5)
|
||||
*
|
||||
* 原始 Dart: lib/ui/auth/bloc/auth_bloc.dart + auth_state.dart + auth_event.dart
|
||||
*
|
||||
* 设计要点:
|
||||
* - 使用 XState v5 `setup({...}).createMachine({...})` 声明式 API
|
||||
* - 异步副作用用 `fromPromise` actor 包装(自动取消、错误捕获)
|
||||
* - 表单字段值放在 `context`,UI 通过事件更新
|
||||
* - 保持事件类型名与原 Dart 一致,便于业务层迁移对照
|
||||
*/
|
||||
import { setup, fromPromise, assign } from "xstate";
|
||||
|
||||
import type { AuthMode } from "@/models/auth/auth-mode";
|
||||
import type { AuthPanelMode } from "@/models/auth/auth-panel-mode";
|
||||
import type { LoginType } from "@/models/auth/login-type";
|
||||
import { authRepository } from "@/data/repositories/auth_repository";
|
||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||
import {
|
||||
loginWithFacebook,
|
||||
getFacebookUserData,
|
||||
} from "@/integrations/facebook-sdk";
|
||||
import { Result } from "@/utils/result";
|
||||
|
||||
// ============================================================
|
||||
// Context
|
||||
// ============================================================
|
||||
export interface AuthContext {
|
||||
/** 当前面板模式(Facebook / Email) */
|
||||
authPanelMode: AuthPanelMode;
|
||||
/** 内部登录模式(login / register) */
|
||||
authMode: AuthMode;
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
confirmPassword: string;
|
||||
errorMessage: string | null;
|
||||
loginType: LoginType;
|
||||
}
|
||||
|
||||
const initialContext: AuthContext = {
|
||||
authPanelMode: "facebook",
|
||||
authMode: "login",
|
||||
email: "",
|
||||
password: "",
|
||||
username: "",
|
||||
confirmPassword: "",
|
||||
errorMessage: null,
|
||||
loginType: "none",
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Events(事件类型名与原 Dart AuthEvent 对齐)
|
||||
// ============================================================
|
||||
export type AuthEvent =
|
||||
// 内部 UI 事件
|
||||
| { type: "AuthPanelModeChanged"; mode: AuthPanelMode }
|
||||
| { type: "AuthModeChanged"; mode: AuthMode }
|
||||
| { type: "AuthFormCleared" }
|
||||
| { type: "AuthReset" }
|
||||
// 业务事件(提交)
|
||||
| { type: "AuthEmailLoginSubmitted"; email: string; password: string }
|
||||
| {
|
||||
type: "AuthEmailRegisterSubmitted";
|
||||
email: string;
|
||||
password: string;
|
||||
username: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
| { type: "AuthGoogleLoginSubmitted" }
|
||||
| { type: "AuthFacebookLoginSubmitted" }
|
||||
| { type: "AuthAppleLoginSubmitted" }
|
||||
| { type: "AuthWebGoogleLoginSuccess"; idToken: string; email: string };
|
||||
|
||||
// ============================================================
|
||||
// Helpers
|
||||
// ============================================================
|
||||
/**
|
||||
* 从存储读取 guestId,失败或缺失返回 undefined
|
||||
* 用 if 语句避免 TS 联合类型 narrowing 问题
|
||||
*/
|
||||
async function readGuestId(): Promise<string | undefined> {
|
||||
const r = await AuthStorage.getInstance().getDeviceId();
|
||||
if (r.success && r.data != null) {
|
||||
return r.data;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Actors(异步服务)
|
||||
// ============================================================
|
||||
const emailLoginActor = fromPromise<LoginType, { email: string; password: string }>(
|
||||
async ({ input }) => {
|
||||
const guestId = await readGuestId();
|
||||
const result = await authRepository.emailLogin({ ...input, guestId });
|
||||
if (Result.isErr(result)) throw result.error;
|
||||
return "email" as LoginType;
|
||||
},
|
||||
);
|
||||
|
||||
const emailRegisterThenLoginActor = fromPromise<
|
||||
LoginType,
|
||||
{ email: string; password: string; username: string; confirmPassword: string }
|
||||
>(async ({ input }) => {
|
||||
const guestId = await readGuestId();
|
||||
|
||||
const registerResult = await authRepository.register({ ...input, guestId });
|
||||
if (Result.isErr(registerResult)) throw registerResult.error;
|
||||
|
||||
// 注册后自动登录(对齐 Dart 行为)
|
||||
const loginResult = await authRepository.emailLogin({
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
guestId,
|
||||
});
|
||||
if (Result.isErr(loginResult)) throw loginResult.error;
|
||||
return "email" as LoginType;
|
||||
});
|
||||
|
||||
const googleLoginActor = fromPromise<LoginType, { idToken: string }>(async ({ input }) => {
|
||||
const guestId = await readGuestId();
|
||||
const result = await authRepository.googleLogin({ idToken: input.idToken, guestId });
|
||||
if (Result.isErr(result)) throw result.error;
|
||||
return "google" as LoginType;
|
||||
});
|
||||
|
||||
const facebookLoginActor = fromPromise<LoginType, Record<string, never>>(
|
||||
async () => {
|
||||
const guestId = await readGuestId();
|
||||
|
||||
const fbResult = await loginWithFacebook();
|
||||
if (Result.isErr(fbResult)) throw fbResult.error;
|
||||
|
||||
const authResult = await authRepository.facebookLogin({
|
||||
accessToken: fbResult.data.accessToken,
|
||||
guestId,
|
||||
});
|
||||
if (Result.isErr(authResult)) throw authResult.error;
|
||||
|
||||
// best-effort:异步上传 Facebook 头像
|
||||
void (async () => {
|
||||
const userData = await getFacebookUserData();
|
||||
if (Result.isOk(userData) && userData.data.pictureUrl) {
|
||||
// TODO: 调用 userStorage.setAvatarUrl
|
||||
}
|
||||
})();
|
||||
return "facebook" as LoginType;
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// Machine
|
||||
// ============================================================
|
||||
export const authMachine = setup({
|
||||
types: {
|
||||
context: {} as AuthContext,
|
||||
events: {} as AuthEvent,
|
||||
},
|
||||
actors: {
|
||||
emailLogin: emailLoginActor,
|
||||
emailRegisterThenLogin: emailRegisterThenLoginActor,
|
||||
googleLogin: googleLoginActor,
|
||||
facebookLogin: facebookLoginActor,
|
||||
},
|
||||
}).createMachine({
|
||||
id: "auth",
|
||||
initial: "idle",
|
||||
context: initialContext,
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
AuthPanelModeChanged: {
|
||||
actions: assign({
|
||||
authPanelMode: ({ event }) => event.mode,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
AuthModeChanged: {
|
||||
actions: assign({
|
||||
authMode: ({ event }) => event.mode,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
AuthFormCleared: {
|
||||
actions: assign({
|
||||
email: "",
|
||||
password: "",
|
||||
username: "",
|
||||
confirmPassword: "",
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
AuthReset: {
|
||||
actions: assign(() => initialContext),
|
||||
},
|
||||
AuthEmailLoginSubmitted: "loadingEmailLogin",
|
||||
AuthEmailRegisterSubmitted: "loadingEmailRegister",
|
||||
AuthGoogleLoginSubmitted: {
|
||||
actions: assign({
|
||||
errorMessage: "Use the Google button to sign in on web",
|
||||
}),
|
||||
},
|
||||
AuthFacebookLoginSubmitted: "loadingFacebookLogin",
|
||||
AuthAppleLoginSubmitted: {
|
||||
actions: assign({
|
||||
errorMessage: "Apple login not implemented",
|
||||
}),
|
||||
},
|
||||
AuthWebGoogleLoginSuccess: "loadingWebGoogleLogin",
|
||||
},
|
||||
},
|
||||
|
||||
loadingEmailLogin: {
|
||||
invoke: {
|
||||
src: "emailLogin",
|
||||
input: ({ event }) => {
|
||||
if (event.type !== "AuthEmailLoginSubmitted")
|
||||
return { email: "", password: "" };
|
||||
return { email: event.email, password: event.password };
|
||||
},
|
||||
onDone: {
|
||||
target: "success",
|
||||
actions: assign({
|
||||
loginType: ({ event }) => event.output,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: assign({
|
||||
errorMessage: ({ event }) =>
|
||||
event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
loadingEmailRegister: {
|
||||
invoke: {
|
||||
src: "emailRegisterThenLogin",
|
||||
input: ({ event }) => {
|
||||
if (event.type !== "AuthEmailRegisterSubmitted")
|
||||
return { email: "", password: "", username: "", confirmPassword: "" };
|
||||
return {
|
||||
email: event.email,
|
||||
password: event.password,
|
||||
username: event.username,
|
||||
confirmPassword: event.confirmPassword,
|
||||
};
|
||||
},
|
||||
onDone: {
|
||||
target: "success",
|
||||
actions: assign({
|
||||
loginType: ({ event }) => event.output,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: assign({
|
||||
errorMessage: ({ event }) =>
|
||||
event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
loadingFacebookLogin: {
|
||||
invoke: {
|
||||
src: "facebookLogin",
|
||||
input: () => ({}),
|
||||
onDone: {
|
||||
target: "success",
|
||||
actions: assign({
|
||||
loginType: ({ event }) => event.output,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: assign({
|
||||
errorMessage: ({ event }) =>
|
||||
event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
loadingWebGoogleLogin: {
|
||||
invoke: {
|
||||
src: "googleLogin",
|
||||
input: ({ event }) => {
|
||||
if (event.type !== "AuthWebGoogleLoginSuccess")
|
||||
return { idToken: "" };
|
||||
return { idToken: event.idToken };
|
||||
},
|
||||
onDone: {
|
||||
target: "success",
|
||||
actions: assign({
|
||||
loginType: ({ event }) => event.output,
|
||||
errorMessage: null,
|
||||
}),
|
||||
},
|
||||
onError: {
|
||||
target: "idle",
|
||||
actions: assign({
|
||||
errorMessage: ({ event }) =>
|
||||
event.error instanceof Error ? event.error.message : String(event.error),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
success: {
|
||||
// 终态:业务层通过 state.matches("success") 判断
|
||||
// isSuccess 由 context 推算(loginType !== "none")
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type AuthMachine = typeof authMachine;
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Auth 类型(向后兼容 re-export)
|
||||
*
|
||||
* 原始文件定义了 AuthState 接口、AuthEvent 联合、initialAuthState。
|
||||
* 状态机重构后:
|
||||
* - `AuthEvent` 联合由 `auth-machine.ts` 重新导出(保留类型)
|
||||
* - `AuthState` 由 `auth-context.tsx` 导出(保持原 API 兼容)
|
||||
*/
|
||||
export type { AuthEvent } from "./auth-machine";
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @file Automatically generated by barrelsby.
|
||||
*/
|
||||
|
||||
export * from "./auth-context";
|
||||
export * from "./auth-machine";
|
||||
export * from "./auth-types";
|
||||
Reference in New Issue
Block a user