diff --git a/src/app/subscription/__tests__/subscription-screen.helpers.test.ts b/src/app/subscription/__tests__/subscription-screen.helpers.test.ts index f5d64127..cc7c4aa4 100644 --- a/src/app/subscription/__tests__/subscription-screen.helpers.test.ts +++ b/src/app/subscription/__tests__/subscription-screen.helpers.test.ts @@ -3,10 +3,12 @@ import { describe, expect, it } from "vitest"; import { PaymentPlan } from "@/data/dto/payment"; import { + canChooseSubscriptionPayChannel, findSelectedSubscriptionPlan, getDefaultPayChannelForCountryCode, getDefaultSubscriptionPlanId, getFirstRechargeOfferView, + resolveSubscriptionPayChannel, toCoinsOfferPlanViews, toVipOfferPlanViews, } from "../subscription-screen.helpers"; @@ -38,6 +40,34 @@ describe("subscription screen helpers", () => { expect(getDefaultPayChannelForCountryCode(null)).toBe("stripe"); }); + it("only allows payment method switching for Philippines users", () => { + expect(canChooseSubscriptionPayChannel("PH")).toBe(true); + expect(canChooseSubscriptionPayChannel("ph")).toBe(true); + expect(canChooseSubscriptionPayChannel("HK")).toBe(false); + expect(canChooseSubscriptionPayChannel(null)).toBe(false); + }); + + it("resolves subscription pay channel by country", () => { + expect( + resolveSubscriptionPayChannel({ + countryCode: "PH", + requestedPayChannel: null, + }), + ).toBe("ezpay"); + expect( + resolveSubscriptionPayChannel({ + countryCode: "PH", + requestedPayChannel: "stripe", + }), + ).toBe("stripe"); + expect( + resolveSubscriptionPayChannel({ + countryCode: "HK", + requestedPayChannel: "ezpay", + }), + ).toBe("stripe"); + }); + it("maps at most three VIP plans and all coin plans", () => { const plans = [ makePlan({ planId: "vip_monthly", planName: "Monthly" }), diff --git a/src/app/subscription/components/subscription-payment-method.tsx b/src/app/subscription/components/subscription-payment-method.tsx index 2a2d1b54..a714a9e1 100644 --- a/src/app/subscription/components/subscription-payment-method.tsx +++ b/src/app/subscription/components/subscription-payment-method.tsx @@ -21,12 +21,14 @@ const PAYMENT_METHODS: Array<{ export interface SubscriptionPaymentMethodProps { value: PayChannel; disabled?: boolean; + caption?: string; onChange: (channel: PayChannel) => void; } export function SubscriptionPaymentMethod({ value, disabled = false, + caption = "Stripe by default", onChange, }: SubscriptionPaymentMethodProps) { return ( @@ -35,7 +37,7 @@ export function SubscriptionPaymentMethod({

Payment Method

- Stripe by default + {caption}
{PAYMENT_METHODS.map((method) => { diff --git a/src/app/subscription/subscription-page-client.tsx b/src/app/subscription/subscription-page-client.tsx index 1ab8b71f..d6a4bf85 100644 --- a/src/app/subscription/subscription-page-client.tsx +++ b/src/app/subscription/subscription-page-client.tsx @@ -17,8 +17,9 @@ function toReturnTo(value: string | null): "chat" | null { return value === "chat" ? "chat" : null; } -function toPayChannel(value: string | null): PayChannel { - return value === "ezpay" ? "ezpay" : "stripe"; +function toPayChannel(value: string | null): PayChannel | null { + if (value === "ezpay" || value === "stripe") return value; + return null; } export function SubscriptionPageClient() { diff --git a/src/app/subscription/subscription-screen.helpers.ts b/src/app/subscription/subscription-screen.helpers.ts index 090f6130..289d268f 100644 --- a/src/app/subscription/subscription-screen.helpers.ts +++ b/src/app/subscription/subscription-screen.helpers.ts @@ -1,4 +1,5 @@ -import type { PaymentPlan } from "@/data/dto/payment"; +import type { PayChannel, PaymentPlan } from "@/data/dto/payment"; +import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel"; export { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel"; import type { CoinsOfferPlanView } from "./components/subscription-coins-offer-section"; @@ -12,6 +13,23 @@ export interface FirstRechargeOfferView { subtitle: string; } +export function canChooseSubscriptionPayChannel( + countryCode: string | null | undefined, +): boolean { + return countryCode?.trim().toUpperCase() === "PH"; +} + +export function resolveSubscriptionPayChannel(input: { + countryCode: string | null | undefined; + requestedPayChannel: PayChannel | null | undefined; +}): PayChannel { + if (!canChooseSubscriptionPayChannel(input.countryCode)) return "stripe"; + return ( + input.requestedPayChannel ?? + getDefaultPayChannelForCountryCode(input.countryCode) + ); +} + export function isVipPlan(plan: PaymentPlan): boolean { return plan.vipDays !== null; } diff --git a/src/app/subscription/subscription-screen.tsx b/src/app/subscription/subscription-screen.tsx index f39b3a66..0cc5f904 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -1,10 +1,11 @@ "use client"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { BackButton } from "@/app/_components"; import { Checkbox, MobileShell } from "@/app/_components/core"; import { AppConstants } from "@/core/app_constants"; import type { PayChannel } from "@/data/dto/payment"; +import { useUserState } from "@/stores/user/user-context"; import { SubscriptionCheckoutButton, @@ -16,8 +17,10 @@ import { import styles from "./components/subscription-screen.module.css"; import { findSelectedSubscriptionPlan, + canChooseSubscriptionPayChannel, getFirstRechargeOfferView, getDefaultSubscriptionPlanId, + resolveSubscriptionPayChannel, toCoinsOfferPlanViews, toVipOfferPlanViews, type SubscriptionType, @@ -30,15 +33,24 @@ export interface SubscriptionScreenProps { subscriptionType?: SubscriptionType; shouldResumePendingOrder?: boolean; returnTo?: "chat" | null; - initialPayChannel?: PayChannel; + initialPayChannel?: PayChannel | null; } export function SubscriptionScreen({ subscriptionType = "vip", shouldResumePendingOrder = false, returnTo = null, - initialPayChannel = "stripe", + initialPayChannel = null, }: SubscriptionScreenProps) { + const userState = useUserState(); + const countryCode = userState.currentUser?.countryCode; + const canChoosePaymentMethod = + canChooseSubscriptionPayChannel(countryCode); + const resolvedInitialPayChannel = resolveSubscriptionPayChannel({ + countryCode, + requestedPayChannel: initialPayChannel, + }); + const phDefaultPayChannelAppliedRef = useRef(false); const { payment, paymentDispatch, @@ -49,7 +61,7 @@ export function SubscriptionScreen({ subscriptionType, shouldResumePendingOrder, returnTo, - initialPayChannel, + initialPayChannel: resolvedInitialPayChannel, }); const canSubscribeVip = subscriptionType === "vip"; @@ -89,6 +101,36 @@ export function SubscriptionScreen({ const canActivate = selectedPlan !== null && payment.agreed && !isPaymentBusy; + useEffect(() => { + if (isPaymentBusy) return; + + if (!canChoosePaymentMethod) { + if (payment.payChannel === "stripe") return; + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel: "stripe", + }); + return; + } + + if (initialPayChannel !== null) return; + if (phDefaultPayChannelAppliedRef.current) return; + phDefaultPayChannelAppliedRef.current = true; + + if (payment.payChannel === resolvedInitialPayChannel) return; + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel: resolvedInitialPayChannel, + }); + }, [ + canChoosePaymentMethod, + initialPayChannel, + isPaymentBusy, + payment.payChannel, + paymentDispatch, + resolvedInitialPayChannel, + ]); + useEffect(() => { const defaultPlanId = getDefaultSubscriptionPlanId({ canSubscribeVip, @@ -171,18 +213,21 @@ export function SubscriptionScreen({ />
-
- { - paymentDispatch({ - type: "PaymentPayChannelChanged", - payChannel, - }); - }} - /> -
+ {canChoosePaymentMethod ? ( +
+ { + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel, + }); + }} + /> +
+ ) : null}