From 285191a8be9031191e939134002c1f3700f7c7c9 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 14 Jun 2026 02:34:08 +0800 Subject: [PATCH] Strengthen expired subscription gate --- .../__tests__/trialUpgradeConversion.test.ts | 6 +- .../dashboard/ScanTerminalDashboard.tsx | 3 + .../scan-terminal/ProductAccessRequired.tsx | 137 +++++++++++++++--- .../__tests__/productAccessRequired.test.ts | 18 +++ 4 files changed, 143 insertions(+), 21 deletions(-) diff --git a/frontend/components/account/__tests__/trialUpgradeConversion.test.ts b/frontend/components/account/__tests__/trialUpgradeConversion.test.ts index 00ad2228..0023e86e 100644 --- a/frontend/components/account/__tests__/trialUpgradeConversion.test.ts +++ b/frontend/components/account/__tests__/trialUpgradeConversion.test.ts @@ -32,9 +32,9 @@ export function runTests() { assert( productAccess.includes('href="/account?checkout=1"') && - productAccess.includes("Subscribe & Activate") && - productAccess.includes("立即订阅并激活"), - "expired terminal gate must send users directly to the checkout entry, not a generic account page", + productAccess.includes("Renew and restore access") && + productAccess.includes("续费并恢复访问"), + "expired terminal gate must send users directly to the checkout entry with renewal recovery copy, not a generic account page", ); assert( diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index c7d07c08..d28f4245 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -1787,6 +1787,9 @@ function ScanTerminalScreen() { ); diff --git a/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx b/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx index c112b92e..f45172bb 100644 --- a/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx +++ b/frontend/components/dashboard/scan-terminal/ProductAccessRequired.tsx @@ -1,13 +1,24 @@ "use client"; import Link from "next/link"; -import { LockKeyhole, CreditCard, LogIn } from "lucide-react"; +import { + AlertTriangle, + CreditCard, + LogIn, + RefreshCw, +} from "lucide-react"; const ACCESS_TERM = { signInToContinue: { en: "Sign in to continue", zh: "请先登录" }, - proAccessRequired: { en: "Pro subscription required", zh: "需要开通 Pro" }, + proAccessRequired: { + en: "Subscription expired", + zh: "订阅已过期", + }, month: { en: "/ 30 days", zh: "/ 30 天" }, - subscribeNow: { en: "Subscribe & Activate", zh: "立即订阅并激活" }, + subscribeNow: { + en: "Renew and restore access", + zh: "续费并恢复访问", + }, backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" }, } as const; @@ -15,8 +26,37 @@ function t(key: keyof typeof ACCESS_TERM, isEn: boolean) { return isEn ? ACCESS_TERM[key].en : ACCESS_TERM[key].zh; } +function isExpiredSubscription(expiresAt?: string | null) { + if (!expiresAt) return false; + const timestamp = Date.parse(expiresAt); + return Number.isFinite(timestamp) && timestamp <= Date.now(); +} + +function formatSubscriptionDate(expiresAt: string | null | undefined, isEn: boolean) { + if (!expiresAt) return ""; + const timestamp = Date.parse(expiresAt); + if (!Number.isFinite(timestamp)) return ""; + try { + return new Intl.DateTimeFormat(isEn ? "en-US" : "zh-CN", { + day: "2-digit", + month: "short", + year: "numeric", + }).format(new Date(timestamp)); + } catch { + return ""; + } +} + // ─── Layer 2: Authenticated but no active subscription ─────────────────────── -function SubscriptionGate({ isEn }: { isEn: boolean }) { +function SubscriptionGate({ + isEn, + subscriptionExpiresAt, +}: { + isEn: boolean; + subscriptionExpiresAt?: string | null; +}) { + const expired = isExpiredSubscription(subscriptionExpiresAt); + const expiryLabel = formatSubscriptionDate(subscriptionExpiresAt, isEn); const features = isEn ? [ "Real-time station, METAR, and runway signals", @@ -33,25 +73,57 @@ function SubscriptionGate({ isEn }: { isEn: boolean }) { return (
-
+
- - - {t("proAccessRequired", isEn)} + + + {expired + ? t("proAccessRequired", isEn) + : isEn + ? "No active subscription" + : "无有效订阅"}
-
+
+
+
+

+ {isEn ? "Access paused" : "终端访问已暂停"} +

+

+ {expired + ? isEn + ? "Your subscription has expired" + : "订阅已过期,终端访问已暂停" + : isEn + ? "No active Pro subscription" + : "未检测到有效订阅"} +

+
+ + {expiryLabel + ? isEn + ? `Expired ${expiryLabel}` + : `到期日 ${expiryLabel}` + : isEn + ? "Action required" + : "需要处理"} + +
+
+ +

{isEn - ? "Unlock the Weather Terminal" - : "解锁天气决策台"} + ? "Renew to restore the Weather Terminal" + : "续费后恢复天气决策台访问"}

-

+

{isEn - ? "Your account is verified. One step away from full access." - : "账号已验证,只差一步即可获得完整访问权限。"} + ? "Your account is verified, but it does not currently have an active subscription. Renewing restores live temperature evidence, DEB paths, runway alerts, and paid Telegram/API access." + : "你的账号已验证,但当前没有有效订阅。续费后会立即恢复实时温度、DEB 路径、跑道提醒和付费 Telegram/API 权益。"}

@@ -78,16 +150,40 @@ function SubscriptionGate({ isEn }: { isEn: boolean }) { {t("subscribeNow", isEn)} +
+

+ {isEn + ? "Already paid but still blocked?" + : "已有付款但未恢复?"} +

+
+ + + {isEn ? "Open account center" : "账户中心"} + +
+
+

{isEn - ? "Cancel anytime · No hidden fees · Instant access after payment" - : "随时取消 · 无隐藏费用 · 付款后立即解锁"} + ? "Access resumes automatically after payment confirmation" + : "付款确认后会自动恢复访问"}

@@ -166,10 +262,12 @@ function UnauthenticatedGate({ export function ProductAccessRequired({ isAuthenticated, isEn, + subscriptionExpiresAt, userLocalTime, }: { isAuthenticated: boolean; isEn: boolean; + subscriptionExpiresAt?: string | null; userLocalTime: string; }) { if (!isAuthenticated) { @@ -186,7 +284,10 @@ export function ProductAccessRequired({
{userLocalTime}
- +
); diff --git a/frontend/components/dashboard/scan-terminal/__tests__/productAccessRequired.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/productAccessRequired.test.ts index 591ecddf..b9b9318e 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/productAccessRequired.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/productAccessRequired.test.ts @@ -20,6 +20,10 @@ export function runTests() { source.indexOf("function UnauthenticatedGate"), source.indexOf("export function ProductAccessRequired"), ); + const subscriptionGate = source.slice( + source.indexOf("function SubscriptionGate"), + source.indexOf("// ─── Layer 1 fallback"), + ); const accessCard = unauthenticatedGate.slice( unauthenticatedGate.indexOf('