refactor: migrate state imports from contexts to stores directory

This commit is contained in:
2026-06-09 18:47:04 +08:00
parent f79755f6ec
commit a5d8214650
29 changed files with 72 additions and 50 deletions
+322
View File
@@ -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;