Files
cozsweet-frontend-nextjs/src/stores/payment/machine/catalog-flow.ts
T

320 lines
9.1 KiB
TypeScript

import type { DoneActorEvent, ErrorActorEvent } from "xstate";
import type {
GiftProductsResponse,
PaymentPlansResponse,
} from "@/data/schemas/payment";
import {
consumeFirstRechargeState,
hydrateGiftProductsState,
hydratePlansResponseState,
refreshPlansResponseState,
selectPlanState,
} from "../helper";
import { resetOrderState, toPaymentErrorMessage } from "../helper/order";
import type { PaymentEvent } from "../payment-events";
import type { PaymentState } from "../payment-state";
import {
basePaymentMachineSetup,
createPaymentActorActionSetup,
} from "./setup";
function initializeCatalogState(
context: PaymentState,
event: Extract<PaymentEvent, { type: "PaymentInit" }>,
): Partial<PaymentState> {
const planCatalog = event.catalog ?? context.planCatalog;
const giftCharacterId =
planCatalog === "tip"
? (event.characterId ?? context.giftCharacterId)
: null;
const catalogChanged = planCatalog !== context.planCatalog;
const characterChanged = giftCharacterId !== context.giftCharacterId;
const selectionChanged =
planCatalog === "tip" &&
((event.category ?? null) !== context.selectedGiftCategory ||
(event.planId ?? null) !== (context.selectedPlanId || null));
const commercialOfferId =
planCatalog === "default" ? (event.commercialOfferId ?? null) : null;
const offerChanged = commercialOfferId !== context.commercialOfferId;
return {
planCatalog,
...(event.payChannel ? { payChannel: event.payChannel } : {}),
giftCharacterId,
commercialOfferId,
requestedGiftCategory:
planCatalog === "tip" ? (event.category ?? null) : null,
requestedGiftPlanId:
planCatalog === "tip" ? (event.planId ?? null) : null,
...(catalogChanged || characterChanged || selectionChanged || offerChanged
? {
...resetOrderState(),
plans: [],
giftCategories: [],
giftProducts: [],
selectedGiftCategory: null,
selectedPlanId:
planCatalog === "default" ? (event.planId ?? "") : "",
isFirstRecharge: false,
commercialOffer: null,
autoRenew: planCatalog !== "tip",
}
: {}),
};
}
const initializeCatalogAction = basePaymentMachineSetup.assign(
({ context, event }) =>
event.type === "PaymentInit"
? initializeCatalogState(context, event)
: {},
);
const refreshCatalogAction = basePaymentMachineSetup.assign(
({ context, event }) =>
event.type === "PaymentInit"
? initializeCatalogState(context, event)
: {},
);
const selectPlanAction = basePaymentMachineSetup.assign(
({ context, event }) =>
event.type === "PaymentPlanSelected"
? selectPlanState(event.planId, context.plans)
: {},
);
const changePayChannelAction = basePaymentMachineSetup.assign(({ event }) =>
event.type === "PaymentPayChannelChanged"
? {
...resetOrderState(),
payChannel: event.payChannel,
errorMessage: null,
}
: {},
);
const changeAutoRenewAction = basePaymentMachineSetup.assign(({ event }) =>
event.type === "PaymentAutoRenewChanged"
? { autoRenew: event.autoRenew, errorMessage: null }
: {},
);
const changeAgreementAction = basePaymentMachineSetup.assign(({ event }) =>
event.type === "PaymentAgreementChanged"
? { agreed: event.agreed, errorMessage: null }
: {},
);
const showCreateOrderValidationErrorAction = basePaymentMachineSetup.assign({
errorMessage: "Choose a plan and accept the agreement before continuing.",
});
const clearPaymentErrorAction = basePaymentMachineSetup.assign({
errorMessage: null,
});
const resetOrderAction = basePaymentMachineSetup.assign(() =>
resetOrderState(),
);
const consumeFirstRechargeAction = basePaymentMachineSetup.assign(
({ context }) => consumeFirstRechargeState(context),
);
export const catalogMachineSetup = basePaymentMachineSetup.extend({
actions: {
initializeCatalog: initializeCatalogAction,
refreshCatalog: refreshCatalogAction,
selectPlan: selectPlanAction,
changePayChannel: changePayChannelAction,
changeAutoRenew: changeAutoRenewAction,
changeAgreement: changeAgreementAction,
showCreateOrderValidationError: showCreateOrderValidationErrorAction,
clearPaymentError: clearPaymentErrorAction,
resetOrder: resetOrderAction,
consumeFirstRecharge: consumeFirstRechargeAction,
},
guards: {
isTipInit: ({ context, event }) =>
event.type === "PaymentInit" &&
(event.catalog ?? context.planCatalog) === "tip",
isTipCatalog: ({ context }) => context.planCatalog === "tip",
},
});
const cachedPlansDoneSetup =
createPaymentActorActionSetup<
DoneActorEvent<PaymentPlansResponse | null>
>();
const plansDoneSetup =
createPaymentActorActionSetup<DoneActorEvent<PaymentPlansResponse>>();
const giftProductsDoneSetup =
createPaymentActorActionSetup<DoneActorEvent<GiftProductsResponse>>();
const plansErrorSetup = createPaymentActorActionSetup<ErrorActorEvent>();
const hydrateCachedPlansAction = cachedPlansDoneSetup.assign(
({ context, event }) =>
event.output
? hydratePlansResponseState(event.output, context.selectedPlanId)
: {},
);
const hydratePlansAction = plansDoneSetup.assign(({ context, event }) =>
hydratePlansResponseState(event.output, context.selectedPlanId),
);
const refreshPlansAction = plansDoneSetup.assign(({ context, event }) =>
refreshPlansResponseState(event.output, context.selectedPlanId),
);
const hydrateGiftProductsAction = giftProductsDoneSetup.assign(
({ context, event }) =>
hydrateGiftProductsState(
event.output,
context.giftCharacterId ?? "",
context.requestedGiftCategory ?? context.selectedGiftCategory,
context.requestedGiftPlanId ?? context.selectedPlanId,
),
);
const applyPlansErrorAction = plansErrorSetup.assign(({ event }) => ({
errorMessage: toPaymentErrorMessage(event.error),
}));
const applyGiftProductsErrorAction = plansErrorSetup.assign(({ event }) => ({
plans: [],
giftCategories: [],
giftProducts: [],
selectedGiftCategory: null,
selectedPlanId: "",
errorMessage: toPaymentErrorMessage(event.error),
}));
export const idleState = catalogMachineSetup.createStateConfig({
on: {
PaymentInit: [
{
guard: "isTipInit",
target: "loadingGiftProducts",
actions: "initializeCatalog",
},
{
target: "loadingCachedPlans",
actions: "initializeCatalog",
},
],
},
});
export const loadingCachedPlansState =
catalogMachineSetup.createStateConfig({
invoke: {
src: "loadCachedPlans",
input: ({ context }) => ({
catalog: context.planCatalog,
commercialOfferId: context.commercialOfferId,
}),
onDone: [
{
guard: ({ event }) => (event.output?.plans.length ?? 0) > 0,
target: "refreshingPlans",
actions: hydrateCachedPlansAction,
},
{ target: "loadingPlans" },
],
onError: { target: "loadingPlans" },
},
});
export const loadingPlansState = catalogMachineSetup.createStateConfig({
invoke: {
src: "refreshPlans",
input: ({ context }) => ({
catalog: context.planCatalog,
commercialOfferId: context.commercialOfferId,
}),
onDone: {
target: "ready",
actions: hydratePlansAction,
},
onError: {
target: "ready",
actions: applyPlansErrorAction,
},
},
});
export const refreshingPlansState = catalogMachineSetup.createStateConfig({
invoke: {
src: "refreshPlans",
input: ({ context }) => ({
catalog: context.planCatalog,
commercialOfferId: context.commercialOfferId,
}),
onDone: {
target: "ready",
actions: refreshPlansAction,
},
onError: {
target: "ready",
actions: applyPlansErrorAction,
},
},
});
export const loadingGiftProductsState =
catalogMachineSetup.createStateConfig({
invoke: {
src: "loadGiftProducts",
input: ({ context }) => ({
characterId: context.giftCharacterId ?? "",
}),
onDone: {
target: "ready",
actions: hydrateGiftProductsAction,
},
onError: {
target: "ready",
actions: applyGiftProductsErrorAction,
},
},
});
export const readyState = catalogMachineSetup.createStateConfig({
on: {
PaymentInit: [
{
guard: "isTipInit",
target: "loadingGiftProducts",
actions: "refreshCatalog",
},
{
target: "refreshingPlans",
actions: "refreshCatalog",
},
],
PaymentCatalogRetryRequested: [
{
guard: "isTipCatalog",
target: "loadingGiftProducts",
},
{ target: "refreshingPlans" },
],
PaymentPlanSelected: { actions: "selectPlan" },
PaymentPayChannelChanged: { actions: "changePayChannel" },
PaymentAutoRenewChanged: { actions: "changeAutoRenew" },
PaymentAgreementChanged: { actions: "changeAgreement" },
PaymentCreateOrderSubmitted: [
{
guard: "canCreateOrder",
target: "creatingOrder",
},
{ actions: "showCreateOrderValidationError" },
],
PaymentErrorCleared: { actions: "clearPaymentError" },
},
});