refactor(private-zoom): rename full feature surface
This commit is contained in:
@@ -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",
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user