From e254c930784f75e06627bf6895221885f8ec68b6 Mon Sep 17 00:00:00 2001 From: chenhang Date: Tue, 30 Jun 2026 16:49:47 +0800 Subject: [PATCH] feat(subscription): default pay channel by country --- .../subscription-screen.helpers.test.ts | 9 +++++ .../subscription-screen.helpers.ts | 8 ++++- src/app/subscription/subscription-screen.tsx | 35 ++++++++++++++++--- src/data/dto/user/__tests__/user.test.ts | 4 +++ src/data/dto/user/user.ts | 2 ++ src/data/dto/user/user_view.ts | 2 ++ src/data/schemas/user/user.ts | 2 ++ .../user/__tests__/user-machine.test.ts | 2 ++ src/stores/user/user-machine.helpers.ts | 4 +++ 9 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/app/subscription/__tests__/subscription-screen.helpers.test.ts b/src/app/subscription/__tests__/subscription-screen.helpers.test.ts index 90aaf000..6a6290b5 100644 --- a/src/app/subscription/__tests__/subscription-screen.helpers.test.ts +++ b/src/app/subscription/__tests__/subscription-screen.helpers.test.ts @@ -4,6 +4,7 @@ import { PaymentPlan } from "@/data/dto/payment"; import { findSelectedSubscriptionPlan, + getDefaultPayChannelForCountryCode, getDefaultSubscriptionPlanId, toCoinsOfferPlanViews, toVipOfferPlanViews, @@ -28,6 +29,14 @@ function makePlan( } describe("subscription screen helpers", () => { + it("defaults Philippines users to Ezpay and other countries to Stripe", () => { + expect(getDefaultPayChannelForCountryCode("PH")).toBe("ezpay"); + expect(getDefaultPayChannelForCountryCode("ph")).toBe("ezpay"); + expect(getDefaultPayChannelForCountryCode("HK")).toBe("stripe"); + expect(getDefaultPayChannelForCountryCode("")).toBe("stripe"); + expect(getDefaultPayChannelForCountryCode(null)).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/subscription-screen.helpers.ts b/src/app/subscription/subscription-screen.helpers.ts index 1d9935a5..00c20bca 100644 --- a/src/app/subscription/subscription-screen.helpers.ts +++ b/src/app/subscription/subscription-screen.helpers.ts @@ -1,10 +1,16 @@ -import type { PaymentPlan } from "@/data/dto/payment"; +import type { PayChannel, PaymentPlan } from "@/data/dto/payment"; import type { CoinsOfferPlanView } from "./components/subscription-coins-offer-section"; import type { VipOfferPlanView } from "./components/subscription-vip-offer-section"; export type SubscriptionType = "vip" | "topup"; +export function getDefaultPayChannelForCountryCode( + countryCode: string | null | undefined, +): PayChannel { + return countryCode?.trim().toUpperCase() === "PH" ? "ezpay" : "stripe"; +} + 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 98996bc4..f20a07b9 100644 --- a/src/app/subscription/subscription-screen.tsx +++ b/src/app/subscription/subscription-screen.tsx @@ -1,9 +1,10 @@ "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 { useUserState } from "@/stores/user/user-context"; import { SubscriptionCheckoutButton, @@ -15,6 +16,7 @@ import { import styles from "./components/subscription-screen.module.css"; import { findSelectedSubscriptionPlan, + getDefaultPayChannelForCountryCode, getDefaultSubscriptionPlanId, toCoinsOfferPlanViews, toVipOfferPlanViews, @@ -46,6 +48,8 @@ export function SubscriptionScreen({ shouldResumePendingOrder, returnTo, }); + const user = useUserState(); + const paymentMethodManuallySelectedRef = useRef(false); const canSubscribeVip = subscriptionType === "vip"; const vipOfferPlans = useMemo( @@ -96,6 +100,28 @@ export function SubscriptionScreen({ vipOfferPlans, ]); + useEffect(() => { + if (payment.status !== "ready") return; + if (paymentMethodManuallySelectedRef.current) return; + + const countryCode = user.currentUser?.countryCode ?? ""; + if (countryCode.trim().length === 0) return; + + const defaultPayChannel = + getDefaultPayChannelForCountryCode(countryCode); + if (payment.payChannel === defaultPayChannel) return; + + paymentDispatch({ + type: "PaymentPayChannelChanged", + payChannel: defaultPayChannel, + }); + }, [ + payment.payChannel, + payment.status, + paymentDispatch, + user.currentUser?.countryCode, + ]); + return (
@@ -136,12 +162,13 @@ export function SubscriptionScreen({ + onChange={(payChannel) => { + paymentMethodManuallySelectedRef.current = true; paymentDispatch({ type: "PaymentPayChannelChanged", payChannel, - }) - } + }); + }} /> diff --git a/src/data/dto/user/__tests__/user.test.ts b/src/data/dto/user/__tests__/user.test.ts index 28079915..bfba5049 100644 --- a/src/data/dto/user/__tests__/user.test.ts +++ b/src/data/dto/user/__tests__/user.test.ts @@ -10,6 +10,8 @@ describe("User", () => { username: "guest_bbc90a38", email: "device_bbc90a38b996187348f54cf2cb0b789e@guest.local", platform: "web", + country: "Hong Kong", + countryCode: "HK", intimacy: 0, dolBalance: 0, personalityTraits: { @@ -29,6 +31,8 @@ describe("User", () => { username: "guest_bbc90a38", email: "device_bbc90a38b996187348f54cf2cb0b789e@guest.local", platform: "web", + country: "Hong Kong", + countryCode: "HK", intimacy: 0, dolBalance: 0, personalityTraits: { diff --git a/src/data/dto/user/user.ts b/src/data/dto/user/user.ts index 9938d74f..e01f2f37 100644 --- a/src/data/dto/user/user.ts +++ b/src/data/dto/user/user.ts @@ -13,6 +13,8 @@ export class User { declare readonly username: string; declare readonly email: string; declare readonly platform: string; + declare readonly country: string; + declare readonly countryCode: string; declare readonly intimacy: number; declare readonly dolBalance: number; declare readonly personalityTraits: PersonalityTraits; diff --git a/src/data/dto/user/user_view.ts b/src/data/dto/user/user_view.ts index 8e01c26e..59b92568 100644 --- a/src/data/dto/user/user_view.ts +++ b/src/data/dto/user/user_view.ts @@ -9,6 +9,8 @@ export const UserViewSchema = z.object({ id: z.string(), username: z.string(), email: z.string(), + country: z.string(), + countryCode: z.string(), avatarUrl: z.string(), intimacy: z.number(), dolBalance: z.number(), diff --git a/src/data/schemas/user/user.ts b/src/data/schemas/user/user.ts index f5910d0c..81abd764 100644 --- a/src/data/schemas/user/user.ts +++ b/src/data/schemas/user/user.ts @@ -14,6 +14,8 @@ export const UserSchema = z.object({ username: stringOrEmpty, email: stringOrEmpty, platform: stringOrEmpty, + country: stringOrEmpty, + countryCode: stringOrEmpty, intimacy: numberOrZero, dolBalance: numberOrZero, personalityTraits: PersonalityTraitsSchema.default(PERSONALITY_TRAITS_DEFAULTS), diff --git a/src/stores/user/__tests__/user-machine.test.ts b/src/stores/user/__tests__/user-machine.test.ts index 79af0332..865a7142 100644 --- a/src/stores/user/__tests__/user-machine.test.ts +++ b/src/stores/user/__tests__/user-machine.test.ts @@ -13,6 +13,8 @@ function makeUser(overrides: Partial = {}): UserView { id: "user-1", username: "Elio Fan", email: "user@example.com", + country: "Hong Kong", + countryCode: "HK", avatarUrl: "https://example.com/avatar.png", intimacy: 42, dolBalance: 7, diff --git a/src/stores/user/user-machine.helpers.ts b/src/stores/user/user-machine.helpers.ts index 7e9b7fb6..62338048 100644 --- a/src/stores/user/user-machine.helpers.ts +++ b/src/stores/user/user-machine.helpers.ts @@ -57,6 +57,8 @@ export function toView(u: { id: string; username: string; email?: string; + country?: string; + countryCode?: string; avatarUrl?: string; intimacy?: number; dolBalance?: number; @@ -74,6 +76,8 @@ export function toView(u: { id: u.id, username: u.username, email: u.email ?? "", + country: u.country ?? "", + countryCode: u.countryCode ?? "", avatarUrl: u.avatarUrl ?? "", intimacy: u.intimacy ?? 0, dolBalance: u.dolBalance ?? creditBalance,