Expose gateway billing flows from the synced server baseline
The preserved local work adds PopiNewAPI billing checkout, gateway account binding, and matching web console/pricing surfaces on top of the current test-server baseline. During the merge, the start-end video helpers from the deployed baseline were kept alongside the actionGenerate prompt handling from the WIP.
Constraint: Current usable baseline is 7054436, already deployed on the test server.
Rejected: Commit the WIP before syncing the baseline | would have hidden conflicts with the deployed start-end-frame changes.
Confidence: medium
Scope-risk: broad
Directive: Do not deploy this commit to the test server without rechecking gateway credentials and billing checkout behavior in that environment.
Tested: go test ./...
Tested: make build
Tested: cd web && npm run build
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import Link from "next/link";
|
||||
import { BillingPurchaseActions } from "@/components/billing-purchase-actions";
|
||||
import {
|
||||
getBillingCatalog,
|
||||
getBillingPointsPackages,
|
||||
getBillingSubscriptionPlans,
|
||||
getViewerSession,
|
||||
} from "@/lib/popiart-api";
|
||||
import { getDictionary, type Locale } from "@/lib/site-content";
|
||||
|
||||
export default async function PricingPage({
|
||||
@@ -7,7 +14,64 @@ export default async function PricingPage({
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const dictionary = getDictionary(locale as Locale);
|
||||
const typedLocale = locale as Locale;
|
||||
const dictionary = getDictionary(typedLocale);
|
||||
const session = await getViewerSession();
|
||||
const isZh = typedLocale === "zh";
|
||||
let plans = dictionary.pricing.plans;
|
||||
let subscriptionPlans: Awaited<ReturnType<typeof getBillingSubscriptionPlans>> = null;
|
||||
let pointsPackages: Awaited<ReturnType<typeof getBillingPointsPackages>> = null;
|
||||
let billingLiveError = "";
|
||||
const bindHint = isZh
|
||||
? "如需查看真实套餐与发起支付,请先在控制台绑定网关用户态。"
|
||||
: "Bind a gateway user in the console first to see live plans and start checkout.";
|
||||
const purchaseLabels = {
|
||||
alipay: isZh ? "支付宝支付" : "Pay with Alipay",
|
||||
wxpay: isZh ? "微信支付" : "Pay with WeChat",
|
||||
creating: isZh ? "创建中..." : "Creating...",
|
||||
tradeNo: isZh ? "交易号" : "Trade no",
|
||||
openLink: isZh ? "打开支付链接" : "Open payment link",
|
||||
codeUrl: isZh ? "扫码地址" : "Code URL",
|
||||
invalid: isZh ? "创建支付失败。" : "Failed to create payment.",
|
||||
pending: isZh ? "支付处理中" : "Payment pending",
|
||||
success: isZh ? "支付成功" : "Payment successful",
|
||||
failed: isZh ? "支付失败" : "Payment failed",
|
||||
paymentStatus: isZh ? "支付状态" : "Payment status",
|
||||
qrTitle: isZh ? "微信扫码支付" : "Scan with WeChat",
|
||||
openConsole: isZh ? "前往控制台" : "Open console",
|
||||
openBilling: isZh ? "查看账单中心" : "Open billing",
|
||||
};
|
||||
|
||||
try {
|
||||
const catalog = await getBillingCatalog();
|
||||
if (catalog?.plans?.length) {
|
||||
plans = catalog.plans.map((plan) => ({
|
||||
badge: plan.badge,
|
||||
name: plan.name,
|
||||
price: plan.price,
|
||||
cadence: plan.cadence,
|
||||
summary: plan.summary,
|
||||
features: plan.features,
|
||||
cta: plan.cta,
|
||||
highlight: plan.highlight,
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
// Keep the seeded dictionary fallback when the product billing catalog is unavailable.
|
||||
}
|
||||
|
||||
if (session?.gateway_bound) {
|
||||
try {
|
||||
const [subscriptionResult, pointsResult] = await Promise.all([
|
||||
getBillingSubscriptionPlans(),
|
||||
getBillingPointsPackages(),
|
||||
]);
|
||||
subscriptionPlans = subscriptionResult;
|
||||
pointsPackages = pointsResult;
|
||||
} catch (error) {
|
||||
billingLiveError = error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-stack">
|
||||
@@ -17,13 +81,52 @@ export default async function PricingPage({
|
||||
<h1>{dictionary.pricing.title}</h1>
|
||||
<p>{dictionary.pricing.subtitle}</p>
|
||||
</div>
|
||||
{session ? (
|
||||
<div className="status-banner">
|
||||
{session.gateway_bound ? (
|
||||
<span>{isZh ? "已绑定网关用户,可直接读取真实套餐并发起支付。" : "Gateway user bound. Live plans and checkout are available."}</span>
|
||||
) : (
|
||||
<span>{bindHint}</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{billingLiveError ? <div className="status-banner status-banner-error">{billingLiveError}</div> : null}
|
||||
</section>
|
||||
|
||||
<section className="pricing-grid">
|
||||
{dictionary.pricing.plans.map((plan) => (
|
||||
{(subscriptionPlans?.items.length ? subscriptionPlans.items.map((item) => ({
|
||||
key: String(item.plan.id),
|
||||
badge: item.plan.recommended ? (isZh ? "推荐方案" : "Recommended") : (isZh ? "订阅方案" : "Subscription"),
|
||||
name: item.plan.title,
|
||||
price: `${item.plan.currency} ${item.plan.price_amount}`,
|
||||
cadence: `${item.plan.duration_value} ${item.plan.duration_unit}`,
|
||||
summary: item.plan.description || item.plan.subtitle,
|
||||
features: [
|
||||
`${item.plan.points_amount} ${isZh ? "订阅赠送积分" : "subscription points"}`,
|
||||
`${item.plan.total_amount || 0} ${isZh ? "总额度" : "total quota"}`,
|
||||
`${isZh ? "会员等级" : "member level"}: ${item.plan.member_level || "-"}`,
|
||||
`${isZh ? "重置周期" : "reset period"}: ${item.plan.quota_reset_period || "-"}`,
|
||||
],
|
||||
cta: isZh ? "购买订阅" : "Buy subscription",
|
||||
highlight: item.plan.recommended,
|
||||
itemId: item.plan.id,
|
||||
kind: "subscription" as const,
|
||||
})) : plans.map((plan) => ({
|
||||
key: plan.name,
|
||||
badge: plan.badge,
|
||||
name: plan.name,
|
||||
price: plan.price,
|
||||
cadence: plan.cadence,
|
||||
summary: plan.summary,
|
||||
features: plan.features,
|
||||
cta: plan.cta,
|
||||
highlight: plan.highlight,
|
||||
itemId: 0,
|
||||
kind: "subscription" as const,
|
||||
}))).map((plan) => (
|
||||
<article
|
||||
className={`pricing-card ${plan.highlight ? "pricing-card-highlight" : ""}`}
|
||||
key={plan.name}
|
||||
key={plan.key}
|
||||
>
|
||||
<div className="pricing-top">
|
||||
<div>
|
||||
@@ -41,13 +144,47 @@ export default async function PricingPage({
|
||||
<li key={feature}>{feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
<Link className="button button-dark" href={`/${locale}/login`}>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
{session?.gateway_bound && plan.itemId > 0 ? (
|
||||
<BillingPurchaseActions itemId={plan.itemId} kind={plan.kind} labels={purchaseLabels} />
|
||||
) : (
|
||||
<Link className="button button-dark" href={`/${locale}/login`}>
|
||||
{plan.cta}
|
||||
</Link>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
{pointsPackages?.items.length ? (
|
||||
<section className="pricing-grid">
|
||||
{pointsPackages.items.map((pack) => (
|
||||
<article className="pricing-card" key={pack.id}>
|
||||
<div className="pricing-top">
|
||||
<div>
|
||||
<span className="card-kicker">{isZh ? "积分包" : "Points pack"}</span>
|
||||
<h2>{pack.name}</h2>
|
||||
</div>
|
||||
<div className="price-line">
|
||||
<strong>{pack.currency} {pack.price_amount}</strong>
|
||||
<span>{isZh ? "/包" : "/pack"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="pricing-summary">
|
||||
{isZh
|
||||
? `到账 ${pack.points_amount + pack.bonus_points} 积分(含赠送 ${pack.bonus_points})`
|
||||
: `${pack.points_amount + pack.bonus_points} total points (${pack.bonus_points} bonus)`}
|
||||
</p>
|
||||
<ul className="bullet-list">
|
||||
<li>{isZh ? `基础积分 ${pack.points_amount}` : `Base points ${pack.points_amount}`}</li>
|
||||
<li>{isZh ? `赠送积分 ${pack.bonus_points}` : `Bonus points ${pack.bonus_points}`}</li>
|
||||
<li>{isZh ? "购买成功后直接进入积分钱包" : "Delivered into your point wallets after payment"}</li>
|
||||
</ul>
|
||||
<BillingPurchaseActions itemId={pack.id} kind="points" labels={purchaseLabels} />
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className="section-panel">
|
||||
<div className="section-heading">
|
||||
<div className="eyebrow">{dictionary.pricing.faqTag}</div>
|
||||
|
||||
Reference in New Issue
Block a user