perf(payment): parse route params on the server

This commit is contained in:
2026-07-14 17:48:20 +08:00
parent f58f47e0a5
commit e270be9bd9
4 changed files with 74 additions and 107 deletions
+45 -11
View File
@@ -1,19 +1,53 @@
import { Suspense } from "react";
import type { PayChannel } from "@/data/dto/payment";
import {
PAYMENT_ANALYTICS_ENTRY_PARAM,
PAYMENT_ANALYTICS_REASON_PARAM,
parsePaymentAnalyticsContext,
} from "@/lib/analytics/payment_analytics_context";
import { PageLoadingFallback } from "@/app/_components/core";
import {
SubscriptionScreen,
type SubscriptionType,
} from "./subscription-screen";
import { SubscriptionPageClient } from "./subscription-page-client";
type SubscriptionSearchParams = Record<string, string | string[] | undefined>;
export default async function SubscriptionPage({
searchParams,
}: {
searchParams: Promise<SubscriptionSearchParams>;
}) {
const query = await searchParams;
const subscriptionType = toSubscriptionType(firstSearchParam(query.type));
const analyticsContext = parsePaymentAnalyticsContext({
entryPoint: firstSearchParam(query[PAYMENT_ANALYTICS_ENTRY_PARAM]),
triggerReason: firstSearchParam(query[PAYMENT_ANALYTICS_REASON_PARAM]),
subscriptionType,
});
export default function SubscriptionPage() {
return (
<Suspense fallback={<SubscriptionFallback />}>
<SubscriptionPageClient />
</Suspense>
<SubscriptionScreen
subscriptionType={subscriptionType}
shouldResumePendingOrder={firstSearchParam(query.paymentReturn) === "1"}
returnTo={toReturnTo(firstSearchParam(query.returnTo))}
initialPayChannel={toPayChannel(firstSearchParam(query.payChannel))}
analyticsContext={analyticsContext}
/>
);
}
function SubscriptionFallback() {
return (
<PageLoadingFallback>Loading subscription...</PageLoadingFallback>
);
function firstSearchParam(value: string | string[] | undefined): string | null {
return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);
}
function toSubscriptionType(value: string | null): SubscriptionType {
return value === "topup" ? "topup" : "vip";
}
function toReturnTo(value: string | null): "chat" | null {
return value === "chat" ? "chat" : null;
}
function toPayChannel(value: string | null): PayChannel | null {
return value === "ezpay" || value === "stripe" ? value : null;
}
@@ -1,51 +0,0 @@
"use client";
import { useSearchParams } from "next/navigation";
import type { PayChannel } from "@/data/dto/payment";
import {
PAYMENT_ANALYTICS_ENTRY_PARAM,
PAYMENT_ANALYTICS_REASON_PARAM,
parsePaymentAnalyticsContext,
} from "@/lib/analytics";
import {
SubscriptionScreen,
type SubscriptionType,
} from "./subscription-screen";
function toSubscriptionType(value: string | null): SubscriptionType {
return value === "topup" ? "topup" : "vip";
}
function toReturnTo(value: string | null): "chat" | null {
return value === "chat" ? "chat" : null;
}
function toPayChannel(value: string | null): PayChannel | null {
if (value === "ezpay" || value === "stripe") return value;
return null;
}
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"));
const analyticsContext = parsePaymentAnalyticsContext({
entryPoint: searchParams.get(PAYMENT_ANALYTICS_ENTRY_PARAM),
triggerReason: searchParams.get(PAYMENT_ANALYTICS_REASON_PARAM),
subscriptionType,
});
return (
<SubscriptionScreen
subscriptionType={subscriptionType}
shouldResumePendingOrder={shouldResumePendingOrder}
returnTo={returnTo}
initialPayChannel={initialPayChannel}
analyticsContext={analyticsContext}
/>
);
}
+29 -13
View File
@@ -1,21 +1,37 @@
import { Suspense } from "react";
import type { PayChannel } from "@/data/dto/payment";
import {
DEFAULT_TIP_COFFEE_TYPE,
resolveTipCoffeeType,
TIP_COFFEE_TYPE_PARAM,
} from "@/lib/tip/tip_coffee";
import { PageLoadingFallback } from "@/app/_components/core";
import { TipScreen } from "./tip-screen";
import { TipPageClient } from "./tip-page-client";
type TipSearchParams = Record<string, string | string[] | undefined>;
export default async function TipPage({
searchParams,
}: {
searchParams: Promise<TipSearchParams>;
}) {
const query = await searchParams;
const coffeeType =
resolveTipCoffeeType(firstSearchParam(query[TIP_COFFEE_TYPE_PARAM])) ??
DEFAULT_TIP_COFFEE_TYPE;
export default function TipPage() {
return (
<Suspense fallback={<TipFallback />}>
<TipPageClient />
</Suspense>
<TipScreen
coffeeType={coffeeType}
shouldResumePendingOrder={firstSearchParam(query.paymentReturn) === "1"}
initialPayChannel={toPayChannel(firstSearchParam(query.payChannel))}
/>
);
}
function TipFallback() {
return (
<PageLoadingFallback background="#fff6ed" tone="warm">
Loading tip...
</PageLoadingFallback>
);
function firstSearchParam(value: string | string[] | undefined): string | null {
return Array.isArray(value) ? (value[0] ?? null) : (value ?? null);
}
function toPayChannel(value: string | null): PayChannel | null {
return value === "ezpay" || value === "stripe" ? value : null;
}
-32
View File
@@ -1,32 +0,0 @@
"use client";
import { useSearchParams } from "next/navigation";
import type { PayChannel } from "@/data/dto/payment";
import {
DEFAULT_TIP_COFFEE_TYPE,
resolveTipCoffeeType,
TIP_COFFEE_TYPE_PARAM,
} from "@/lib/tip/tip_coffee";
import { TipScreen } from "./tip-screen";
function toPayChannel(value: string | null): PayChannel | null {
if (value === "ezpay" || value === "stripe") return value;
return null;
}
export function TipPageClient() {
const searchParams = useSearchParams();
const coffeeType =
resolveTipCoffeeType(searchParams.get(TIP_COFFEE_TYPE_PARAM)) ??
DEFAULT_TIP_COFFEE_TYPE;
return (
<TipScreen
coffeeType={coffeeType}
shouldResumePendingOrder={searchParams.get("paymentReturn") === "1"}
initialPayChannel={toPayChannel(searchParams.get("payChannel"))}
/>
);
}