refactor(private-zoom): rename full feature surface

This commit is contained in:
Codex
2026-07-22 18:21:47 +08:00
parent e813333607
commit ed3b34ce1c
102 changed files with 695 additions and 519 deletions
@@ -0,0 +1,34 @@
import { fromPromise } from "xstate";
import type {
PrivateAlbumsResponse,
PrivateAlbumUnlockResponse,
} from "@/data/schemas/private-zoom";
import { getPrivateZoomRepository } from "@/data/repositories/private_zoom_repository";
import { Result } from "@/utils/result";
import { PRIVATE_ZOOM_PAGE_SIZE } from "../../helper/albums";
export const loadPrivateAlbumsActor =
fromPromise<PrivateAlbumsResponse, { characterId: string }>(async ({
input,
}) => {
const result = await getPrivateZoomRepository().getAlbums({
characterId: input.characterId,
limit: PRIVATE_ZOOM_PAGE_SIZE,
});
if (Result.isErr(result)) throw result.error;
return result.data;
});
export const unlockPrivateAlbumActor = fromPromise<
PrivateAlbumUnlockResponse,
{ albumId: string; expectedCost: number }
>(async ({ input }) => {
const result = await getPrivateZoomRepository().unlockAlbum(
input.albumId,
input.expectedCost,
);
if (Result.isErr(result)) throw result.error;
return result.data;
});
@@ -0,0 +1,65 @@
import type { DoneActorEvent, ErrorActorEvent } from "xstate";
import type { PrivateAlbumsResponse } from "@/data/schemas/private-zoom";
import { applyAlbumsResponse } from "../helper/albums";
import { toPrivateZoomErrorMessage } from "../helper/unlock";
import {
basePrivateZoomMachineSetup,
createPrivateZoomActorActionSetup,
} from "./setup";
const clearAlbumLoadStateAction = basePrivateZoomMachineSetup.assign({
errorMessage: null,
unlockPaywallRequest: null,
});
export const albumMachineSetup = basePrivateZoomMachineSetup.extend({
actions: {
clearAlbumLoadState: clearAlbumLoadStateAction,
},
});
const albumsDoneSetup =
createPrivateZoomActorActionSetup<
DoneActorEvent<PrivateAlbumsResponse>
>();
const albumErrorSetup =
createPrivateZoomActorActionSetup<ErrorActorEvent>();
const applyAlbumsResponseAction = albumsDoneSetup.assign(({ event }) =>
applyAlbumsResponse(event.output),
);
const applyAlbumLoadErrorAction = albumErrorSetup.assign(({ event }) => ({
errorMessage: toPrivateZoomErrorMessage(event.error),
}));
export const idleState = albumMachineSetup.createStateConfig({
on: {
PrivateZoomInit: "loading",
},
});
export const loadingState = albumMachineSetup.createStateConfig({
entry: "clearAlbumLoadState",
invoke: {
src: "loadAlbums",
input: ({ context }) => ({ characterId: context.characterId }),
onDone: {
target: "ready",
actions: applyAlbumsResponseAction,
},
onError: {
target: "failed",
actions: applyAlbumLoadErrorAction,
},
},
});
export const failedState = albumMachineSetup.createStateConfig({
on: {
PrivateZoomInit: "loading",
PrivateZoomRefresh: "loading",
},
});
+60
View File
@@ -0,0 +1,60 @@
import { setup, type ActionFunction, type EventObject } from "xstate";
import { getPendingAlbum } from "../helper/albums";
import type { PrivateZoomEvent } from "../private-zoom-events";
import type { PrivateZoomState } from "../private-zoom-state";
import {
loadPrivateAlbumsActor,
unlockPrivateAlbumActor,
} from "./actors/albums";
export interface PrivateZoomMachineInput {
characterId: string;
}
export const basePrivateZoomMachineSetup = setup({
types: {
context: {} as PrivateZoomState,
events: {} as PrivateZoomEvent,
input: {} as PrivateZoomMachineInput,
},
actors: {
loadAlbums: loadPrivateAlbumsActor,
unlockAlbum: unlockPrivateAlbumActor,
},
guards: {
hasPendingAlbum: ({ context }) => getPendingAlbum(context) !== null,
},
});
type PrivateZoomActorAction<TEvent extends EventObject> = ActionFunction<
PrivateZoomState,
TEvent,
PrivateZoomEvent,
undefined,
never,
never,
never,
never,
never
>;
export function createPrivateZoomActorActionSetup<
TEvent extends EventObject,
>() {
const actorActionSetup = setup({
types: {
context: {} as PrivateZoomState,
events: {} as TEvent,
},
});
return {
assign(
assignment: Parameters<typeof actorActionSetup.assign>[0],
): PrivateZoomActorAction<TEvent> {
return actorActionSetup.assign(
assignment,
) as unknown as PrivateZoomActorAction<TEvent>;
},
};
}
@@ -0,0 +1,188 @@
import type { DoneActorEvent, ErrorActorEvent } from "xstate";
import type { PrivateAlbumUnlockResponse } from "@/data/schemas/private-zoom";
import {
getPendingAlbum,
patchAlbumInList,
removeAlbum,
} from "../helper/albums";
import {
toPaywallRequest,
toPrivateZoomErrorMessage,
toUnlockErrorMessage,
} from "../helper/unlock";
import {
albumMachineSetup,
failedState,
idleState,
loadingState,
} from "./album-flow";
import { createPrivateZoomActorActionSetup } from "./setup";
const requestUnlockAction = albumMachineSetup.assign(({ event }) =>
event.type === "PrivateZoomUnlockRequested"
? {
pendingConfirmAlbumId: event.albumId,
unlockErrorMessage: null,
unlockPaywallRequest: null,
}
: {},
);
const cancelUnlockAction = albumMachineSetup.assign({
pendingConfirmAlbumId: null,
});
const clearUnlockPaywallAction = albumMachineSetup.assign({
unlockPaywallRequest: null,
});
const beginUnlockAction = albumMachineSetup.assign(({ context }) => ({
unlockingAlbumId: context.pendingConfirmAlbumId,
unlockErrorMessage: null,
unlockPaywallRequest: null,
}));
export const privateZoomMachineSetup = albumMachineSetup.extend({
actions: {
requestUnlock: requestUnlockAction,
cancelUnlock: cancelUnlockAction,
clearUnlockPaywall: clearUnlockPaywallAction,
beginUnlock: beginUnlockAction,
},
});
const unlockDoneSetup =
createPrivateZoomActorActionSetup<
DoneActorEvent<PrivateAlbumUnlockResponse>
>();
const unlockErrorSetup =
createPrivateZoomActorActionSetup<ErrorActorEvent>();
const applyUnlockSuccessAction = unlockDoneSetup.assign(
({ context, event }) => ({
items: patchAlbumInList(context.items, event.output),
creditBalance: event.output.creditBalance,
pendingConfirmAlbumId: null,
unlockingAlbumId: null,
unlockErrorMessage: null,
unlockSuccessNonce: context.unlockSuccessNonce + 1,
}),
);
const applyUnlockPaywallAction = unlockDoneSetup.assign(
({ context, event }) => ({
items: patchAlbumInList(context.items, event.output),
creditBalance: event.output.creditBalance,
pendingConfirmAlbumId: null,
unlockingAlbumId: null,
unlockErrorMessage: null,
unlockPaywallRequest: toPaywallRequest(event.output),
}),
);
const applyCostChangedAction = unlockDoneSetup.assign(({ event }) => ({
pendingConfirmAlbumId: null,
unlockingAlbumId: null,
unlockErrorMessage: toUnlockErrorMessage(event.output),
}));
const applyAlbumNotFoundAction = unlockDoneSetup.assign(
({ context, event }) => ({
items: removeAlbum(context.items, event.output.albumId),
creditBalance: event.output.creditBalance,
pendingConfirmAlbumId: null,
unlockingAlbumId: null,
unlockErrorMessage: toUnlockErrorMessage(event.output),
}),
);
const applyUnlockFailureAction = unlockDoneSetup.assign(
({ context, event }) => ({
items: patchAlbumInList(context.items, event.output),
creditBalance: event.output.creditBalance,
pendingConfirmAlbumId: null,
unlockingAlbumId: null,
unlockErrorMessage: toUnlockErrorMessage(event.output),
}),
);
const applyUnlockActorErrorAction = unlockErrorSetup.assign(({ event }) => ({
pendingConfirmAlbumId: null,
unlockingAlbumId: null,
unlockErrorMessage: toPrivateZoomErrorMessage(event.error),
}));
const readyState = privateZoomMachineSetup.createStateConfig({
on: {
PrivateZoomInit: "loading",
PrivateZoomRefresh: "loading",
PrivateZoomUnlockRequested: { actions: "requestUnlock" },
PrivateZoomUnlockCancelled: { actions: "cancelUnlock" },
PrivateZoomUnlockConfirmed: [
{
guard: "hasPendingAlbum",
target: "unlocking",
},
],
PrivateZoomUnlockPaywallConsumed: { actions: "clearUnlockPaywall" },
},
});
const unlockingState = privateZoomMachineSetup.createStateConfig({
entry: "beginUnlock",
invoke: {
src: "unlockAlbum",
input: ({ context }) => {
const pendingAlbum = getPendingAlbum(context);
return {
albumId: pendingAlbum?.albumId ?? "",
expectedCost: pendingAlbum?.unlockCost ?? 0,
};
},
onDone: [
{
guard: ({ event }) => event.output.unlocked && !event.output.locked,
target: "ready",
actions: applyUnlockSuccessAction,
},
{
guard: ({ event }) =>
event.output.reason === "insufficient_credits",
target: "ready",
actions: applyUnlockPaywallAction,
},
{
guard: ({ event }) => event.output.reason === "cost_changed",
target: "loading",
actions: applyCostChangedAction,
},
{
guard: ({ event }) => event.output.reason === "not_found",
target: "ready",
actions: applyAlbumNotFoundAction,
},
{
target: "ready",
actions: applyUnlockFailureAction,
},
],
onError: {
target: "ready",
actions: applyUnlockActorErrorAction,
},
},
});
export const privateZoomRootStateConfig =
privateZoomMachineSetup.createStateConfig({
initial: "idle",
states: {
idle: idleState,
loading: loadingState,
failed: failedState,
ready: readyState,
unlocking: unlockingState,
},
});