fix(subscription): preselect pay channel before navigation

This commit is contained in:
2026-07-02 11:38:47 +08:00
parent 35c8de917d
commit cd25fa35f2
15 changed files with 121 additions and 56 deletions
@@ -62,7 +62,7 @@ describe("subscription screen helpers", () => {
{
id: "coin_1000",
coins: 1000,
price: "9.90",
price: "9.9",
currency: "US$",
},
]);
@@ -98,7 +98,7 @@ describe("subscription screen helpers", () => {
).toMatchObject([
{
id: "vip_test",
price: "77.90",
price: "77.9",
},
]);
});
@@ -138,7 +138,7 @@ describe("subscription screen helpers", () => {
{
id: "coin_1000",
coins: 1000,
price: "9.90",
price: "9.9",
currency: "US$",
},
],
@@ -156,7 +156,7 @@ describe("subscription screen helpers", () => {
selectedPlan: {
id: "coin_1000",
coins: 1000,
price: "9.90",
price: "9.9",
currency: "US$",
},
vipPlans: [],
@@ -164,7 +164,7 @@ describe("subscription screen helpers", () => {
{
id: "coin_1000",
coins: 1000,
price: "9.90",
price: "9.9",
currency: "US$",
},
],
@@ -2,6 +2,8 @@
import { useSearchParams } from "next/navigation";
import type { PayChannel } from "@/data/dto/payment";
import {
SubscriptionScreen,
type SubscriptionType,
@@ -15,17 +17,23 @@ function toReturnTo(value: string | null): "chat" | null {
return value === "chat" ? "chat" : null;
}
function toPayChannel(value: string | null): PayChannel {
return value === "ezpay" ? "ezpay" : "stripe";
}
export function SubscriptionPageClient() {
const searchParams = useSearchParams();
const subscriptionType = toSubscriptionType(searchParams.get("type"));
const shouldResumePendingOrder = searchParams.get("paymentReturn") === "1";
const returnTo = toReturnTo(searchParams.get("returnTo"));
const initialPayChannel = toPayChannel(searchParams.get("payChannel"));
return (
<SubscriptionScreen
subscriptionType={subscriptionType}
shouldResumePendingOrder={shouldResumePendingOrder}
returnTo={returnTo}
initialPayChannel={initialPayChannel}
/>
);
}
@@ -1,16 +1,11 @@
import type { PayChannel, PaymentPlan } from "@/data/dto/payment";
import type { PaymentPlan } from "@/data/dto/payment";
export { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
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;
}
+5 -28
View File
@@ -1,10 +1,10 @@
"use client";
import { useEffect, useMemo, useRef } from "react";
import { useEffect, useMemo } 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 type { PayChannel } from "@/data/dto/payment";
import {
SubscriptionCheckoutButton,
@@ -16,7 +16,6 @@ import {
import styles from "./components/subscription-screen.module.css";
import {
findSelectedSubscriptionPlan,
getDefaultPayChannelForCountryCode,
getDefaultSubscriptionPlanId,
toCoinsOfferPlanViews,
toVipOfferPlanViews,
@@ -30,12 +29,14 @@ export interface SubscriptionScreenProps {
subscriptionType?: SubscriptionType;
shouldResumePendingOrder?: boolean;
returnTo?: "chat" | null;
initialPayChannel?: PayChannel;
}
export function SubscriptionScreen({
subscriptionType = "vip",
shouldResumePendingOrder = false,
returnTo = null,
initialPayChannel = "stripe",
}: SubscriptionScreenProps) {
const {
payment,
@@ -47,9 +48,8 @@ export function SubscriptionScreen({
subscriptionType,
shouldResumePendingOrder,
returnTo,
initialPayChannel,
});
const user = useUserState();
const paymentMethodManuallySelectedRef = useRef(false);
const canSubscribeVip = subscriptionType === "vip";
const vipOfferPlans = useMemo(
@@ -100,28 +100,6 @@ 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 (
<MobileShell>
<div className={styles.shell}>
@@ -163,7 +141,6 @@ export function SubscriptionScreen({
value={payment.payChannel}
disabled={isPaymentBusy}
onChange={(payChannel) => {
paymentMethodManuallySelectedRef.current = true;
paymentDispatch({
type: "PaymentPayChannelChanged",
payChannel,
@@ -16,6 +16,7 @@ import {
usePaymentState,
} from "@/stores/payment/payment-context";
import { useUserDispatch } from "@/stores/user/user-context";
import type { PayChannel } from "@/data/dto/payment";
import type { SubscriptionType } from "./subscription-screen.helpers";
@@ -23,12 +24,14 @@ export interface UseSubscriptionPaymentFlowInput {
subscriptionType: SubscriptionType;
shouldResumePendingOrder: boolean;
returnTo: "chat" | null;
initialPayChannel: PayChannel;
}
export function useSubscriptionPaymentFlow({
subscriptionType,
shouldResumePendingOrder,
returnTo,
initialPayChannel,
}: UseSubscriptionPaymentFlowInput) {
const router = useRouter();
const userDispatch = useUserDispatch();
@@ -37,14 +40,37 @@ export function useSubscriptionPaymentFlow({
const refreshedPaidOrderRef = useRef<string | null>(null);
const resumedPendingOrderRef = useRef<string | null>(null);
const successDialogShownOrderRef = useRef<string | null>(null);
const initialPayChannelAppliedRef = useRef(false);
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
useState(false);
useEffect(() => {
if (payment.status === "idle") {
paymentDispatch({ type: "PaymentInit" });
initialPayChannelAppliedRef.current = true;
paymentDispatch({
type: "PaymentInit",
payChannel: initialPayChannel,
});
return;
}
}, [payment.status, paymentDispatch]);
if (
!initialPayChannelAppliedRef.current &&
payment.status === "ready"
) {
initialPayChannelAppliedRef.current = true;
if (payment.payChannel === initialPayChannel) return;
paymentDispatch({
type: "PaymentPayChannelChanged",
payChannel: initialPayChannel,
});
}
}, [
initialPayChannel,
payment.payChannel,
payment.status,
paymentDispatch,
]);
useEffect(() => {
if (!payment.isPaid || !payment.currentOrderId) return;