Fix terminal subscription gate on unknown auth state

This commit is contained in:
2569718930@qq.com
2026-05-30 18:34:38 +08:00
parent 90bc895000
commit 097971f107
8 changed files with 212 additions and 38 deletions
@@ -127,7 +127,7 @@ const TERM = {
logIn: { en: "Log in", zh: "登录" },
createAccount: { en: "Create an account", zh: "注册账号" },
learnAbout: { en: "Learn about PolyWeather", zh: "了解 PolyWeather" },
proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" },
proAccessRequired: { en: "Pro subscription required", zh: "需要开通 Pro" },
proDesc: {
en: "The PolyWeather terminal is a paid product. Subscribe to unlock real-time weather-signal intelligence.",
zh: "PolyWeather 决策台为付费产品。订阅以解锁实时天气信号情报。",
@@ -137,7 +137,7 @@ const TERM = {
zh: "按月计费,随时可取消。通过 Polygon 链 USDC 支付。",
},
month: { en: "/ month", zh: "/ 月" },
subscribeNow: { en: "Subscribe Now — $10/mo", zh: "立即订阅 — $10/月" },
subscribeNow: { en: "View Pro plans", zh: "查看订阅方案" },
subscribePrompt: {
en: "You need an active subscription to access the terminal.",
zh: "你需要开通有效订阅才能访问决策台。",
@@ -1078,6 +1078,62 @@ function ScanTerminalScreen() {
};
}, [loadAuthProfile]);
useEffect(() => {
if (
!hydrated ||
canUseLocalFullAccess ||
!proAccess.authenticated ||
!proAccess.loading ||
proAccess.subscriptionActive ||
typeof fetch !== "function"
) {
return;
}
let cancelled = false;
const supabaseEnabled = hasSupabasePublicEnv();
const retryAuthProfile = async () => {
try {
const payload = await loadTerminalAuthProfile({
getSession: () =>
supabaseEnabled
? getSupabaseBrowserClient().auth.getSession()
: Promise.resolve({ data: { session: null } }),
hasSupabasePublicEnv: supabaseEnabled,
loadAuthProfile,
});
if (cancelled) return;
setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload));
} catch (error) {
if (cancelled) return;
setProAccess((prev) =>
prev.loading && prev.authenticated && !prev.subscriptionActive
? { ...prev, error: String(error) }
: prev,
);
}
};
const firstRetry = window.setTimeout(() => {
void retryAuthProfile();
}, 1500);
const interval = window.setInterval(() => {
void retryAuthProfile();
}, 5000);
return () => {
cancelled = true;
window.clearTimeout(firstRetry);
window.clearInterval(interval);
};
}, [
canUseLocalFullAccess,
hydrated,
loadAuthProfile,
proAccess.authenticated,
proAccess.loading,
proAccess.subscriptionActive,
]);
useEffect(() => {
setSelectedRegionKey("all");
setLocalTimezoneOffsetSeconds(-new Date().getTimezoneOffset() * 60);
@@ -5,9 +5,9 @@ import { LockKeyhole, CreditCard, LogIn } from "lucide-react";
const ACCESS_TERM = {
signInToContinue: { en: "Sign in to continue", zh: "请先登录" },
proAccessRequired: { en: "Pro Access Required", zh: "需要付费订阅" },
month: { en: "/ month", zh: "/ " },
subscribeNow: { en: "Subscribe Now — $10/mo", zh: "立即订阅 — $10/月" },
proAccessRequired: { en: "Pro subscription required", zh: "需要开通 Pro" },
month: { en: "/ 30 days", zh: "/ 30 天" },
subscribeNow: { en: "View Pro plans", zh: "查看订阅方案" },
backToProduct: { en: "Back to product overview", zh: "返回产品介绍页" },
} as const;
@@ -19,16 +19,16 @@ function t(key: keyof typeof ACCESS_TERM, isEn: boolean) {
function SubscriptionGate({ isEn }: { isEn: boolean }) {
const features = isEn
? [
"Real-time METAR observations across 500+ stations",
"DEB forecast blends with 0240h horizon",
"AI decision cards with Poly-score ranking",
"Historical backtesting & weather signals",
"Real-time station, METAR, and runway signals",
"DEB forecast curves and model comparison",
"Full terminal grid with high-frequency refresh",
"API and paid Telegram group access on paid Pro",
]
: [
"500+ 气象站实时 METAR 实况",
"DEB 智能融合预测(0240 小时)",
"AI 决策卡片 + Poly-score 排名",
"历史回测与天气信号",
"实时气象站、METAR 与跑道信号",
"DEB 预测曲线与模型对比",
"完整终端网格与高频刷新",
"付费 Pro 可进入 API 与付费 Telegram 群",
];
return (
@@ -57,7 +57,7 @@ function SubscriptionGate({ isEn }: { isEn: boolean }) {
<div className="p-8">
<div className="mb-6 flex items-baseline gap-1">
<span className="text-4xl font-black text-slate-900">$10</span>
<span className="text-4xl font-black text-slate-900">29.9 USDC</span>
<span className="text-base text-slate-500">
{t("month", isEn)}
</span>
@@ -65,7 +65,9 @@ export function runTests() {
degraded_auth_profile: true,
});
assert(
coldUnknown.subscriptionActive === false && coldUnknown.authenticated === true,
"cold-start unknown subscription state must not fabricate Pro access",
coldUnknown.subscriptionActive === false &&
coldUnknown.authenticated === true &&
coldUnknown.loading === true,
"cold-start unknown subscription state must keep the terminal gate loading instead of showing a false paywall",
);
}
@@ -16,11 +16,21 @@ function queuedDays(value: unknown) {
return Math.max(0, Number(value ?? 0));
}
export function isSubscriptionStatusUnknown(payload: AuthProfilePayload) {
return (
payload.subscription_active === null ||
payload.subscription_active === undefined ||
payload.degraded_auth_profile === true
);
}
export function createAccessStateFromAuthPayload(
payload: AuthProfilePayload,
): ProAccessState {
const subscriptionUnknown =
Boolean(payload.authenticated) && isSubscriptionStatusUnknown(payload);
return {
loading: false,
loading: subscriptionUnknown,
authenticated: Boolean(payload.authenticated),
userId: payload.user_id ?? null,
subscriptionActive: payload.subscription_active === true,
@@ -41,10 +51,7 @@ export function mergeAccessStateWithAuthPayload(
payload: AuthProfilePayload,
): ProAccessState {
const next = createAccessStateFromAuthPayload(payload);
const subscriptionUnknown =
payload.subscription_active === null ||
payload.subscription_active === undefined ||
payload.degraded_auth_profile === true;
const subscriptionUnknown = isSubscriptionStatusUnknown(payload);
if (!subscriptionUnknown || !previous.subscriptionActive || !next.authenticated) {
return next;
@@ -52,6 +59,7 @@ export function mergeAccessStateWithAuthPayload(
return {
...next,
loading: false,
subscriptionActive: true,
subscriptionPlanCode: previous.subscriptionPlanCode,
subscriptionExpiresAt: previous.subscriptionExpiresAt,