310 lines
8.1 KiB
TypeScript
310 lines
8.1 KiB
TypeScript
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<CreatePaymentOrderResponse>
|
|
>();
|
|
const pollOrderDoneSetup =
|
|
createPaymentActorActionSetup<
|
|
DoneActorEvent<PaymentOrderStatusResponse>
|
|
>();
|
|
const orderErrorSetup = createPaymentActorActionSetup<ErrorActorEvent>();
|
|
|
|
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",
|
|
},
|
|
},
|
|
});
|