Fix terminal subscription gate on unknown auth state
This commit is contained in:
@@ -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 0–240h 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 智能融合预测(0–240 小时)",
|
||||
"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,
|
||||
|
||||
Reference in New Issue
Block a user