263 lines
8.2 KiB
TypeScript
263 lines
8.2 KiB
TypeScript
import { assign, setup } from "xstate";
|
|
|
|
import type {
|
|
PrivateZonePostsResponse,
|
|
PrivateZonePostUnlockResponse,
|
|
PrivateZoneVideoPost,
|
|
} from "@/data/schemas/private-zone";
|
|
|
|
import {
|
|
loadPrivateZonePostsActor,
|
|
unlockPrivateZonePostActor,
|
|
} from "./private-zone-post-actors";
|
|
import type { PrivateZonePostEvent } from "./private-zone-post-events";
|
|
import {
|
|
createInitialPrivateZonePostState,
|
|
type PrivateZonePostState,
|
|
} from "./private-zone-post-state";
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error && error.message
|
|
? error.message
|
|
: "Moments are temporarily unavailable. Please try again.";
|
|
}
|
|
|
|
function pendingPost(context: PrivateZonePostState): PrivateZoneVideoPost | null {
|
|
if (!context.pendingConfirmPostId) return null;
|
|
return context.items.find(
|
|
(post) => post.postId === context.pendingConfirmPostId,
|
|
) ?? null;
|
|
}
|
|
|
|
function patchPost(
|
|
items: readonly PrivateZoneVideoPost[],
|
|
response: PrivateZonePostUnlockResponse,
|
|
): readonly PrivateZoneVideoPost[] {
|
|
if (!response.post) return items;
|
|
return items.map((post) =>
|
|
post.postId === response.postId ? response.post! : post,
|
|
);
|
|
}
|
|
|
|
function appendPosts(
|
|
current: readonly PrivateZoneVideoPost[],
|
|
response: PrivateZonePostsResponse,
|
|
): readonly PrivateZoneVideoPost[] {
|
|
const byId = new Map(current.map((post) => [post.postId, post]));
|
|
response.items.forEach((post) => byId.set(post.postId, post));
|
|
return [...byId.values()];
|
|
}
|
|
|
|
const machineSetup = setup({
|
|
types: {
|
|
context: {} as PrivateZonePostState,
|
|
events: {} as PrivateZonePostEvent,
|
|
input: {} as { characterId: string },
|
|
},
|
|
actors: {
|
|
loadPosts: loadPrivateZonePostsActor,
|
|
unlockPost: unlockPrivateZonePostActor,
|
|
},
|
|
guards: {
|
|
hasMore: ({ context }) => context.hasMore && !!context.nextCursor,
|
|
hasPendingPost: ({ context }) => pendingPost(context) !== null,
|
|
},
|
|
});
|
|
|
|
export const privateZonePostMachine = machineSetup.createMachine({
|
|
id: "privateZonePosts",
|
|
context: ({ input }) => createInitialPrivateZonePostState(input.characterId),
|
|
initial: "idle",
|
|
states: {
|
|
idle: {
|
|
on: { PrivateZonePostInit: "loading" },
|
|
},
|
|
loading: {
|
|
entry: assign({
|
|
errorMessage: null,
|
|
unlockPaywallRequest: null,
|
|
}),
|
|
invoke: {
|
|
src: "loadPosts",
|
|
input: ({ context }) => ({ characterId: context.characterId }),
|
|
onDone: {
|
|
target: "ready",
|
|
actions: assign(({ event }) => ({
|
|
items: event.output.items,
|
|
creditBalance: event.output.creditBalance,
|
|
nextCursor: event.output.nextCursor,
|
|
hasMore: event.output.hasMore,
|
|
errorMessage: null,
|
|
})),
|
|
},
|
|
onError: {
|
|
target: "failed",
|
|
actions: assign(({ event }) => ({
|
|
errorMessage: errorMessage(event.error),
|
|
})),
|
|
},
|
|
},
|
|
},
|
|
failed: {
|
|
on: {
|
|
PrivateZonePostInit: "loading",
|
|
PrivateZonePostRefresh: "loading",
|
|
},
|
|
},
|
|
ready: {
|
|
on: {
|
|
PrivateZonePostRefresh: "loading",
|
|
PrivateZonePostLoadMore: {
|
|
guard: "hasMore",
|
|
target: "loadingMore",
|
|
},
|
|
PrivateZonePostUnlockRequested: {
|
|
actions: assign(({ event }) => ({
|
|
pendingConfirmPostId: event.postId,
|
|
unlockErrorMessage: null,
|
|
unlockPaywallRequest: null,
|
|
})),
|
|
},
|
|
PrivateZonePostUnlockCancelled: {
|
|
actions: assign({ pendingConfirmPostId: null }),
|
|
},
|
|
PrivateZonePostUnlockConfirmed: {
|
|
guard: "hasPendingPost",
|
|
target: "unlocking",
|
|
},
|
|
PrivateZonePostUnlockPaywallConsumed: {
|
|
actions: assign({ unlockPaywallRequest: null }),
|
|
},
|
|
},
|
|
},
|
|
loadingMore: {
|
|
invoke: {
|
|
src: "loadPosts",
|
|
input: ({ context }) => ({
|
|
characterId: context.characterId,
|
|
cursor: context.nextCursor,
|
|
}),
|
|
onDone: {
|
|
target: "ready",
|
|
actions: assign(({ context, event }) => ({
|
|
items: appendPosts(context.items, event.output),
|
|
creditBalance: event.output.creditBalance,
|
|
nextCursor: event.output.nextCursor,
|
|
hasMore: event.output.hasMore,
|
|
errorMessage: null,
|
|
})),
|
|
},
|
|
onError: {
|
|
target: "ready",
|
|
actions: assign(({ event }) => ({
|
|
errorMessage: errorMessage(event.error),
|
|
})),
|
|
},
|
|
},
|
|
},
|
|
unlocking: {
|
|
entry: assign(({ context }) => ({
|
|
unlockingPostId: context.pendingConfirmPostId,
|
|
unlockErrorMessage: null,
|
|
unlockPaywallRequest: null,
|
|
})),
|
|
invoke: {
|
|
src: "unlockPost",
|
|
input: ({ context }) => {
|
|
const post = pendingPost(context);
|
|
return {
|
|
postId: post?.postId ?? "",
|
|
expectedCost: post?.unlockCostCredits ?? 0,
|
|
};
|
|
},
|
|
onDone: [
|
|
{
|
|
guard: ({ event }) => event.output.unlocked,
|
|
target: "ready",
|
|
actions: assign(({ context, event }) => ({
|
|
items: patchPost(context.items, event.output),
|
|
creditBalance: event.output.creditBalance,
|
|
pendingConfirmPostId: null,
|
|
unlockingPostId: null,
|
|
unlockErrorMessage: null,
|
|
unlockSuccessNonce: context.unlockSuccessNonce + 1,
|
|
})),
|
|
},
|
|
{
|
|
guard: ({ event }) =>
|
|
event.output.reason === "insufficient_credits",
|
|
target: "ready",
|
|
actions: assign(({ context, event }) => ({
|
|
items: patchPost(context.items, event.output),
|
|
creditBalance: event.output.creditBalance,
|
|
pendingConfirmPostId: null,
|
|
unlockingPostId: null,
|
|
unlockErrorMessage: null,
|
|
unlockPaywallRequest: {
|
|
postId: event.output.postId,
|
|
reason: event.output.reason,
|
|
requiredCredits:
|
|
event.output.requiredCredits ||
|
|
event.output.post?.unlockCostCredits ||
|
|
0,
|
|
currentCredits:
|
|
event.output.currentCredits || event.output.creditBalance,
|
|
shortfallCredits: event.output.shortfallCredits,
|
|
...(event.output.paymentGuidance
|
|
? { paymentGuidance: event.output.paymentGuidance }
|
|
: {}),
|
|
},
|
|
})),
|
|
},
|
|
{
|
|
guard: ({ event }) => event.output.reason === "cost_changed",
|
|
target: "ready",
|
|
actions: assign(({ context, event }) => ({
|
|
items: patchPost(context.items, event.output),
|
|
creditBalance: event.output.creditBalance,
|
|
pendingConfirmPostId: null,
|
|
unlockingPostId: null,
|
|
unlockErrorMessage:
|
|
"Unlock price changed. Please review the new price and confirm again.",
|
|
})),
|
|
},
|
|
{
|
|
guard: ({ event }) =>
|
|
event.output.reason === "not_found" ||
|
|
event.output.reason === "unavailable",
|
|
target: "ready",
|
|
actions: assign(({ context, event }) => ({
|
|
items: context.items.filter(
|
|
(post) => post.postId !== event.output.postId,
|
|
),
|
|
pendingConfirmPostId: null,
|
|
unlockingPostId: null,
|
|
unlockErrorMessage: "This moment is no longer available.",
|
|
})),
|
|
},
|
|
{
|
|
target: "ready",
|
|
actions: assign(({ context, event }) => ({
|
|
items: patchPost(context.items, event.output),
|
|
creditBalance: event.output.creditBalance,
|
|
pendingConfirmPostId: null,
|
|
unlockingPostId: null,
|
|
unlockErrorMessage: "Unlock failed. Please try again.",
|
|
})),
|
|
},
|
|
],
|
|
onError: {
|
|
target: "ready",
|
|
actions: assign(({ event }) => ({
|
|
pendingConfirmPostId: null,
|
|
unlockingPostId: null,
|
|
unlockErrorMessage: errorMessage(event.error),
|
|
})),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
export type PrivateZonePostMachine = typeof privateZonePostMachine;
|