347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
/**
|
||
* Payment 状态机(XState v5)
|
||
*/
|
||
import { assign, setup } from "xstate";
|
||
|
||
import type { PaymentEvent } from "./payment-events";
|
||
import { initialState, type PaymentState } from "./payment-state";
|
||
import {
|
||
createPaymentOrderActor,
|
||
loadCachedPaymentPlansActor,
|
||
pollPaymentOrderStatusActor,
|
||
refreshPaymentPlansActor,
|
||
} from "./payment-machine.actors";
|
||
import {
|
||
defaultAutoRenewForPlan,
|
||
getDefaultPlanId,
|
||
resetOrderState,
|
||
toPaymentErrorMessage,
|
||
} from "./payment-machine.helpers";
|
||
|
||
export type { PaymentEvent } from "./payment-events";
|
||
export type { PaymentState } from "./payment-state";
|
||
export { initialState } from "./payment-state";
|
||
|
||
const POLL_DELAY_MS = 4000;
|
||
|
||
export const paymentMachine = setup({
|
||
types: {
|
||
context: {} as PaymentState,
|
||
events: {} as PaymentEvent,
|
||
},
|
||
actors: {
|
||
loadCachedPlans: loadCachedPaymentPlansActor,
|
||
refreshPlans: refreshPaymentPlansActor,
|
||
createOrder: createPaymentOrderActor,
|
||
pollOrderStatus: pollPaymentOrderStatusActor,
|
||
},
|
||
}).createMachine({
|
||
/** @xstate-layout N4IgpgJg5mDOIC5QAcCGBPAtmAdgFwGIAFDbfAGVQFccBjACwDFUBLAG0gG0AGAXURQB7WCzwtBOASAAeiACwAmADQh0iAIwA2bnIB0CuQE5j67t20AOBQF9rKtFlx5dLCB2KknASRyie-JBBkYVFxSUDZBAVDPUMFdQs5dXUAVk0Adk0AZhSUlTUEdUMLXXV0w00LCyzzc00U23tPfBc3MA9HfAAlMAAzACc4egA1FmQAZTxUPCpYfylgkTEJKUjo2PjE5LTMnLzVRBTq3R1iuRTdzWz0xqDm5zZBVAgWHCgiNlQcWAIICTAXDgAG6CADWAIcZAeTxebw+X1gCFeINo0zC-nmgUWoRWEQ0WQsKV0lXSSQuFk0MUM6XyiAs6l0RmMxXSWS0FIUNjsd060Oer3en2+BDA-X6gn6umQnzwvQlmCl910j35cKFiORglRyxwGL4CxCOtW+MJxIspNS6QpVJpB0KpMZKTZBiSWXiMQst0hTl0g2e6A6UJ8fn1WMNYWNhRShiyujOCiq5yOSXUtKihm4jOilTkbKO6Ru3O9LT9EADJF58Jw4zAHFoeC4oaESwjeMKhlMunS3BqCiycm4VviWTTpnSCn02iycSdigMmi9StL5fuFYAwvQvjhaxuvjAIJjmzjwqBIgkrPoe9wFNxUhk5HI09TNLoauOMtxzRYO4ZF7zfWA-qBk4ACCVB4IIPTbgA7rubyNgER5Gm256xpocjVDkCgXNoCiaGmVSZjkSRJOhqTRlyTT-suwH4CBUCDGAUJwfuh5BOGuKnhoVREnhuTxCkWg4WmVp6AOphaEUKScvUf5QgBQEVsxfoNgA8v0ECiuMVAAEaYKIDYHk27EtpxMiIJyRRxuY6iKPScQ1I+drJOOXbqFh6RHLeTpyT6NFKU4a4qWA6maf02l6QZXDqIhJnHpGCQJG5FLnBUeHlPhdqVCUD5WhmpJVDGnpFkugFlrReA9AMQyjBMUwzHMxnYshXGFEYmZ8YJGbnOkWjKHa-aZnEFRWPEZR9b5JZlSuvIAKJihKa4cKggxGbFzWtq1RTqBOBjFISBYKFamRphRjIxJkTrXmkySTc4tAqQKoWir8-yAiC4KKv+D2AWIbzPf0SLAlqaISHq60cSe5lRCRr47VkGQUgS2SjtGeiksyn6JvSDQld9j3-RpL2iuKkrStMcr9Aqxb3QTUAA0DKKg7qfBsRtZlrGSxJpNwHb0vehijm+XYjfUD7xBhxVUfJwRsGwT1E-0r3bu9YIQkqsvy4TYWMyDOrgwaplQ2sMRxpsKY7NkuRptksYpEYiQI9GKTXoW0s+prCthcrAKap9NNSoIcte6KuvauirMxYb8VtusZsJBbGRW-sBTuTojJyJU1xuuh0Z3YHwfay9fwq376v-p7ReA5q4dg6zCgQ0bkZx3ECfbEnexPjUjKsjGmimAOWT9vnlf04rIoLWTMqU9TGtB1rY86zXzMG2GTex6brdbHeuzW85Y3nVkrIWNwgnJHh+fQawf1QIwEoBfgFU9LAYB4GzkORhSmZWjtO3GAO9QLBPj7K+DM-dM4fmqFLHk8kr6hDeHffoD9CDSFgPVAEqBegNn6AACgHGYAAlAQAOcCb6IOQe-derUjB6G-KfeoToMhD3OARC81JpxH2pNEGMbsYE+gYGAWgoIBS1UmNMWYPtVb+yVAIoRIixhiIamHFerMmofzbF-Ls9J4jcIAUcESHYTjTnoaYMwpJeEB1kcIt4oj6oSJLr7YG0jvr0EEdYqAtjxEamBrXFmvBOBRzXjHVqmif46P-uYfRdpM4MgyCNbYFQcgWJka4uRNiFF2J+CTCUUpp7yi+vJKx8i6peOUfrVRjdgnQ1Cdov+MRIlAKysUE4hIYw6ASPbWy+cinpJKQ1CepNckU3yZY1J7jPFKOXuU-xgSkKbWqdoLRv9dENNOvbYkboT45mSJSEerAIBPzgK-ShVSzwEiJCSMkVpKRGFtAUAshg4y2VPkUXMHZPJ7NcIc6qsARgZK8Sclq0MOwvk6kcXIMRNg2zkOkXQ4KYzYU5N2Ps+deisA4Ac5BQVfohUVhFfSeBDKAvmWc00lzLTWluaOB8jyCz9gTAyjsaRUXosgBVeapMlqAVWsSjmJoLnmiuZS6ko46i6ApL1OlGZsLXhZewNlyDn7HLUVQ6G7kyWCopTckVzk+ripvL2aogDoi2G5DgQQml4BYnuNHIFkQAC0mUCj2qJMyN17qMyn3zq4DgtqSXcVdZsPMmdFAJBHHaC4sL3JskMNGdp05fx43kiqWEgoER+r5YUMotKEiVHtrsQko5PKlAyjGfsmF7z52XBm42GhqQMm7L2TkVhpyslHGUWMw5BLlBPrkVk3S6YAxrc3fusKeE5DiZ61Mzk0hEhzmkct15eaUT4S0UeQ6gl2vkP1VON49BulJGYG6R7L7XwFOQm1m7-UIE0HhOG84Bx7quE6xAMQXyjp0e+bQPZuljOKYo2Yw62z9xKPUW95hEnfkaQUfu762naLiKyDInyIBAa2jeF8v9rzfmMAJHddIeylAATZKSJI5UYrQ2qvsRIew7JqIlKoQs70dqSCfbC34CymusEAA */
|
||
id: "payment",
|
||
initial: "idle",
|
||
context: initialState,
|
||
states: {
|
||
idle: {
|
||
on: {
|
||
PaymentInit: "loadingCachedPlans",
|
||
},
|
||
},
|
||
|
||
loadingCachedPlans: {
|
||
invoke: {
|
||
src: "loadCachedPlans",
|
||
onDone: [
|
||
{
|
||
guard: ({ event }) => (event.output?.plans.length ?? 0) > 0,
|
||
target: "refreshingPlans",
|
||
actions: assign(({ context, event }) => {
|
||
const plans = event.output?.plans ?? [];
|
||
const selectedPlanId =
|
||
context.selectedPlanId || getDefaultPlanId(plans);
|
||
return {
|
||
plans,
|
||
selectedPlanId,
|
||
autoRenew: defaultAutoRenewForPlan(selectedPlanId, plans),
|
||
errorMessage: null,
|
||
};
|
||
}),
|
||
},
|
||
{
|
||
target: "loadingPlans",
|
||
},
|
||
],
|
||
onError: {
|
||
target: "loadingPlans",
|
||
},
|
||
},
|
||
},
|
||
|
||
loadingPlans: {
|
||
invoke: {
|
||
src: "refreshPlans",
|
||
onDone: {
|
||
target: "ready",
|
||
actions: assign(({ context, event }) => {
|
||
const plans = event.output.plans;
|
||
const selectedPlanId =
|
||
context.selectedPlanId || getDefaultPlanId(plans);
|
||
return {
|
||
plans,
|
||
selectedPlanId,
|
||
autoRenew: defaultAutoRenewForPlan(selectedPlanId, plans),
|
||
errorMessage: null,
|
||
};
|
||
}),
|
||
},
|
||
onError: {
|
||
target: "ready",
|
||
actions: assign(({ event }) => ({
|
||
errorMessage: toPaymentErrorMessage(event.error),
|
||
})),
|
||
},
|
||
},
|
||
},
|
||
|
||
refreshingPlans: {
|
||
invoke: {
|
||
src: "refreshPlans",
|
||
onDone: {
|
||
target: "ready",
|
||
actions: assign(({ context, event }) => {
|
||
const plans = event.output.plans;
|
||
const selectedPlanId =
|
||
plans.some((plan) => plan.planId === context.selectedPlanId)
|
||
? context.selectedPlanId
|
||
: getDefaultPlanId(plans);
|
||
return {
|
||
plans,
|
||
selectedPlanId,
|
||
autoRenew: defaultAutoRenewForPlan(selectedPlanId, plans),
|
||
errorMessage: null,
|
||
};
|
||
}),
|
||
},
|
||
onError: {
|
||
target: "ready",
|
||
actions: assign(({ event }) => ({
|
||
errorMessage: toPaymentErrorMessage(event.error),
|
||
})),
|
||
},
|
||
},
|
||
},
|
||
|
||
ready: {
|
||
on: {
|
||
PaymentInit: "refreshingPlans",
|
||
PaymentPlanSelected: {
|
||
actions: assign(({ context, event }) => ({
|
||
...resetOrderState(),
|
||
selectedPlanId: event.planId,
|
||
autoRenew: defaultAutoRenewForPlan(event.planId, context.plans),
|
||
})),
|
||
},
|
||
PaymentPayChannelChanged: {
|
||
actions: assign(({ event }) => ({
|
||
...resetOrderState(),
|
||
payChannel: event.payChannel,
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
PaymentAutoRenewChanged: {
|
||
actions: assign(({ event }) => ({
|
||
autoRenew: event.autoRenew,
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
PaymentAgreementChanged: {
|
||
actions: assign(({ event }) => ({
|
||
agreed: event.agreed,
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
PaymentCreateOrderSubmitted: [
|
||
{
|
||
guard: ({ context }) =>
|
||
context.agreed && context.selectedPlanId.length > 0,
|
||
target: "creatingOrder",
|
||
},
|
||
{
|
||
actions: assign({
|
||
errorMessage:
|
||
"Choose a plan and accept the agreement before continuing.",
|
||
}),
|
||
},
|
||
],
|
||
PaymentErrorCleared: {
|
||
actions: assign({ errorMessage: null }),
|
||
},
|
||
},
|
||
},
|
||
|
||
creatingOrder: {
|
||
invoke: {
|
||
src: "createOrder",
|
||
input: ({ context }) => ({
|
||
planId: context.selectedPlanId,
|
||
payChannel: context.payChannel,
|
||
autoRenew: context.autoRenew,
|
||
}),
|
||
onDone: {
|
||
target: "pollingOrder",
|
||
actions: assign(({ context, event }) => ({
|
||
currentOrderId: event.output.orderId,
|
||
payParams: event.output.payParams,
|
||
orderStatus: "pending",
|
||
errorMessage: null,
|
||
launchNonce: context.launchNonce + 1,
|
||
})),
|
||
},
|
||
onError: {
|
||
target: "failed",
|
||
actions: assign(({ event }) => ({
|
||
errorMessage: toPaymentErrorMessage(event.error),
|
||
})),
|
||
},
|
||
},
|
||
},
|
||
|
||
pollingOrder: {
|
||
invoke: {
|
||
src: "pollOrderStatus",
|
||
input: ({ context }) => ({
|
||
orderId: context.currentOrderId ?? "",
|
||
}),
|
||
onDone: [
|
||
{
|
||
guard: ({ event }) => event.output.status === "paid",
|
||
target: "paid",
|
||
actions: assign(({ event }) => ({
|
||
orderStatus: event.output.status,
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
{
|
||
guard: ({ event }) => event.output.status === "failed",
|
||
target: "failed",
|
||
actions: assign(({ event }) => ({
|
||
orderStatus: event.output.status,
|
||
errorMessage: "Payment failed or was cancelled.",
|
||
})),
|
||
},
|
||
{
|
||
target: "waitingForPayment",
|
||
actions: assign(({ event }) => ({
|
||
orderStatus: event.output.status,
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
],
|
||
onError: {
|
||
target: "failed",
|
||
actions: assign(({ event }) => ({
|
||
errorMessage: toPaymentErrorMessage(event.error),
|
||
})),
|
||
},
|
||
},
|
||
on: {
|
||
PaymentReset: {
|
||
target: "ready",
|
||
actions: assign(resetOrderState),
|
||
},
|
||
},
|
||
},
|
||
|
||
waitingForPayment: {
|
||
after: {
|
||
[POLL_DELAY_MS]: "pollingOrder",
|
||
},
|
||
on: {
|
||
PaymentReset: {
|
||
target: "ready",
|
||
actions: assign(resetOrderState),
|
||
},
|
||
},
|
||
},
|
||
|
||
paid: {
|
||
on: {
|
||
PaymentPlanSelected: {
|
||
target: "ready",
|
||
actions: assign(({ context, event }) => ({
|
||
...resetOrderState(),
|
||
selectedPlanId: event.planId,
|
||
autoRenew: defaultAutoRenewForPlan(event.planId, context.plans),
|
||
})),
|
||
},
|
||
PaymentPayChannelChanged: {
|
||
target: "ready",
|
||
actions: assign(({ event }) => ({
|
||
...resetOrderState(),
|
||
payChannel: event.payChannel,
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
PaymentCreateOrderSubmitted: [
|
||
{
|
||
guard: ({ context }) =>
|
||
context.agreed && context.selectedPlanId.length > 0,
|
||
target: "creatingOrder",
|
||
actions: assign(resetOrderState),
|
||
},
|
||
{
|
||
target: "ready",
|
||
actions: assign({
|
||
errorMessage:
|
||
"Choose a plan and accept the agreement before continuing.",
|
||
}),
|
||
},
|
||
],
|
||
PaymentReset: {
|
||
target: "ready",
|
||
actions: assign(resetOrderState),
|
||
},
|
||
},
|
||
},
|
||
|
||
failed: {
|
||
on: {
|
||
PaymentCreateOrderSubmitted: [
|
||
{
|
||
guard: ({ context }) =>
|
||
context.agreed && context.selectedPlanId.length > 0,
|
||
target: "creatingOrder",
|
||
actions: assign(resetOrderState),
|
||
},
|
||
],
|
||
PaymentErrorCleared: {
|
||
target: "ready",
|
||
actions: assign({ errorMessage: null }),
|
||
},
|
||
PaymentReset: {
|
||
target: "ready",
|
||
actions: assign(resetOrderState),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
on: {
|
||
PaymentReturned: {
|
||
target: ".pollingOrder",
|
||
actions: assign(({ event }) => ({
|
||
currentOrderId: event.orderId,
|
||
payParams: null,
|
||
orderStatus: "pending",
|
||
errorMessage: null,
|
||
})),
|
||
},
|
||
PaymentLaunchFailed: {
|
||
target: ".failed",
|
||
actions: assign(({ event }) => ({
|
||
errorMessage: event.errorMessage,
|
||
})),
|
||
},
|
||
},
|
||
});
|
||
|
||
export type PaymentMachine = typeof paymentMachine;
|