diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index b8b86ec2..019ca90a 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -410,7 +410,6 @@ export function AccountCenter() { const canTrialUpgrade = Boolean(isSubscribed && isTrialSubscription); const canOpenCheckoutOverlay = Boolean( paymentFeatureReady && - !isSubscriptionUnknown && (canTrialUpgrade || !isSubscribed || showExpiringSoon || showExpiredReminder), ); const subscriptionStatusTitle = showExpiredReminder diff --git a/frontend/components/account/__tests__/paymentShell.test.ts b/frontend/components/account/__tests__/paymentShell.test.ts index 36bd9950..8567c7e9 100644 --- a/frontend/components/account/__tests__/paymentShell.test.ts +++ b/frontend/components/account/__tests__/paymentShell.test.ts @@ -253,6 +253,17 @@ export function runTests() { ), "account center must distinguish unknown subscription sync state from a confirmed unsubscribed account", ); + assert( + !accountCenterSource.includes("paymentFeatureReady &&\n !isSubscriptionUnknown") && + !accountCenterSource.includes("paymentFeatureReady &&\r\n !isSubscriptionUnknown"), + "account center must not block checkout when an authenticated user's subscription status is temporarily unknown", + ); + assert( + accountFeatureSource.includes("状态待确认") && + accountFeatureSource.includes("刷新或直接续费") && + !accountFeatureSource.includes("订阅状态正在同步,请稍后刷新。"), + "account unknown subscription copy must tell users to refresh or renew instead of waiting indefinitely", + ); assert( hookSource.includes("backendJson.authenticated === false") && hookSource.includes("refreshSession()") && diff --git a/frontend/components/account/account-copy.ts b/frontend/components/account/account-copy.ts index 484a7968..39da0736 100644 --- a/frontend/components/account/account-copy.ts +++ b/frontend/components/account/account-copy.ts @@ -165,10 +165,10 @@ export function createAccountCopy(isEn: boolean): Record { : "钱包已绑定,正在创建订单并发起支付...", proMember: "PRO MEMBER", proPendingSync: isEn ? "Activated (pending sync)" : "已开通(待同步)", - subscriptionChecking: isEn ? "Checking subscription" : "订阅同步中", + subscriptionChecking: isEn ? "Status pending" : "状态待确认", subscriptionUnknown: isEn - ? "Subscription status is syncing. Please refresh shortly." - : "订阅状态正在同步,请稍后刷新。", + ? "Subscription status is not confirmed yet. Refresh or renew directly." + : "订阅状态暂未确认,可刷新或直接续费。", noProSubscription: isEn ? "No Pro subscription" : "暂无 Pro 订阅", proEndsSoonTitle: isEn ? "Pro renewal due soon" : "Pro 即将到期", proEndsSoonBody: isEn diff --git a/frontend/components/dashboard/scan-terminal/__tests__/terminalAuthBootstrap.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/terminalAuthBootstrap.test.ts index a0d02736..a5f92775 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/terminalAuthBootstrap.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/terminalAuthBootstrap.test.ts @@ -228,6 +228,46 @@ export async function runTests() { "terminal auth bootstrap must not resolve to an anonymous paywall when a bearer session exists but the auth profile request is transiently failing", ); + const unresolvedSession = deferred<{ data: { session: { access_token: string } | null } }>(); + let anonymousResolvedBeforeSession = false; + try { + const maybeAnonymous = await loadTerminalAuthProfile({ + hasSupabasePublicEnv: true, + getSession: () => unresolvedSession.promise, + timeoutMs: 1, + loadAuthProfile: (accessToken) => { + assert(!accessToken, "cookie-only anonymous probe should not receive a bearer token"); + return Promise.resolve({ + authenticated: false, + subscription_active: false, + points: 0, + }); + }, + }); + anonymousResolvedBeforeSession = maybeAnonymous.authenticated === false; + } catch {} + assert( + !anonymousResolvedBeforeSession, + "terminal auth bootstrap must not show the signed-out gate while the local Supabase session is still unresolved", + ); + + const signedOutProfile = await loadTerminalAuthProfile({ + hasSupabasePublicEnv: true, + getSession: () => Promise.resolve({ data: { session: null } }), + loadAuthProfile: (accessToken) => { + assert(!accessToken, "signed-out profile should be resolved through the cookie probe only"); + return Promise.resolve({ + authenticated: false, + subscription_active: false, + points: 0, + }); + }, + }); + assert( + signedOutProfile.authenticated === false, + "terminal auth bootstrap must still resolve a confirmed signed-out browser as unauthenticated", + ); + assert( isSubscriptionRequiredBackendResponse( 403, diff --git a/frontend/components/dashboard/scan-terminal/terminal-auth-bootstrap.ts b/frontend/components/dashboard/scan-terminal/terminal-auth-bootstrap.ts index b350ad81..9d3028fa 100644 --- a/frontend/components/dashboard/scan-terminal/terminal-auth-bootstrap.ts +++ b/frontend/components/dashboard/scan-terminal/terminal-auth-bootstrap.ts @@ -96,6 +96,7 @@ export async function loadTerminalAuthProfile({ | null = null; let latestCookiePayload: TerminalAuthProfilePayload | null = null; let latestBearerPayload: TerminalAuthProfilePayload | null = null; + let sessionSettled = !hasSupabasePublicEnv; const authenticatedProfile = new Promise((resolve) => { resolveAuthenticated = resolve; @@ -119,6 +120,7 @@ export async function loadTerminalAuthProfile({ (async () => { if (!hasSupabasePublicEnv) return null; const sessionResult = await getSession(); + sessionSettled = true; if (resolvedAuthenticated) return null; const accessToken = String( sessionResult?.data?.session?.access_token || "", @@ -142,6 +144,14 @@ export async function loadTerminalAuthProfile({ resolve(latestBearerPayload); return; } + if ( + latestCookiePayload?.authenticated === false && + hasSupabasePublicEnv && + !sessionSettled + ) { + reject(new Error("Terminal auth bootstrap timeout")); + return; + } if (latestCookiePayload) { resolve(latestCookiePayload); return;