diff --git a/src/app/subscription/components/subscription-checkout-button.tsx b/src/app/subscription/components/subscription-checkout-button.tsx index 574c2917..90444f36 100644 --- a/src/app/subscription/components/subscription-checkout-button.tsx +++ b/src/app/subscription/components/subscription-checkout-button.tsx @@ -96,8 +96,7 @@ export function SubscriptionCheckoutButton({ const isLoading = payment.isCreatingOrder || - payment.isPollingOrder || - payment.isCheckingVipStatus; + payment.isPollingOrder; const label = payment.isPollingOrder ? "Processing payment..." : payment.isCreatingOrder diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index f9ce5979..8eabcad5 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -57,11 +57,8 @@ export function SubscriptionScreen({ const refreshedPaidOrderRef = useRef(null); const resumedPendingOrderRef = useRef(null); const successDialogShownOrderRef = useRef(null); - const syncedVipOrderRef = useRef(null); const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] = useState(false); - const [pendingChatReturnAfterVipSync, setPendingChatReturnAfterVipSync] = - useState(false); const isVipSubscription = subscriptionType === "vip"; useEffect(() => { @@ -77,35 +74,6 @@ export function SubscriptionScreen({ userDispatch({ type: "UserFetch" }); }, [payment.currentOrderId, payment.isPaid, userDispatch]); - useEffect(() => { - const currentUser = user.currentUser; - const canMarkVipSynced = - subscriptionType === "vip" && - payment.isPaid && - payment.currentOrderId && - payment.vipStatus?.isVip === true && - currentUser && - currentUser.isVip !== true; - if (!canMarkVipSynced) return; - if (syncedVipOrderRef.current === payment.currentOrderId) return; - - syncedVipOrderRef.current = payment.currentOrderId; - userDispatch({ - type: "UserUpdate", - user: { - ...currentUser, - isVip: true, - }, - }); - }, [ - payment.currentOrderId, - payment.isPaid, - payment.vipStatus?.isVip, - subscriptionType, - user.currentUser, - userDispatch, - ]); - useEffect(() => { if (!payment.isPaid || !payment.currentOrderId) return; if (successDialogShownOrderRef.current === payment.currentOrderId) return; @@ -200,8 +168,7 @@ export function SubscriptionScreen({ : plans.find((plan) => plan.id === payment.selectedPlanId)) ?? null; const isPaymentBusy = payment.isCreatingOrder || - payment.isPollingOrder || - payment.isCheckingVipStatus; + payment.isPollingOrder; const canActivate = selectedPlan !== null && payment.agreed && !isPaymentBusy; @@ -217,7 +184,6 @@ export function SubscriptionScreen({ : "Choose a subscription plan"; const finishPaymentSuccessClose = () => { - setPendingChatReturnAfterVipSync(false); setShowPaymentSuccessDialog(false); paymentDispatch({ type: "PaymentReset" }); if (returnTo === "chat") { @@ -226,44 +192,9 @@ export function SubscriptionScreen({ }; const handlePaymentSuccessClose = () => { - const shouldWaitForVipSync = - returnTo === "chat" && - subscriptionType === "vip" && - user.currentUser?.isVip !== true; - - if (shouldWaitForVipSync) { - setPendingChatReturnAfterVipSync(true); - if (payment.vipStatus?.isVip === true && user.currentUser) { - userDispatch({ - type: "UserUpdate", - user: { - ...user.currentUser, - isVip: true, - }, - }); - } else { - userDispatch({ type: "UserFetch" }); - } - return; - } - finishPaymentSuccessClose(); }; - useEffect(() => { - if (!pendingChatReturnAfterVipSync) return; - if (subscriptionType === "vip" && user.currentUser?.isVip !== true) return; - - const timer = window.setTimeout(() => { - finishPaymentSuccessClose(); - }, 0); - - return () => window.clearTimeout(timer); - // `finishPaymentSuccessClose` intentionally stays local to this component; - // this effect is gated by pendingChatReturnAfterVipSync to avoid repeated closes. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pendingChatReturnAfterVipSync, subscriptionType, user.currentUser?.isVip]); - useEffect(() => { if (isVipSubscription) { const firstVipPlanId = vipOfferPlans[0]?.id ?? ""; diff --git a/src/stores/payment/__tests__/payment-machine.test.ts b/src/stores/payment/__tests__/payment-machine.test.ts index c944ca21..3603f85a 100644 --- a/src/stores/payment/__tests__/payment-machine.test.ts +++ b/src/stores/payment/__tests__/payment-machine.test.ts @@ -6,7 +6,6 @@ import { type PayChannel, PaymentOrderStatusResponse, PaymentPlansResponse, - PaymentVipStatusResponse, } from "@/data/dto/payment"; import { paymentMachine } from "@/stores/payment/payment-machine"; @@ -94,12 +93,6 @@ function createTestPaymentMachine( planId: "vip_monthly", }), ), - loadVipStatus: fromPromise(async () => - PaymentVipStatusResponse.from({ - isVip: true, - vipExpiresAt: "2026-07-25T00:00:00.000Z", - }), - ), }, }); } @@ -163,7 +156,6 @@ describe("paymentMachine", () => { expect(context.currentOrderId).toBe("pay_test_001"); expect(context.orderStatus).toBe("paid"); expect(context.launchNonce).toBe(1); - expect(context.vipStatus?.isVip).toBe(true); actor.stop(); }); diff --git a/src/stores/payment/payment-context.tsx b/src/stores/payment/payment-context.tsx index eaeb0d2e..2e9ff714 100644 --- a/src/stores/payment/payment-context.tsx +++ b/src/stores/payment/payment-context.tsx @@ -28,13 +28,11 @@ export interface PaymentContextState { currentOrderId: string | null; payParams: Record | null; orderStatus: MachineContext["orderStatus"]; - vipStatus: MachineContext["vipStatus"]; errorMessage: string | null; launchNonce: number; isLoadingPlans: boolean; isCreatingOrder: boolean; isPollingOrder: boolean; - isCheckingVipStatus: boolean; isPaid: boolean; } @@ -59,14 +57,12 @@ export function PaymentProvider({ children }: PaymentProviderProps) { 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], diff --git a/src/stores/payment/payment-events.ts b/src/stores/payment/payment-events.ts index 91ce5e03..7633ea65 100644 --- a/src/stores/payment/payment-events.ts +++ b/src/stores/payment/payment-events.ts @@ -11,7 +11,6 @@ export type PaymentEvent = | { type: "PaymentAgreementChanged"; agreed: boolean } | { type: "PaymentCreateOrderSubmitted" } | { type: "PaymentReturned"; orderId: string } - | { type: "PaymentRefreshVipStatus" } | { type: "PaymentLaunchFailed"; errorMessage: string } | { type: "PaymentErrorCleared" } | { type: "PaymentReset" }; diff --git a/src/stores/payment/payment-machine.actors.ts b/src/stores/payment/payment-machine.actors.ts index 9dc0282d..437ae15c 100644 --- a/src/stores/payment/payment-machine.actors.ts +++ b/src/stores/payment/payment-machine.actors.ts @@ -8,7 +8,6 @@ import { PayChannel, PaymentOrderStatusResponse, PaymentPlansResponse, - PaymentVipStatusResponse, } from "@/data/dto/payment"; import { paymentRepository } from "@/data/repositories/payment_repository"; import type { IPaymentRepository } from "@/data/repositories/interfaces"; @@ -45,10 +44,3 @@ export const pollPaymentOrderStatusActor = fromPromise< if (Result.isErr(result)) throw result.error; return result.data; }); - -export const loadPaymentVipStatusActor = - fromPromise(async () => { - const result = await paymentRepo.getVipStatus(); - if (Result.isErr(result)) throw result.error; - return result.data; - }); diff --git a/src/stores/payment/payment-machine.helpers.ts b/src/stores/payment/payment-machine.helpers.ts new file mode 100644 index 00000000..9007db6f --- /dev/null +++ b/src/stores/payment/payment-machine.helpers.ts @@ -0,0 +1,34 @@ +import type { PaymentPlan } from "@/data/dto/payment"; + +import type { PaymentState } from "./payment-state"; + +export function toPaymentErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export function defaultAutoRenewForPlan(planId: string): boolean { + return ( + !planId.includes("lifetime") && + !planId.startsWith("dol_") && + !planId.startsWith("points_") && + !planId.startsWith("credit_") + ); +} + +export function getDefaultPlanId(plans: readonly PaymentPlan[]): string { + return ( + plans.find((plan) => plan.planId === "vip_monthly")?.planId ?? + plans.find((plan) => plan.orderType.startsWith("vip_"))?.planId ?? + plans[0]?.planId ?? + "" + ); +} + +export function resetOrderState(): Partial { + return { + currentOrderId: null, + payParams: null, + orderStatus: null, + errorMessage: null, + }; +} diff --git a/src/stores/payment/payment-machine.ts b/src/stores/payment/payment-machine.ts index 89835b89..a9964a73 100644 --- a/src/stores/payment/payment-machine.ts +++ b/src/stores/payment/payment-machine.ts @@ -3,16 +3,19 @@ */ import { assign, setup } from "xstate"; -import type { PaymentPlan } from "@/data/dto/payment"; - import type { PaymentEvent } from "./payment-events"; import { initialState, type PaymentState } from "./payment-state"; import { createPaymentOrderActor, loadPaymentPlansActor, - loadPaymentVipStatusActor, pollPaymentOrderStatusActor, } 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"; @@ -20,37 +23,6 @@ export { initialState } from "./payment-state"; const POLL_DELAY_MS = 4000; -function toErrorMessage(error: unknown): string { - return error instanceof Error ? error.message : String(error); -} - -function defaultAutoRenewForPlan(planId: string): boolean { - return ( - !planId.includes("lifetime") && - !planId.startsWith("dol_") && - !planId.startsWith("points_") && - !planId.startsWith("credit_") - ); -} - -function getDefaultPlanId(plans: readonly PaymentPlan[]): string { - return ( - plans.find((plan) => plan.planId === "vip_monthly")?.planId ?? - plans.find((plan) => plan.orderType.startsWith("vip_"))?.planId ?? - plans[0]?.planId ?? - "" - ); -} - -function resetOrderState() { - return { - currentOrderId: null, - payParams: null, - orderStatus: null, - errorMessage: null, - } satisfies Partial; -} - export const paymentMachine = setup({ types: { context: {} as PaymentState, @@ -60,7 +32,6 @@ export const paymentMachine = setup({ loadPlans: loadPaymentPlansActor, createOrder: createPaymentOrderActor, pollOrderStatus: pollPaymentOrderStatusActor, - loadVipStatus: loadPaymentVipStatusActor, }, }).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 */ @@ -71,7 +42,6 @@ export const paymentMachine = setup({ idle: { on: { PaymentInit: "loadingPlans", - PaymentRefreshVipStatus: "checkingVipStatus", }, }, @@ -95,7 +65,7 @@ export const paymentMachine = setup({ onError: { target: "ready", actions: assign(({ event }) => ({ - errorMessage: toErrorMessage(event.error), + errorMessage: toPaymentErrorMessage(event.error), })), }, }, @@ -143,7 +113,6 @@ export const paymentMachine = setup({ }), }, ], - PaymentRefreshVipStatus: "checkingVipStatus", PaymentErrorCleared: { actions: assign({ errorMessage: null }), }, @@ -171,7 +140,7 @@ export const paymentMachine = setup({ onError: { target: "failed", actions: assign(({ event }) => ({ - errorMessage: toErrorMessage(event.error), + errorMessage: toPaymentErrorMessage(event.error), })), }, }, @@ -186,7 +155,7 @@ export const paymentMachine = setup({ onDone: [ { guard: ({ event }) => event.output.status === "paid", - target: "checkingVipStatus", + target: "paid", actions: assign(({ event }) => ({ orderStatus: event.output.status, errorMessage: null, @@ -211,7 +180,7 @@ export const paymentMachine = setup({ onError: { target: "failed", actions: assign(({ event }) => ({ - errorMessage: toErrorMessage(event.error), + errorMessage: toPaymentErrorMessage(event.error), })), }, }, @@ -235,44 +204,6 @@ export const paymentMachine = setup({ }, }, - checkingVipStatus: { - invoke: { - src: "loadVipStatus", - onDone: [ - { - guard: ({ context }) => context.orderStatus === "paid", - target: "paid", - actions: assign(({ event }) => ({ - vipStatus: event.output, - errorMessage: null, - })), - }, - { - target: "ready", - actions: assign(({ event }) => ({ - vipStatus: event.output, - errorMessage: null, - })), - }, - ], - onError: [ - { - guard: ({ context }) => context.orderStatus === "paid", - target: "paid", - actions: assign(({ event }) => ({ - errorMessage: toErrorMessage(event.error), - })), - }, - { - target: "ready", - actions: assign(({ event }) => ({ - errorMessage: toErrorMessage(event.error), - })), - }, - ], - }, - }, - paid: { on: { PaymentPlanSelected: { @@ -310,7 +241,6 @@ export const paymentMachine = setup({ target: "ready", actions: assign(resetOrderState), }, - PaymentRefreshVipStatus: "checkingVipStatus", }, }, diff --git a/src/stores/payment/payment-state.ts b/src/stores/payment/payment-state.ts index 0aba2068..aa139327 100644 --- a/src/stores/payment/payment-state.ts +++ b/src/stores/payment/payment-state.ts @@ -5,7 +5,6 @@ import type { PayChannel, PaymentOrderStatus, PaymentPlan, - PaymentVipStatusResponse, } from "@/data/dto/payment"; export interface PaymentState { @@ -17,7 +16,6 @@ export interface PaymentState { currentOrderId: string | null; payParams: Record | null; orderStatus: PaymentOrderStatus | null; - vipStatus: PaymentVipStatusResponse | null; errorMessage: string | null; launchNonce: number; } @@ -31,7 +29,6 @@ export const initialState: PaymentState = { currentOrderId: null, payParams: null, orderStatus: null, - vipStatus: null, errorMessage: null, launchNonce: 0, };