Files
cozsweet-frontend-nextjs/src/stores/private-zone/machine/unlock-flow.ts
T

189 lines
5.1 KiB
TypeScript

import type { DoneActorEvent, ErrorActorEvent } from "xstate";
import type { PrivateAlbumUnlockResponse } from "@/data/schemas/private-zone";
import {
getPendingAlbum,
patchAlbumInList,
removeAlbum,
} from "../helper/albums";
import {
toPaywallRequest,
toPrivateZoneErrorMessage,
toUnlockErrorMessage,
} from "../helper/unlock";
import {
albumMachineSetup,
failedState,
idleState,
loadingState,
} from "./album-flow";
import { createPrivateZoneActorActionSetup } from "./setup";
const requestUnlockAction = albumMachineSetup.assign(({ event }) =>
event.type === "PrivateZoneUnlockRequested"
? {
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 privateZoneMachineSetup = albumMachineSetup.extend({
actions: {
requestUnlock: requestUnlockAction,
cancelUnlock: cancelUnlockAction,
clearUnlockPaywall: clearUnlockPaywallAction,
beginUnlock: beginUnlockAction,
},
});
const unlockDoneSetup =
createPrivateZoneActorActionSetup<
DoneActorEvent<PrivateAlbumUnlockResponse>
>();
const unlockErrorSetup =
createPrivateZoneActorActionSetup<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: toPrivateZoneErrorMessage(event.error),
}));
const readyState = privateZoneMachineSetup.createStateConfig({
on: {
PrivateZoneInit: "loading",
PrivateZoneRefresh: "loading",
PrivateZoneUnlockRequested: { actions: "requestUnlock" },
PrivateZoneUnlockCancelled: { actions: "cancelUnlock" },
PrivateZoneUnlockConfirmed: [
{
guard: "hasPendingAlbum",
target: "unlocking",
},
],
PrivateZoneUnlockPaywallConsumed: { actions: "clearUnlockPaywall" },
},
});
const unlockingState = privateZoneMachineSetup.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 privateZoneRootStateConfig =
privateZoneMachineSetup.createStateConfig({
initial: "idle",
states: {
idle: idleState,
loading: loadingState,
failed: failedState,
ready: readyState,
unlocking: unlockingState,
},
});