fix(subscription): preselect pay channel before navigation
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { LoginStatus } from "@/data/dto/auth";
|
||||
import type { PayChannel } from "@/data/dto/payment";
|
||||
import { ROUTE_BUILDERS } from "@/router/routes";
|
||||
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", {
|
||||
payChannel,
|
||||
returnTo: "chat",
|
||||
});
|
||||
}
|
||||
|
||||
export function getChatCreditsTopUpSubscriptionUrl(): string {
|
||||
export function getChatCreditsTopUpSubscriptionUrl(
|
||||
payChannel: PayChannel = "stripe",
|
||||
): string {
|
||||
return ROUTE_BUILDERS.subscription("topup", {
|
||||
payChannel,
|
||||
returnTo: "chat",
|
||||
});
|
||||
}
|
||||
@@ -51,11 +58,12 @@ export function getInsufficientCreditsSubscriptionType(
|
||||
export function getChatPaywallNavigationUrl(
|
||||
loginStatus: LoginStatus,
|
||||
type: "vip" | "topup" = "vip",
|
||||
payChannel: PayChannel = "stripe",
|
||||
): string {
|
||||
const subscriptionUrl =
|
||||
type === "topup"
|
||||
? getChatCreditsTopUpSubscriptionUrl()
|
||||
: getChatPaywallSubscriptionUrl();
|
||||
? getChatCreditsTopUpSubscriptionUrl(payChannel)
|
||||
: getChatPaywallSubscriptionUrl(payChannel);
|
||||
if (deriveIsGuest(loginStatus)) {
|
||||
return ROUTE_BUILDERS.authWithRedirect(subscriptionUrl);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
recordExternalBrowserPromptShown,
|
||||
resolveExternalBrowserPromptEligibility,
|
||||
} from "@/lib/chat/chat_external_browser";
|
||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
|
||||
@@ -115,10 +116,14 @@ export function ChatScreen() {
|
||||
}
|
||||
|
||||
function handleMessageLimitUnlock(): void {
|
||||
const defaultPayChannel = getDefaultPayChannelForCountryCode(
|
||||
userState.currentUser?.countryCode,
|
||||
);
|
||||
router.push(
|
||||
getChatPaywallNavigationUrl(
|
||||
authState.loginStatus,
|
||||
getInsufficientCreditsSubscriptionType(userState.isVip),
|
||||
defaultPayChannel,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type PendingChatUnlock,
|
||||
type PendingChatUnlockKind,
|
||||
} from "@/lib/navigation/chat_unlock_session";
|
||||
import { getDefaultPayChannelForCountryCode } from "@/lib/payment/default_pay_channel";
|
||||
|
||||
import { getInsufficientCreditsSubscriptionType } from "../chat-screen.helpers";
|
||||
|
||||
@@ -135,10 +136,13 @@ export function useChatUnlockNavigationFlow({
|
||||
stage: "payment",
|
||||
});
|
||||
chatDispatch({ type: "ChatUnlockPaywallNavigationConsumed" });
|
||||
const defaultPayChannel = getDefaultPayChannelForCountryCode(
|
||||
userState.currentUser?.countryCode,
|
||||
);
|
||||
router.push(
|
||||
ROUTE_BUILDERS.subscription(
|
||||
getInsufficientCreditsSubscriptionType(userState.isVip),
|
||||
{ returnTo: "chat" },
|
||||
{ payChannel: defaultPayChannel, returnTo: "chat" },
|
||||
),
|
||||
);
|
||||
})();
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import { BackButton } from "@/app/_components";
|
||||
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 { useUserDispatch, useUserState } from "@/stores/user/user-context";
|
||||
|
||||
@@ -20,8 +21,6 @@ import {
|
||||
import styles from "./components/sidebar-screen.module.css";
|
||||
|
||||
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() {
|
||||
const router = useRouter();
|
||||
@@ -61,6 +60,15 @@ export function SidebarScreen() {
|
||||
|
||||
const name = user.currentUser?.username ?? FALLBACK_USERNAME;
|
||||
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 (
|
||||
<MobileShell background="#fcf3f4">
|
||||
@@ -88,9 +96,9 @@ export function SidebarScreen() {
|
||||
onActivateVip={
|
||||
sidebarState === "vip"
|
||||
? 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)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user