fix(subscription): preselect pay channel before navigation
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { LoginStatus } from "@/data/dto/auth";
|
import type { LoginStatus } from "@/data/dto/auth";
|
||||||
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
import { ROUTE_BUILDERS } from "@/router/routes";
|
import { ROUTE_BUILDERS } from "@/router/routes";
|
||||||
import { AppEnvUtil, BrowserDetector } from "@/utils";
|
import { AppEnvUtil, BrowserDetector } from "@/utils";
|
||||||
|
|
||||||
@@ -30,14 +31,20 @@ export function shouldStartExternalBrowserPrompt({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChatPaywallSubscriptionUrl(): string {
|
export function getChatPaywallSubscriptionUrl(
|
||||||
|
payChannel: PayChannel = "stripe",
|
||||||
|
): string {
|
||||||
return ROUTE_BUILDERS.subscription("vip", {
|
return ROUTE_BUILDERS.subscription("vip", {
|
||||||
|
payChannel,
|
||||||
returnTo: "chat",
|
returnTo: "chat",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChatCreditsTopUpSubscriptionUrl(): string {
|
export function getChatCreditsTopUpSubscriptionUrl(
|
||||||
|
payChannel: PayChannel = "stripe",
|
||||||
|
): string {
|
||||||
return ROUTE_BUILDERS.subscription("topup", {
|
return ROUTE_BUILDERS.subscription("topup", {
|
||||||
|
payChannel,
|
||||||
returnTo: "chat",
|
returnTo: "chat",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -51,11 +58,12 @@ export function getInsufficientCreditsSubscriptionType(
|
|||||||
export function getChatPaywallNavigationUrl(
|
export function getChatPaywallNavigationUrl(
|
||||||
loginStatus: LoginStatus,
|
loginStatus: LoginStatus,
|
||||||
type: "vip" | "topup" = "vip",
|
type: "vip" | "topup" = "vip",
|
||||||
|
payChannel: PayChannel = "stripe",
|
||||||
): string {
|
): string {
|
||||||
const subscriptionUrl =
|
const subscriptionUrl =
|
||||||
type === "topup"
|
type === "topup"
|
||||||
? getChatCreditsTopUpSubscriptionUrl()
|
? getChatCreditsTopUpSubscriptionUrl(payChannel)
|
||||||
: getChatPaywallSubscriptionUrl();
|
: getChatPaywallSubscriptionUrl(payChannel);
|
||||||
if (deriveIsGuest(loginStatus)) {
|
if (deriveIsGuest(loginStatus)) {
|
||||||
return ROUTE_BUILDERS.authWithRedirect(subscriptionUrl);
|
return ROUTE_BUILDERS.authWithRedirect(subscriptionUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
recordExternalBrowserPromptShown,
|
recordExternalBrowserPromptShown,
|
||||||
resolveExternalBrowserPromptEligibility,
|
resolveExternalBrowserPromptEligibility,
|
||||||
} from "@/lib/chat/chat_external_browser";
|
} from "@/lib/chat/chat_external_browser";
|
||||||
|
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
|
|
||||||
@@ -115,10 +116,14 @@ export function ChatScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMessageLimitUnlock(): void {
|
function handleMessageLimitUnlock(): void {
|
||||||
|
const defaultPayChannel = getDefaultPayChannelForCountryCode(
|
||||||
|
userState.currentUser?.countryCode,
|
||||||
|
);
|
||||||
router.push(
|
router.push(
|
||||||
getChatPaywallNavigationUrl(
|
getChatPaywallNavigationUrl(
|
||||||
authState.loginStatus,
|
authState.loginStatus,
|
||||||
getInsufficientCreditsSubscriptionType(userState.isVip),
|
getInsufficientCreditsSubscriptionType(userState.isVip),
|
||||||
|
defaultPayChannel,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
type PendingChatUnlock,
|
type PendingChatUnlock,
|
||||||
type PendingChatUnlockKind,
|
type PendingChatUnlockKind,
|
||||||
} from "@/lib/navigation/chat_unlock_session";
|
} from "@/lib/navigation/chat_unlock_session";
|
||||||
|
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||||
|
|
||||||
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
|
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
|
||||||
|
|
||||||
@@ -135,10 +136,13 @@ export function useChatUnlockNavigationFlow({
|
|||||||
stage: "payment",
|
stage: "payment",
|
||||||
});
|
});
|
||||||
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
|
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
|
||||||
|
const defaultPayChannel = getDefaultPayChannelForCountryCode(
|
||||||
|
userState.currentUser?.countryCode,
|
||||||
|
);
|
||||||
router.push(
|
router.push(
|
||||||
ROUTE_BUILDERS.subscription(
|
ROUTE_BUILDERS.subscription(
|
||||||
getInsufficientCreditsSubscriptionType(userState.isVip),
|
getInsufficientCreditsSubscriptionType(userState.isVip),
|
||||||
{ returnTo: "chat" },
|
{ payChannel: defaultPayChannel, returnTo: "chat" },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { useRouter } from "next/navigation";
|
|||||||
|
|
||||||
import { BackButton } from "@/app/_components";
|
import { BackButton } from "@/app/_components";
|
||||||
import { MobileShell } from "@/app/_components/core";
|
import { MobileShell } from "@/app/_components/core";
|
||||||
import { ROUTES } from "@/router/routes";
|
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||||
|
import { ROUTE_BUILDERS, ROUTES } from "@/router/routes";
|
||||||
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
|
||||||
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||||
|
|
||||||
@@ -20,8 +21,6 @@ import {
|
|||||||
import styles from "./components/sidebar-screen.module.css";
|
import styles from "./components/sidebar-screen.module.css";
|
||||||
|
|
||||||
const FALLBACK_USERNAME = "User name";
|
const FALLBACK_USERNAME = "User name";
|
||||||
const VIP_SUBSCRIPTION_URL = `${ROUTES.subscription}?type=vip`;
|
|
||||||
const TOP_UP_SUBSCRIPTION_URL = `${ROUTES.subscription}?type=topup`;
|
|
||||||
|
|
||||||
export function SidebarScreen() {
|
export function SidebarScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -61,6 +60,15 @@ export function SidebarScreen() {
|
|||||||
|
|
||||||
const name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
const name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
||||||
const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null;
|
const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null;
|
||||||
|
const defaultPayChannel = getDefaultPayChannelForCountryCode(
|
||||||
|
user.currentUser?.countryCode,
|
||||||
|
);
|
||||||
|
const vipSubscriptionUrl = ROUTE_BUILDERS.subscription("vip", {
|
||||||
|
payChannel: defaultPayChannel,
|
||||||
|
});
|
||||||
|
const topUpSubscriptionUrl = ROUTE_BUILDERS.subscription("topup", {
|
||||||
|
payChannel: defaultPayChannel,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell background="#fcf3f4">
|
<MobileShell background="#fcf3f4">
|
||||||
@@ -88,9 +96,9 @@ export function SidebarScreen() {
|
|||||||
onActivateVip={
|
onActivateVip={
|
||||||
sidebarState === "vip"
|
sidebarState === "vip"
|
||||||
? undefined
|
? undefined
|
||||||
: () => router.push(VIP_SUBSCRIPTION_URL)
|
: () => router.push(vipSubscriptionUrl)
|
||||||
}
|
}
|
||||||
onTopUp={() => router.push(TOP_UP_SUBSCRIPTION_URL)}
|
onTopUp={() => router.push(topUpSubscriptionUrl)}
|
||||||
onRulesClick={() => router.push(ROUTES.coinsRules)}
|
onRulesClick={() => router.push(ROUTES.coinsRules)}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ describe("subscription screen helpers", () => {
|
|||||||
{
|
{
|
||||||
id: "coin_1000",
|
id: "coin_1000",
|
||||||
coins: 1000,
|
coins: 1000,
|
||||||
price: "9.90",
|
price: "9.9",
|
||||||
currency: "US$",
|
currency: "US$",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -98,7 +98,7 @@ describe("subscription screen helpers", () => {
|
|||||||
).toMatchObject([
|
).toMatchObject([
|
||||||
{
|
{
|
||||||
id: "vip_test",
|
id: "vip_test",
|
||||||
price: "77.90",
|
price: "77.9",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@@ -138,7 +138,7 @@ describe("subscription screen helpers", () => {
|
|||||||
{
|
{
|
||||||
id: "coin_1000",
|
id: "coin_1000",
|
||||||
coins: 1000,
|
coins: 1000,
|
||||||
price: "9.90",
|
price: "9.9",
|
||||||
currency: "US$",
|
currency: "US$",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -156,7 +156,7 @@ describe("subscription screen helpers", () => {
|
|||||||
selectedPlan: {
|
selectedPlan: {
|
||||||
id: "coin_1000",
|
id: "coin_1000",
|
||||||
coins: 1000,
|
coins: 1000,
|
||||||
price: "9.90",
|
price: "9.9",
|
||||||
currency: "US$",
|
currency: "US$",
|
||||||
},
|
},
|
||||||
vipPlans: [],
|
vipPlans: [],
|
||||||
@@ -164,7 +164,7 @@ describe("subscription screen helpers", () => {
|
|||||||
{
|
{
|
||||||
id: "coin_1000",
|
id: "coin_1000",
|
||||||
coins: 1000,
|
coins: 1000,
|
||||||
price: "9.90",
|
price: "9.9",
|
||||||
currency: "US$",
|
currency: "US$",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SubscriptionScreen,
|
SubscriptionScreen,
|
||||||
type SubscriptionType,
|
type SubscriptionType,
|
||||||
@@ -15,17 +17,23 @@ function toReturnTo(value: string | null): "chat" | null {
|
|||||||
return value === "chat" ? "chat" : null;
|
return value === "chat" ? "chat" : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toPayChannel(value: string | null): PayChannel {
|
||||||
|
return value === "ezpay" ? "ezpay" : "stripe";
|
||||||
|
}
|
||||||
|
|
||||||
export function SubscriptionPageClient() {
|
export function SubscriptionPageClient() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const subscriptionType = toSubscriptionType(searchParams.get("type"));
|
const subscriptionType = toSubscriptionType(searchParams.get("type"));
|
||||||
const shouldResumePendingOrder = searchParams.get("paymentReturn") === "1";
|
const shouldResumePendingOrder = searchParams.get("paymentReturn") === "1";
|
||||||
const returnTo = toReturnTo(searchParams.get("returnTo"));
|
const returnTo = toReturnTo(searchParams.get("returnTo"));
|
||||||
|
const initialPayChannel = toPayChannel(searchParams.get("payChannel"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubscriptionScreen
|
<SubscriptionScreen
|
||||||
subscriptionType={subscriptionType}
|
subscriptionType={subscriptionType}
|
||||||
shouldResumePendingOrder={shouldResumePendingOrder}
|
shouldResumePendingOrder={shouldResumePendingOrder}
|
||||||
returnTo={returnTo}
|
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 { CoinsOfferPlanView } from "./components/subscription-coins-offer-section";
|
||||||
import type { VipOfferPlanView } from "./components/subscription-vip-offer-section";
|
import type { VipOfferPlanView } from "./components/subscription-vip-offer-section";
|
||||||
|
|
||||||
export type SubscriptionType = "vip" | "topup";
|
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 {
|
export function isVipPlan(plan: PaymentPlan): boolean {
|
||||||
return plan.vipDays !== null;
|
return plan.vipDays !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useMemo, useRef } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { BackButton } from "@/app/_components";
|
import { BackButton } from "@/app/_components";
|
||||||
import { Checkbox, MobileShell } from "@/app/_components/core";
|
import { Checkbox, MobileShell } from "@/app/_components/core";
|
||||||
import { AppConstants } from "@/core/app_constants";
|
import { AppConstants } from "@/core/app_constants";
|
||||||
import { useUserState } from "@/stores/user/user-context";
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SubscriptionCheckoutButton,
|
SubscriptionCheckoutButton,
|
||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import styles from "./components/subscription-screen.module.css";
|
import styles from "./components/subscription-screen.module.css";
|
||||||
import {
|
import {
|
||||||
findSelectedSubscriptionPlan,
|
findSelectedSubscriptionPlan,
|
||||||
getDefaultPayChannelForCountryCode,
|
|
||||||
getDefaultSubscriptionPlanId,
|
getDefaultSubscriptionPlanId,
|
||||||
toCoinsOfferPlanViews,
|
toCoinsOfferPlanViews,
|
||||||
toVipOfferPlanViews,
|
toVipOfferPlanViews,
|
||||||
@@ -30,12 +29,14 @@ export interface SubscriptionScreenProps {
|
|||||||
subscriptionType?: SubscriptionType;
|
subscriptionType?: SubscriptionType;
|
||||||
shouldResumePendingOrder?: boolean;
|
shouldResumePendingOrder?: boolean;
|
||||||
returnTo?: "chat" | null;
|
returnTo?: "chat" | null;
|
||||||
|
initialPayChannel?: PayChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SubscriptionScreen({
|
export function SubscriptionScreen({
|
||||||
subscriptionType = "vip",
|
subscriptionType = "vip",
|
||||||
shouldResumePendingOrder = false,
|
shouldResumePendingOrder = false,
|
||||||
returnTo = null,
|
returnTo = null,
|
||||||
|
initialPayChannel = "stripe",
|
||||||
}: SubscriptionScreenProps) {
|
}: SubscriptionScreenProps) {
|
||||||
const {
|
const {
|
||||||
payment,
|
payment,
|
||||||
@@ -47,9 +48,8 @@ export function SubscriptionScreen({
|
|||||||
subscriptionType,
|
subscriptionType,
|
||||||
shouldResumePendingOrder,
|
shouldResumePendingOrder,
|
||||||
returnTo,
|
returnTo,
|
||||||
|
initialPayChannel,
|
||||||
});
|
});
|
||||||
const user = useUserState();
|
|
||||||
const paymentMethodManuallySelectedRef = useRef(false);
|
|
||||||
const canSubscribeVip = subscriptionType === "vip";
|
const canSubscribeVip = subscriptionType === "vip";
|
||||||
|
|
||||||
const vipOfferPlans = useMemo(
|
const vipOfferPlans = useMemo(
|
||||||
@@ -100,28 +100,6 @@ export function SubscriptionScreen({
|
|||||||
vipOfferPlans,
|
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 (
|
return (
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
<div className={styles.shell}>
|
<div className={styles.shell}>
|
||||||
@@ -163,7 +141,6 @@ export function SubscriptionScreen({
|
|||||||
value={payment.payChannel}
|
value={payment.payChannel}
|
||||||
disabled={isPaymentBusy}
|
disabled={isPaymentBusy}
|
||||||
onChange={(payChannel) => {
|
onChange={(payChannel) => {
|
||||||
paymentMethodManuallySelectedRef.current = true;
|
|
||||||
paymentDispatch({
|
paymentDispatch({
|
||||||
type: "PaymentPayChannelChanged",
|
type: "PaymentPayChannelChanged",
|
||||||
payChannel,
|
payChannel,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
usePaymentState,
|
usePaymentState,
|
||||||
} from "@/stores/payment/payment-context";
|
} from "@/stores/payment/payment-context";
|
||||||
import { useUserDispatch } from "@/stores/user/user-context";
|
import { useUserDispatch } from "@/stores/user/user-context";
|
||||||
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
|
|
||||||
import type { SubscriptionType } from "./subscription-screen.helpers";
|
import type { SubscriptionType } from "./subscription-screen.helpers";
|
||||||
|
|
||||||
@@ -23,12 +24,14 @@ export interface UseSubscriptionPaymentFlowInput {
|
|||||||
subscriptionType: SubscriptionType;
|
subscriptionType: SubscriptionType;
|
||||||
shouldResumePendingOrder: boolean;
|
shouldResumePendingOrder: boolean;
|
||||||
returnTo: "chat" | null;
|
returnTo: "chat" | null;
|
||||||
|
initialPayChannel: PayChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubscriptionPaymentFlow({
|
export function useSubscriptionPaymentFlow({
|
||||||
subscriptionType,
|
subscriptionType,
|
||||||
shouldResumePendingOrder,
|
shouldResumePendingOrder,
|
||||||
returnTo,
|
returnTo,
|
||||||
|
initialPayChannel,
|
||||||
}: UseSubscriptionPaymentFlowInput) {
|
}: UseSubscriptionPaymentFlowInput) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userDispatch = useUserDispatch();
|
const userDispatch = useUserDispatch();
|
||||||
@@ -37,14 +40,37 @@ export function useSubscriptionPaymentFlow({
|
|||||||
const refreshedPaidOrderRef = useRef<string | null>(null);
|
const refreshedPaidOrderRef = useRef<string | null>(null);
|
||||||
const resumedPendingOrderRef = useRef<string | null>(null);
|
const resumedPendingOrderRef = useRef<string | null>(null);
|
||||||
const successDialogShownOrderRef = useRef<string | null>(null);
|
const successDialogShownOrderRef = useRef<string | null>(null);
|
||||||
|
const initialPayChannelAppliedRef = useRef(false);
|
||||||
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
|
const [showPaymentSuccessDialog, setShowPaymentSuccessDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (payment.status === "idle") {
|
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(() => {
|
useEffect(() => {
|
||||||
if (!payment.isPaid || !payment.currentOrderId) return;
|
if (!payment.isPaid || !payment.currentOrderId) return;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { buildPendingPaymentSubscriptionUrl } from "../pending_payment_order";
|
||||||
|
|
||||||
|
describe("pending payment order helpers", () => {
|
||||||
|
it("keeps the pay channel when rebuilding subscription return urls", () => {
|
||||||
|
expect(
|
||||||
|
buildPendingPaymentSubscriptionUrl({
|
||||||
|
payChannel: "ezpay",
|
||||||
|
returnTo: "chat",
|
||||||
|
subscriptionType: "topup",
|
||||||
|
}),
|
||||||
|
).toBe("/subscription?type=topup&payChannel=ezpay&paymentReturn=1&returnTo=chat");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
|
|
||||||
|
export function getDefaultPayChannelForCountryCode(
|
||||||
|
countryCode: string | null | undefined,
|
||||||
|
): PayChannel {
|
||||||
|
return countryCode?.trim().toUpperCase() === "PH" ? "ezpay" : "stripe";
|
||||||
|
}
|
||||||
@@ -43,10 +43,14 @@ export function clearPendingPaymentOrder(): Promise<Result<void>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildPendingPaymentSubscriptionUrl(
|
export function buildPendingPaymentSubscriptionUrl(
|
||||||
order: Pick<PendingPaymentOrder, "subscriptionType" | "returnTo">,
|
order: Pick<
|
||||||
|
PendingPaymentOrder,
|
||||||
|
"payChannel" | "returnTo" | "subscriptionType"
|
||||||
|
>,
|
||||||
): string {
|
): string {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
type: order.subscriptionType,
|
type: order.subscriptionType,
|
||||||
|
payChannel: order.payChannel,
|
||||||
paymentReturn: "1",
|
paymentReturn: "1",
|
||||||
});
|
});
|
||||||
if (order.returnTo) params.set("returnTo", order.returnTo);
|
if (order.returnTo) params.set("returnTo", order.returnTo);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
* error / not-found / page / sidebar-screen / splash-screen)使用 —— 不能整体注释。
|
* error / not-found / page / sidebar-screen / splash-screen)使用 —— 不能整体注释。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
|
|
||||||
/** 静态路由字面量 */
|
/** 静态路由字面量 */
|
||||||
export const ROUTES = {
|
export const ROUTES = {
|
||||||
root: "/",
|
root: "/",
|
||||||
@@ -35,9 +37,10 @@ export const ROUTE_BUILDERS = {
|
|||||||
`/chat/image/${encodeURIComponent(messageId)}` as const,
|
`/chat/image/${encodeURIComponent(messageId)}` as const,
|
||||||
subscription: (
|
subscription: (
|
||||||
type: "vip" | "topup",
|
type: "vip" | "topup",
|
||||||
options: { returnTo?: "chat" } = {},
|
options: { payChannel?: PayChannel; returnTo?: "chat" } = {},
|
||||||
): `/subscription?${string}` => {
|
): `/subscription?${string}` => {
|
||||||
const params = new URLSearchParams({ type });
|
const params = new URLSearchParams({ type });
|
||||||
|
if (options.payChannel) params.set("payChannel", options.payChannel);
|
||||||
if (options.returnTo) params.set("returnTo", options.returnTo);
|
if (options.returnTo) params.set("returnTo", options.returnTo);
|
||||||
return `${ROUTES.subscription}?${params.toString()}` as const;
|
return `${ROUTES.subscription}?${params.toString()}` as const;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import type { PayChannel } from "@/data/dto/payment";
|
import type { PayChannel } from "@/data/dto/payment";
|
||||||
|
|
||||||
export type PaymentEvent =
|
export type PaymentEvent =
|
||||||
| { type: "PaymentInit" }
|
| { type: "PaymentInit"; payChannel?: PayChannel }
|
||||||
| { type: "PaymentPlanSelected"; planId: string }
|
| { type: "PaymentPlanSelected"; planId: string }
|
||||||
| { type: "PaymentPayChannelChanged"; payChannel: PayChannel }
|
| { type: "PaymentPayChannelChanged"; payChannel: PayChannel }
|
||||||
| { type: "PaymentAutoRenewChanged"; autoRenew: boolean }
|
| { type: "PaymentAutoRenewChanged"; autoRenew: boolean }
|
||||||
|
|||||||
@@ -46,7 +46,12 @@ export const paymentMachine = setup({
|
|||||||
states: {
|
states: {
|
||||||
idle: {
|
idle: {
|
||||||
on: {
|
on: {
|
||||||
PaymentInit: "loadingCachedPlans",
|
PaymentInit: {
|
||||||
|
target: "loadingCachedPlans",
|
||||||
|
actions: assign(({ event }) =>
|
||||||
|
event.payChannel ? { payChannel: event.payChannel } : {},
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user