import type { DoneActorEvent, ErrorActorEvent } from "xstate"; import type { CreatePaymentOrderResponse, PaymentOrderStatusResponse, } from "@/data/schemas/payment"; import { behaviorAnalytics } from "@/lib/analytics"; import { getCurrentOrderPlan, getSelectedPlan, hasOrderPollingTimedOut, PAYMENT_TIMEOUT_ERROR_MESSAGE, toPaymentErrorMessage, } from "../helper/order"; import { catalogMachineSetup, idleState, loadingCachedPlansState, loadingPlansState, readyState, refreshingPlansState, } from "./catalog-flow"; import { createPaymentActorActionSetup } from "./setup"; const POLL_DELAY_MS = 4000; const trackCreateOrderStartAction = catalogMachineSetup.createAction( ({ context }) => { const plan = getSelectedPlan(context); if (plan) behaviorAnalytics.createOrderStart(plan, context.payChannel); }, ); const trackReturnedPollStartAction = catalogMachineSetup.createAction( ({ event }) => { if (event.type === "PaymentReturned") { behaviorAnalytics.statusPollStart(event.orderId); } }, ); const applyReturnedOrderAction = catalogMachineSetup.assign(({ event }) => { if (event.type !== "PaymentReturned") return {}; return { currentOrderId: event.orderId, currentOrderPlanId: null, payParams: null, orderStatus: "pending", orderPollingStartedAt: event.createdAt ?? Date.now(), errorMessage: null, }; }); const applyLaunchFailureAction = catalogMachineSetup.assign(({ event }) => event.type === "PaymentLaunchFailed" ? { errorMessage: event.errorMessage } : {}, ); export const paymentMachineSetup = catalogMachineSetup.extend({ actions: { trackCreateOrderStart: trackCreateOrderStartAction, trackReturnedPollStart: trackReturnedPollStartAction, applyReturnedOrder: applyReturnedOrderAction, applyLaunchFailure: applyLaunchFailureAction, }, }); const createOrderDoneSetup = createPaymentActorActionSetup< DoneActorEvent >(); const pollOrderDoneSetup = createPaymentActorActionSetup< DoneActorEvent >(); const orderErrorSetup = createPaymentActorActionSetup(); const trackCreateOrderSuccessAction = createOrderDoneSetup.createAction( ({ context, event }) => { const plan = getSelectedPlan(context); if (plan) { behaviorAnalytics.createOrderSuccess( plan, event.output.orderId, context.payChannel, ); } behaviorAnalytics.statusPollStart(event.output.orderId, plan); }, ); const applyCreateOrderSuccessAction = createOrderDoneSetup.assign( ({ context, event }) => ({ currentOrderId: event.output.orderId, currentOrderPlanId: context.selectedPlanId, payParams: event.output.payParams, orderStatus: "pending", orderPollingStartedAt: Date.now(), errorMessage: null, launchNonce: context.launchNonce + 1, }), ); const trackCreateOrderFailureAction = orderErrorSetup.createAction( ({ context }) => { const plan = getSelectedPlan(context); if (plan) behaviorAnalytics.createOrderFailed(plan, context.payChannel); }, ); const applyOrderErrorAction = orderErrorSetup.assign(({ event }) => ({ errorMessage: toPaymentErrorMessage(event.error), })); const trackPollUpdateAction = pollOrderDoneSetup.createAction( ({ context, event }) => behaviorAnalytics.statusPollUpdate( context.currentOrderId ?? "", event.output.status, getCurrentOrderPlan(context), ), ); const trackPollTimeoutAction = pollOrderDoneSetup.createAction( ({ context, event }) => { const orderId = context.currentOrderId ?? ""; const plan = getCurrentOrderPlan(context); behaviorAnalytics.statusPollUpdate(orderId, event.output.status, plan); behaviorAnalytics.statusPollTimeout(orderId, plan); }, ); const applyPaidOrderAction = pollOrderDoneSetup.assign(({ event }) => ({ orderStatus: event.output.status, orderPollingStartedAt: null, errorMessage: null, })); const applyFailedOrderAction = pollOrderDoneSetup.assign(({ event }) => ({ orderStatus: event.output.status, orderPollingStartedAt: null, errorMessage: "Payment failed or was cancelled.", })); const applyTimedOutOrderAction = pollOrderDoneSetup.assign({ orderStatus: "failed", orderPollingStartedAt: null, errorMessage: PAYMENT_TIMEOUT_ERROR_MESSAGE, }); const applyPendingOrderAction = pollOrderDoneSetup.assign(({ event }) => ({ orderStatus: event.output.status, errorMessage: null, })); const creatingOrderState = paymentMachineSetup.createStateConfig({ entry: "trackCreateOrderStart", invoke: { src: "createOrder", input: ({ context, event }) => ({ planId: context.selectedPlanId, payChannel: context.payChannel, autoRenew: context.autoRenew, ...(event.type === "PaymentCreateOrderSubmitted" && event.recipientCharacterId ? { recipientCharacterId: event.recipientCharacterId } : {}), }), onDone: { target: "pollingOrder", actions: [ trackCreateOrderSuccessAction, applyCreateOrderSuccessAction, ], }, onError: { target: "failed", actions: [trackCreateOrderFailureAction, applyOrderErrorAction], }, }, }); const pollingOrderState = paymentMachineSetup.createStateConfig({ invoke: { src: "pollOrderStatus", input: ({ context }) => ({ orderId: context.currentOrderId ?? "" }), onDone: [ { guard: ({ event }) => event.output.status === "paid", target: "paid", actions: [trackPollUpdateAction, applyPaidOrderAction], }, { guard: ({ event }) => event.output.status === "failed", target: "failed", actions: [trackPollUpdateAction, applyFailedOrderAction], }, { guard: ({ context }) => hasOrderPollingTimedOut(context.orderPollingStartedAt), target: "failed", actions: [trackPollTimeoutAction, applyTimedOutOrderAction], }, { target: "waitingForPayment", actions: [trackPollUpdateAction, applyPendingOrderAction], }, ], onError: { target: "failed", actions: applyOrderErrorAction, }, }, on: { PaymentReset: { target: "ready", actions: "resetOrder", }, }, }); const waitingForPaymentState = paymentMachineSetup.createStateConfig({ after: { [POLL_DELAY_MS]: "pollingOrder", }, on: { PaymentReset: { target: "ready", actions: "resetOrder", }, }, }); const paidState = paymentMachineSetup.createStateConfig({ on: { PaymentPlanSelected: { target: "ready", actions: "selectPlan", }, PaymentPayChannelChanged: { target: "ready", actions: "changePayChannel", }, PaymentCreateOrderSubmitted: [ { guard: "canCreateOrder", target: "creatingOrder", actions: "resetOrder", }, { target: "ready", actions: "showCreateOrderValidationError", }, ], PaymentReset: { target: "ready", actions: "resetOrder", }, }, }); const failedState = paymentMachineSetup.createStateConfig({ on: { PaymentCreateOrderSubmitted: [ { guard: "canCreateOrder", target: "creatingOrder", actions: "resetOrder", }, ], PaymentErrorCleared: { target: "ready", actions: "clearPaymentError", }, PaymentReset: { target: "ready", actions: "resetOrder", }, }, }); export const paymentRootStateConfig = paymentMachineSetup.createStateConfig({ initial: "idle", states: { idle: idleState, loadingCachedPlans: loadingCachedPlansState, loadingPlans: loadingPlansState, refreshingPlans: refreshingPlansState, ready: readyState, creatingOrder: creatingOrderState, pollingOrder: pollingOrderState, waitingForPayment: waitingForPaymentState, paid: paidState, failed: failedState, }, on: { PaymentFirstRechargeConsumed: { actions: "consumeFirstRecharge" }, PaymentReturned: { target: ".pollingOrder", actions: ["trackReturnedPollStart", "applyReturnedOrder"], }, PaymentLaunchFailed: { target: ".failed", actions: "applyLaunchFailure", }, }, });