2026-03-13 08:51:06 +08:00
"use client" ;
import Link from "next/link" ;
import {
ArrowRight ,
BellRing ,
CheckCircle2 ,
Coins ,
Crown ,
2026-03-13 13:13:51 +08:00
ExternalLink ,
2026-03-13 08:51:06 +08:00
Loader2 ,
Lock ,
MessageSquare ,
2026-03-13 09:25:37 +08:00
Radar ,
Send ,
Sparkles ,
2026-03-13 08:51:06 +08:00
TrendingUp ,
Wallet ,
X ,
2026-03-13 09:25:37 +08:00
Zap ,
2026-03-13 08:51:06 +08:00
} from "lucide-react" ;
2026-03-13 09:32:14 +08:00
import s from "./UnlockProOverlay.module.css" ;
2026-03-13 08:51:06 +08:00
2026-03-13 20:38:10 +08:00
const DEFAULT_FAQ_HREF = "/subscription-help" ;
2026-05-30 18:04:36 +08:00
const DEFAULT_TELEGRAM_GROUP_URL = "" ;
2026-03-13 20:38:10 +08:00
2026-03-13 08:51:06 +08:00
export type UnlockProBilling = {
pointsEnabled : boolean ;
isEligible : boolean ;
pointsUsed : number ;
discountAmount : number ;
finalPrice : number ;
maxDiscountUsd : number ;
pointsPerUsd : number ;
};
type UnlockProOverlayProps = {
points : number ;
planPriceUsd : number ;
2026-06-07 01:08:13 +08:00
planLabel? : string ;
periodLabel? : string ;
2026-03-13 08:51:06 +08:00
usePoints : boolean ;
billing : UnlockProBilling ;
onToggleUsePoints : () => void ;
onPay : () => void ;
2026-05-18 16:18:26 +08:00
onManualPay ?: () => void ;
2026-03-13 08:51:06 +08:00
onClose ?: () => void ;
payBusy? : boolean ;
payLabel? : string ;
2026-05-18 16:18:26 +08:00
manualPayLabel? : string ;
2026-03-13 08:51:06 +08:00
locale ?: "zh-CN" | "en-US" ;
errorText? : string ;
infoText? : string ;
faqHref? : string ;
telegramGroupUrl? : string ;
2026-03-13 13:13:51 +08:00
txHash? : string ;
chainId? : number ;
2026-03-13 13:58:41 +08:00
paymentTokenLabel? : string ;
2026-03-13 08:51:06 +08:00
};
2026-06-07 01:08:13 +08:00
function formatUsdcAmount ( value : number ) {
if ( ! Number . isFinite ( value )) return "0" ;
return value . toFixed ( 2 ). replace ( /\.?0+$/ , "" );
}
2026-03-13 09:32:14 +08:00
const FEATURES = {
2026-03-13 12:26:35 +08:00
"zh-CN" : [
2026-06-14 06:24:21 +08:00
"Terminal:市场温度与城市机会扫描" ,
"机场 / 跑道实测:METAR + 结算源优先" ,
"DEB 路径与高温时段复核" ,
"Telegram 提醒与账户权益同步" ,
2026-03-13 12:26:35 +08:00
],
2026-03-13 09:25:37 +08:00
"en-US" : [
2026-06-14 06:24:21 +08:00
"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" ,
2026-03-13 09:25:37 +08:00
],
};
2026-03-13 08:51:06 +08:00
export function UnlockProOverlay ({
points ,
planPriceUsd ,
2026-06-07 01:08:13 +08:00
planLabel ,
periodLabel ,
2026-03-13 08:51:06 +08:00
usePoints ,
billing ,
onToggleUsePoints ,
onPay ,
2026-05-18 16:18:26 +08:00
onManualPay ,
2026-03-13 08:51:06 +08:00
onClose ,
payBusy = false ,
payLabel ,
2026-05-18 16:18:26 +08:00
manualPayLabel ,
2026-03-13 08:51:06 +08:00
locale = "zh-CN" ,
errorText ,
infoText ,
2026-03-13 20:38:10 +08:00
faqHref = DEFAULT_FAQ_HREF ,
telegramGroupUrl = DEFAULT_TELEGRAM_GROUP_URL ,
2026-03-13 13:13:51 +08:00
txHash ,
chainId = 137 ,
2026-03-13 13:58:41 +08:00
paymentTokenLabel ,
2026-03-13 08:51:06 +08:00
} : UnlockProOverlayProps ) {
const isEn = locale === "en-US" ;
2026-03-13 09:32:14 +08:00
const canUsePoints = billing . pointsEnabled && billing . isEligible ;
const featureList = FEATURES [ locale ] ?? FEATURES [ "zh-CN" ];
2026-06-07 01:08:13 +08:00
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 天" );
2026-03-13 08:51:06 +08:00
const finalPayLabel =
payLabel || ( isEn ? "Subscribe & Activate" : "立即订阅并激活服务" );
2026-03-13 12:45:39 +08:00
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 ;
2026-06-07 01:08:13 +08:00
const formattedRedeemableNow = formatUsdcAmount ( redeemableUsdNow );
const formattedMaxDiscountInt = formatUsdcAmount ( maxDiscountUsdInt );
2026-03-13 09:32:14 +08:00
const progressPct = billing . pointsEnabled
2026-03-13 12:45:39 +08:00
? Math . min ( 100 , Math . round (( points / maxPointsForFullDiscount ) * 100 ))
2026-03-13 09:32:14 +08:00
: 0 ;
2026-03-13 20:38:10 +08:00
const resolvedTelegramGroupUrl = String ( telegramGroupUrl || "" ). trim ();
2026-03-13 13:13:51 +08:00
const txHref =
txHash && txHash . startsWith ( "0x" )
? ` ${ chainId === 137 ? "https://polygonscan.com" : "https://etherscan.io" } /tx/ ${ txHash } `
: "" ;
2026-03-13 09:25:37 +08:00
2026-03-13 08:51:06 +08:00
return (
2026-03-13 09:32:14 +08:00
< div className = { s . modal }>
{ /* Ambient glows */ }
< div className = { s . glowLeft } />
< div className = { s . glowRight } />
< div className = { s . topLine } />
2026-03-13 09:25:37 +08:00
{ /* Close button */ }
2026-03-13 08:51:06 +08:00
{ onClose && (
< button
onClick = { onClose }
2026-03-13 09:32:14 +08:00
className = { s . closeBtn }
2026-03-13 08:51:06 +08:00
title = { isEn ? "Close" : "关闭" }
>
2026-03-13 09:32:14 +08:00
< X size = { 15 } />
2026-03-13 08:51:06 +08:00
</ button >
)}
2026-03-13 09:32:14 +08:00
{ /* ── Header ── */ }
< div className = { s . header }>
< div className = { s . badge }>
< Crown size = { 12 } style = {{ color : "#fbbf24" }} />
< span className = { s . badgeText }> Pro </ span >
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:32:14 +08:00
< h2 className = { s . title }>
2026-06-14 06:24:21 +08:00
{ isEn ? "Activate PolyWeather Pro" : "确认开通 PolyWeather Pro" }
2026-03-13 09:32:14 +08:00
</ h2 >
< p className = { s . subtitle }>
{ isEn
2026-06-14 06:24:21 +08:00
? "Confirm your subscription for the settlement-source-first terminal: observations, DEB paths, city charts, and alerts."
: "确认订阅结算源优先的天气决策终端:实测观测、DEB 路径、城市图表和提醒。" }
2026-03-13 09:32:14 +08:00
</ p >
</ div >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
{ /* ── Cards ── */ }
< div className = { s . grid }>
{ /* Left: Plan card */ }
< div className = { s . planCard }>
< span className = { s . planChip }>
< Zap size = { 10 } />
2026-06-07 01:08:13 +08:00
{ resolvedPlanLabel }
2026-03-13 09:32:14 +08:00
</ span >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
< div
style = {{
display : "flex" ,
alignItems : "baseline" ,
gap : 4 ,
marginTop : 12 ,
}}
>
2026-06-07 01:08:13 +08:00
< span className = { s . price }>{ formattedPlanPrice }</ span >
< span className = { s . priceSuffix }>{ currencyLabel } { resolvedPeriodLabel }</ span >
2026-03-13 09:32:14 +08:00
</ div >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
< ul className = { s . featureList }>
{ featureList . map (( item , i ) => (
< li key = { i } className = { s . featureItem }>
< span className = { s . featureIcon }>
< CheckCircle2 size = { 11 } />
2026-03-13 09:25:37 +08:00
</ span >
2026-03-13 09:32:14 +08:00
{ item }
</ li >
))}
</ ul >
</ div >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
{ /* 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 >
2026-03-13 09:25:37 +08:00
</ div >
2026-03-13 09:32:14 +08:00
< button
onClick = { onToggleUsePoints }
className = { ` ${ s . toggle } ${ usePoints ? s . toggleActive : "" } ` }
title = { isEn ? "Toggle points" : "切换积分" }
>
< div
className = { ` ${ s . toggleThumb } ${ usePoints ? s . toggleThumbActive : "" } ` }
/>
</ button >
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
{ /* Discount amount */ }
< div style = {{ display : "flex" , alignItems : "baseline" }}>
< span
className = { ` ${ s . discount } ${ usePoints ? s . discountActive : "" } ` }
>
2026-06-07 01:08:13 +08:00
- { formattedDiscount }
2026-03-13 09:32:14 +08:00
</ span >
2026-06-07 01:08:13 +08:00
< span className = { s . discountSuffix }>{ currencyLabel }</ span >
2026-03-13 09:32:14 +08:00
</ div >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
< p className = { s . pointsNote }>
{ usePoints
? isEn
2026-06-07 01:08:13 +08:00
? `Using ${ billing . pointsUsed } pts · saves ${ formattedDiscount } ${ currencyLabel } `
: `已消耗 ${ billing . pointsUsed } 积分 · 省 ${ formattedDiscount } ${ currencyLabel } `
2026-03-13 09:32:14 +08:00
: isEn
2026-06-07 01:08:13 +08:00
? `Toggle to save up to ${ formattedMaxDiscount } ${ currencyLabel } `
: `开启可最多抵扣 ${ formattedMaxDiscount } ${ currencyLabel } ` }
2026-03-13 09:32:14 +08:00
</ p >
2026-03-13 12:45:39 +08:00
< p className = { s . pointsNote }>
{ isEn
2026-06-07 01:08:13 +08:00
? `Now redeemable: ${ formattedRedeemableNow } / ${ formattedMaxDiscountInt } ${ currencyLabel } `
: `当前可抵: ${ formattedRedeemableNow } / ${ formattedMaxDiscountInt } ${ currencyLabel } ` }
2026-03-13 12:45:39 +08:00
</ p >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
< 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 } />
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:32:14 +08:00
< span className = { s . unavailChipLabel }>
{ ! billing . pointsEnabled
? isEn
? "Points Disabled"
: "积分未开启"
: isEn
? "Insufficient Points"
: "积分不足" }
</ span >
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
< h4 className = { s . unavailTitle }>
{ isEn ? "Earn Points & Save" : "赚取积分,抵扣订阅" }
</ h4 >
< p className = { s . unavailDesc }>
{ ! billing . pointsEnabled
? isEn
? "Points redemption is unavailable for this plan."
: "当前套餐暂不支持积分抵扣。"
: isEn
2026-06-07 01:08:13 +08:00
? `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 } 分` }
2026-03-13 09:32:14 +08:00
</ p >
2026-03-13 09:25:37 +08:00
2026-03-13 09:32:14 +08:00
{ billing . pointsEnabled && (
< div className = { s . progressWrap }>
< div className = { s . progressHeader }>
< span >
2026-03-13 12:45:39 +08:00
{ points } / { maxPointsForFullDiscount }
</ span >
< span >
{ isEn
2026-06-07 01:08:13 +08:00
? ` ${ formattedRedeemableNow } / ${ formattedMaxDiscountInt } ${ currencyLabel } `
: ` ${ formattedRedeemableNow } / ${ formattedMaxDiscountInt } ${ currencyLabel } ` }
2026-03-13 09:32:14 +08:00
</ span >
</ div >
< div className = { s . progressTrack }>
< div
className = { s . progressFill }
style = {{ width : ` ${ progressPct } %` }}
/>
2026-03-13 09:25:37 +08:00
</ div >
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:32:14 +08:00
)}
2026-03-13 08:51:06 +08:00
2026-03-13 09:32:14 +08:00
< div style = {{ marginTop : "auto" , paddingTop : 16 }}>
2026-03-13 20:38:10 +08:00
{ resolvedTelegramGroupUrl ? (
2026-03-13 09:32:14 +08:00
< Link
2026-03-13 20:38:10 +08:00
href = { resolvedTelegramGroupUrl }
2026-03-13 09:32:14 +08:00
target = "_blank"
className = { s . unavailCta }
>
< MessageSquare size = { 12 } />
2026-03-13 20:38:10 +08:00
{ isEn
2026-05-30 18:04:36 +08:00
? "Invite paid users to earn points"
: "邀请付费用户即可获得积分" }
2026-03-13 09:32:14 +08:00
< ArrowRight size = { 11 } />
</ Link >
) : (
< span className = { s . unavailCta } style = {{ cursor : "default" }}>
< MessageSquare size = { 12 } />
{ isEn
2026-05-30 18:04:36 +08:00
? "Invite paid users to earn points"
: "邀请付费用户即可获得积分" }
2026-03-13 09:32:14 +08:00
</ span >
2026-03-13 09:25:37 +08:00
)}
</ div >
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:32:14 +08:00
)}
</ div >
2026-03-13 08:51:06 +08:00
2026-03-13 09:32:14 +08:00
{ /* ── Payment summary ── */ }
< div className = { s . summaryBox }>
< div className = { s . summaryRow }>
< div >
< p className = { s . summaryLabel }>
{ isEn ? "Total Due Today" : "今日应付" }
</ p >
2026-03-13 13:58:41 +08:00
{ paymentTokenLabel && (
< p className = { s . summaryOriginal }>
{ isEn ? "Token" : "支付币种" } : { paymentTokenLabel }
</ p >
)}
2026-03-13 09:32:14 +08:00
{ billing . discountAmount > 0 && usePoints && (
< p className = { s . summaryOriginal }>
2026-06-07 01:08:13 +08:00
{ formattedPlanPrice } { currencyLabel }
2026-03-13 09:32:14 +08:00
</ p >
)}
</ div >
< div className = { s . summaryAmount }>
< span className = { s . summaryPrice }>
2026-06-07 01:08:13 +08:00
{ formattedFinalPrice }
2026-03-13 09:32:14 +08:00
</ span >
2026-06-07 01:08:13 +08:00
< span className = { s . summaryUnit }>{ currencyLabel }</ span >
2026-03-13 08:51:06 +08:00
</ div >
2026-03-13 09:25:37 +08:00
</ div >
2026-03-13 09:32:14 +08:00
< 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 >
2026-05-18 16:18:26 +08:00
{ 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 }
2026-03-13 09:25:37 +08:00
</ div >
2026-03-13 09:32:14 +08:00
</ div >
2026-03-13 09:06:45 +08:00
2026-03-13 09:32:14 +08:00
{ /* ── 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 } />
2026-03-13 09:25:37 +08:00
</ div >
2026-03-13 09:32:14 +08:00
{ errorText }
</ div >
)}
{ infoText && (
< div className = { s . alertInfo }>
< div className = { ` ${ s . alertIconBox } ${ s . alertIconInfo } ` }>
< CheckCircle2 size = { 10 } />
2026-03-13 09:25:37 +08:00
</ div >
2026-03-13 13:13:51 +08:00
< div >
< div >{ infoText }</ div >
{ txHref && (
< Link href = { txHref } target = "_blank" className = { s . txLink }>
2026-05-25 02:04:00 +08:00
{ isEn ? "View on-chain transaction" : "查看链上交易" }
2026-03-13 13:13:51 +08:00
< ExternalLink size = { 11 } />
</ Link >
)}
</ div >
2026-03-13 09:32:14 +08:00
</ div >
)}
2026-03-13 08:51:06 +08:00
</ div >
);
2026-03-13 09:06:45 +08:00
}