feat(payment): connect payment service flow

This commit is contained in:
2026-06-18 15:40:59 +08:00
parent 35c30ac31e
commit a347b39001
34 changed files with 759 additions and 889 deletions
+96
View File
@@ -0,0 +1,96 @@
"use client";
/**
* PaymentContext:基于 XState v5 的 React Context Provider
*/
import {
type Dispatch,
type ReactNode,
createContext,
useContext,
useMemo,
} from "react";
import { useMachine } from "@xstate/react";
import { paymentMachine } from "./payment-machine";
import type {
PaymentEvent,
PaymentState as MachineContext,
} from "./payment-machine";
export interface PaymentContextState {
status: string;
plans: MachineContext["plans"];
selectedPlanId: string;
payChannel: MachineContext["payChannel"];
autoRenew: boolean;
agreed: boolean;
currentOrderId: string | null;
payParams: Record<string, unknown> | null;
orderStatus: MachineContext["orderStatus"];
vipStatus: MachineContext["vipStatus"];
errorMessage: string | null;
launchNonce: number;
isLoadingPlans: boolean;
isCreatingOrder: boolean;
isPollingOrder: boolean;
isCheckingVipStatus: boolean;
isPaid: boolean;
}
const PaymentStateCtx = createContext<PaymentContextState | null>(null);
const PaymentDispatchCtx = createContext<Dispatch<PaymentEvent> | null>(null);
export interface PaymentProviderProps {
children: ReactNode;
}
export function PaymentProvider({ children }: PaymentProviderProps) {
const [state, send] = useMachine(paymentMachine);
const paymentState = useMemo<PaymentContextState>(
() => ({
status: String(state.value),
plans: state.context.plans,
selectedPlanId: state.context.selectedPlanId,
payChannel: state.context.payChannel,
autoRenew: state.context.autoRenew,
agreed: state.context.agreed,
currentOrderId: state.context.currentOrderId,
payParams: state.context.payParams,
orderStatus: state.context.orderStatus,
vipStatus: state.context.vipStatus,
errorMessage: state.context.errorMessage,
launchNonce: state.context.launchNonce,
isLoadingPlans: state.matches("loadingPlans"),
isCreatingOrder: state.matches("creatingOrder"),
isPollingOrder:
state.matches("pollingOrder") || state.matches("waitingForPayment"),
isCheckingVipStatus: state.matches("checkingVipStatus"),
isPaid: state.matches("paid"),
}),
[state],
);
return (
<PaymentStateCtx.Provider value={paymentState}>
<PaymentDispatchCtx.Provider value={send}>
{children}
</PaymentDispatchCtx.Provider>
</PaymentStateCtx.Provider>
);
}
export function usePaymentState(): PaymentContextState {
const ctx = useContext(PaymentStateCtx);
if (!ctx)
throw new Error("usePaymentState must be used inside <PaymentProvider>");
return ctx;
}
export function usePaymentDispatch(): Dispatch<PaymentEvent> {
const ctx = useContext(PaymentDispatchCtx);
if (!ctx)
throw new Error("usePaymentDispatch must be used inside <PaymentProvider>");
return ctx;
}