/** * 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;