Files
2026-06-14 06:24:21 +08:00

469 lines
15 KiB
TypeScript

"use client";
import Link from "next/link";
import {
ArrowRight,
BellRing,
CheckCircle2,
Coins,
Crown,
ExternalLink,
Loader2,
Lock,
MessageSquare,
Radar,
Send,
Sparkles,
TrendingUp,
Wallet,
X,
Zap,
} from "lucide-react";
import s from "./UnlockProOverlay.module.css";
const DEFAULT_FAQ_HREF = "/subscription-help";
const DEFAULT_TELEGRAM_GROUP_URL = "";
export type UnlockProBilling = {
pointsEnabled: boolean;
isEligible: boolean;
pointsUsed: number;
discountAmount: number;
finalPrice: number;
maxDiscountUsd: number;
pointsPerUsd: number;
};
type UnlockProOverlayProps = {
points: number;
planPriceUsd: number;
planLabel?: string;
periodLabel?: string;
usePoints: boolean;
billing: UnlockProBilling;
onToggleUsePoints: () => void;
onPay: () => void;
onManualPay?: () => void;
onClose?: () => void;
payBusy?: boolean;
payLabel?: string;
manualPayLabel?: string;
locale?: "zh-CN" | "en-US";
errorText?: string;
infoText?: string;
faqHref?: string;
telegramGroupUrl?: string;
txHash?: string;
chainId?: number;
paymentTokenLabel?: string;
};
function formatUsdcAmount(value: number) {
if (!Number.isFinite(value)) return "0";
return value.toFixed(2).replace(/\.?0+$/, "");
}
const FEATURES = {
"zh-CN": [
"Terminal:市场温度与城市机会扫描",
"机场 / 跑道实测:METAR + 结算源优先",
"DEB 路径与高温时段复核",
"Telegram 提醒与账户权益同步",
],
"en-US": [
"Terminal: market temperature and city signal scanning",
"Airport / runway observations: METAR + settlement-source-first",
"DEB paths and peak-temperature window review",
"Telegram alerts and entitlement sync",
],
};
export function UnlockProOverlay({
points,
planPriceUsd,
planLabel,
periodLabel,
usePoints,
billing,
onToggleUsePoints,
onPay,
onManualPay,
onClose,
payBusy = false,
payLabel,
manualPayLabel,
locale = "zh-CN",
errorText,
infoText,
faqHref = DEFAULT_FAQ_HREF,
telegramGroupUrl = DEFAULT_TELEGRAM_GROUP_URL,
txHash,
chainId = 137,
paymentTokenLabel,
}: UnlockProOverlayProps) {
const isEn = locale === "en-US";
const canUsePoints = billing.pointsEnabled && billing.isEligible;
const featureList = FEATURES[locale] ?? FEATURES["zh-CN"];
const currencyLabel = "USDC";
const formattedPlanPrice = formatUsdcAmount(planPriceUsd);
const formattedFinalPrice = formatUsdcAmount(billing.finalPrice);
const formattedDiscount = formatUsdcAmount(billing.discountAmount);
const formattedMaxDiscount = formatUsdcAmount(billing.maxDiscountUsd);
const resolvedPlanLabel = planLabel || "Standard Pro";
const resolvedPeriodLabel = periodLabel || (isEn ? "/ 30 days" : "/ 30 天");
const finalPayLabel =
payLabel || (isEn ? "Subscribe & Activate" : "立即订阅并激活服务");
const maxDiscountUsdInt = Math.max(1, Math.floor(billing.maxDiscountUsd || 0));
const maxPointsForFullDiscount = Math.max(
billing.pointsPerUsd,
billing.pointsPerUsd * maxDiscountUsdInt,
);
const redeemableUsdNow = billing.pointsEnabled
? Math.min(maxDiscountUsdInt, Math.floor(points / Math.max(1, billing.pointsPerUsd)))
: 0;
const formattedRedeemableNow = formatUsdcAmount(redeemableUsdNow);
const formattedMaxDiscountInt = formatUsdcAmount(maxDiscountUsdInt);
const progressPct = billing.pointsEnabled
? Math.min(100, Math.round((points / maxPointsForFullDiscount) * 100))
: 0;
const resolvedTelegramGroupUrl = String(telegramGroupUrl || "").trim();
const txHref =
txHash && txHash.startsWith("0x")
? `${chainId === 137 ? "https://polygonscan.com" : "https://etherscan.io"}/tx/${txHash}`
: "";
return (
<div className={s.modal}>
{/* Ambient glows */}
<div className={s.glowLeft} />
<div className={s.glowRight} />
<div className={s.topLine} />
{/* Close button */}
{onClose && (
<button
onClick={onClose}
className={s.closeBtn}
title={isEn ? "Close" : "关闭"}
>
<X size={15} />
</button>
)}
{/* ── Header ── */}
<div className={s.header}>
<div className={s.badge}>
<Crown size={12} style={{ color: "#fbbf24" }} />
<span className={s.badgeText}>Pro</span>
</div>
<h2 className={s.title}>
{isEn ? "Activate PolyWeather Pro" : "确认开通 PolyWeather Pro"}
</h2>
<p className={s.subtitle}>
{isEn
? "Confirm your subscription for the settlement-source-first terminal: observations, DEB paths, city charts, and alerts."
: "确认订阅结算源优先的天气决策终端:实测观测、DEB 路径、城市图表和提醒。"}
</p>
</div>
{/* ── Cards ── */}
<div className={s.grid}>
{/* Left: Plan card */}
<div className={s.planCard}>
<span className={s.planChip}>
<Zap size={10} />
{resolvedPlanLabel}
</span>
<div
style={{
display: "flex",
alignItems: "baseline",
gap: 4,
marginTop: 12,
}}
>
<span className={s.price}>{formattedPlanPrice}</span>
<span className={s.priceSuffix}>{currencyLabel} {resolvedPeriodLabel}</span>
</div>
<ul className={s.featureList}>
{featureList.map((item, i) => (
<li key={i} className={s.featureItem}>
<span className={s.featureIcon}>
<CheckCircle2 size={11} />
</span>
{item}
</li>
))}
</ul>
</div>
{/* Right: Points card */}
{canUsePoints ? (
<div
className={`${s.pointsCard} ${usePoints ? s.pointsCardActive : ""}`}
>
{/* Header row */}
<div className={s.pointsHeader}>
<div className={s.pointsLabelRow}>
<div
className={`${s.pointsIconBox} ${usePoints ? s.pointsIconBoxActive : ""}`}
>
<Coins size={13} />
</div>
<span
className={`${s.pointsLabel} ${usePoints ? s.pointsLabelActive : ""}`}
>
{isEn ? "Points Credit" : "积分抵扣"}
</span>
</div>
<button
onClick={onToggleUsePoints}
className={`${s.toggle} ${usePoints ? s.toggleActive : ""}`}
title={isEn ? "Toggle points" : "切换积分"}
>
<div
className={`${s.toggleThumb} ${usePoints ? s.toggleThumbActive : ""}`}
/>
</button>
</div>
{/* Discount amount */}
<div style={{ display: "flex", alignItems: "baseline" }}>
<span
className={`${s.discount} ${usePoints ? s.discountActive : ""}`}
>
-{formattedDiscount}
</span>
<span className={s.discountSuffix}>{currencyLabel}</span>
</div>
<p className={s.pointsNote}>
{usePoints
? isEn
? `Using ${billing.pointsUsed} pts · saves ${formattedDiscount} ${currencyLabel}`
: `已消耗 ${billing.pointsUsed} 积分 · 省 ${formattedDiscount} ${currencyLabel}`
: isEn
? `Toggle to save up to ${formattedMaxDiscount} ${currencyLabel}`
: `开启可最多抵扣 ${formattedMaxDiscount} ${currencyLabel}`}
</p>
<p className={s.pointsNote}>
{isEn
? `Now redeemable: ${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`
: `当前可抵:${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`}
</p>
<div className={s.pointsBalance}>
<Sparkles size={11} />
<span>
{isEn ? "Balance:" : "当前积分:"}{" "}
<span
className={`${s.balanceNum} ${usePoints ? s.balanceNumActive : ""}`}
>
{points}
</span>
</span>
</div>
</div>
) : (
/* Not eligible */
<div className={s.pointsUnavailableCard}>
<div className={s.unavailChip}>
<div className={s.unavailChipIcon}>
<Coins size={12} />
</div>
<span className={s.unavailChipLabel}>
{!billing.pointsEnabled
? isEn
? "Points Disabled"
: "积分未开启"
: isEn
? "Insufficient Points"
: "积分不足"}
</span>
</div>
<h4 className={s.unavailTitle}>
{isEn ? "Earn Points & Save" : "赚取积分,抵扣订阅"}
</h4>
<p className={s.unavailDesc}>
{!billing.pointsEnabled
? isEn
? "Points redemption is unavailable for this plan."
: "当前套餐暂不支持积分抵扣。"
: isEn
? `Starts at ${billing.pointsPerUsd} pts, ${billing.pointsPerUsd} pts per 1 ${currencyLabel}, up to ${formattedMaxDiscountInt} ${currencyLabel}. You have: ${points}`
: `满 ${billing.pointsPerUsd} 分起兑,每 ${billing.pointsPerUsd} 分抵 1 ${currencyLabel},最多抵 ${formattedMaxDiscountInt} ${currencyLabel}。当前 ${points} 分`}
</p>
{billing.pointsEnabled && (
<div className={s.progressWrap}>
<div className={s.progressHeader}>
<span>
{points} / {maxPointsForFullDiscount}
</span>
<span>
{isEn
? `${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`
: `${formattedRedeemableNow} / ${formattedMaxDiscountInt} ${currencyLabel}`}
</span>
</div>
<div className={s.progressTrack}>
<div
className={s.progressFill}
style={{ width: `${progressPct}%` }}
/>
</div>
</div>
)}
<div style={{ marginTop: "auto", paddingTop: 16 }}>
{resolvedTelegramGroupUrl ? (
<Link
href={resolvedTelegramGroupUrl}
target="_blank"
className={s.unavailCta}
>
<MessageSquare size={12} />
{isEn
? "Invite paid users to earn points"
: "邀请付费用户即可获得积分"}
<ArrowRight size={11} />
</Link>
) : (
<span className={s.unavailCta} style={{ cursor: "default" }}>
<MessageSquare size={12} />
{isEn
? "Invite paid users to earn points"
: "邀请付费用户即可获得积分"}
</span>
)}
</div>
</div>
)}
</div>
{/* ── Payment summary ── */}
<div className={s.summaryBox}>
<div className={s.summaryRow}>
<div>
<p className={s.summaryLabel}>
{isEn ? "Total Due Today" : "今日应付"}
</p>
{paymentTokenLabel && (
<p className={s.summaryOriginal}>
{isEn ? "Token" : "支付币种"}: {paymentTokenLabel}
</p>
)}
{billing.discountAmount > 0 && usePoints && (
<p className={s.summaryOriginal}>
{formattedPlanPrice} {currencyLabel}
</p>
)}
</div>
<div className={s.summaryAmount}>
<span className={s.summaryPrice}>
{formattedFinalPrice}
</span>
<span className={s.summaryUnit}>{currencyLabel}</span>
</div>
</div>
<div className={s.summaryDivider} />
<div className={s.ctaWrap}>
<button onClick={onPay} disabled={payBusy} className={s.ctaBtn}>
{payBusy ? (
<Loader2 size={18} className="animate-spin" />
) : (
<>
<Wallet size={17} />
{finalPayLabel}
<ArrowRight size={17} />
</>
)}
</button>
{onManualPay ? (
<button
onClick={onManualPay}
disabled={payBusy}
className={s.ctaBtn}
style={{
marginTop: 10,
background: "rgba(15, 23, 42, 0.78)",
border: "1px solid rgba(148, 163, 184, 0.28)",
}}
>
{payBusy ? (
<Loader2 size={18} className="animate-spin" />
) : (
<>
<Wallet size={17} />
{manualPayLabel || (isEn ? "Manual transfer" : "手动转账")}
<ArrowRight size={17} />
</>
)}
</button>
) : null}
{onManualPay ? (
<p
style={{
marginTop: 10,
color: "rgba(203, 213, 225, 0.72)",
fontSize: 12,
lineHeight: 1.5,
textAlign: "center",
}}
>
{isEn
? "Choose one payment method only. Do not pay again after the order is completed."
: "请只选择一种支付方式;订单完成后请勿重复付款。"}
</p>
) : null}
</div>
</div>
{/* ── Footer ── */}
<div className={s.footer}>
<span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
<Lock size={11} />
{isEn ? "Secure Payment" : "安全付款"}
</span>
<span style={{ color: "#0f172a" }}>·</span>
<Link href={faqHref} className={s.footerLink}>
<BellRing size={11} />
{isEn ? "Subscription FAQ" : "订阅说明"}
</Link>
</div>
{/* ── Error / Info ── */}
{errorText && (
<div className={s.alertError}>
<div className={`${s.alertIconBox} ${s.alertIconError}`}>
<X size={10} />
</div>
{errorText}
</div>
)}
{infoText && (
<div className={s.alertInfo}>
<div className={`${s.alertIconBox} ${s.alertIconInfo}`}>
<CheckCircle2 size={10} />
</div>
<div>
<div>{infoText}</div>
{txHref && (
<Link href={txHref} target="_blank" className={s.txLink}>
{isEn ? "View on-chain transaction" : "查看链上交易"}
<ExternalLink size={11} />
</Link>
)}
</div>
</div>
)}
</div>
);
}