diff --git a/frontend/app/auth/callback/route.ts b/frontend/app/auth/callback/route.ts index ff8c14ba..9c8f0233 100644 --- a/frontend/app/auth/callback/route.ts +++ b/frontend/app/auth/callback/route.ts @@ -96,7 +96,7 @@ async function resolvePostAuthRedirect({ } const redirectPath = - profile?.subscription_active === false ? TERMINAL_CHECKOUT_PATH : nextPath; + profile?.subscription_active === true ? nextPath : TERMINAL_CHECKOUT_PATH; return new URL(redirectPath, baseUrl); } diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index 3880c27f..b8b86ec2 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -675,7 +675,7 @@ export function AccountCenter() { ) : ( {copy.signIn} diff --git a/frontend/components/account/__tests__/trialUpgradeConversion.test.ts b/frontend/components/account/__tests__/trialUpgradeConversion.test.ts index 0023e86e..243d3d1c 100644 --- a/frontend/components/account/__tests__/trialUpgradeConversion.test.ts +++ b/frontend/components/account/__tests__/trialUpgradeConversion.test.ts @@ -11,6 +11,14 @@ export function runTests() { const accountCenter = fs.readFileSync(path.join(accountDir, "AccountCenter.tsx"), "utf8"); const paymentFlow = fs.readFileSync(path.join(accountDir, "usePaymentFlow.ts"), "utf8"); const accountPayment = fs.readFileSync(path.join(accountDir, "useAccountPayment.ts"), "utf8"); + const landingPage = fs.readFileSync( + path.join(projectRoot, "components", "landing", "InstitutionalLandingPage.tsx"), + "utf8", + ); + const landingAuthActions = fs.readFileSync( + path.join(projectRoot, "components", "landing", "LandingAuthActions.tsx"), + "utf8", + ); const productAccess = fs.readFileSync( path.join(projectRoot, "components", "dashboard", "scan-terminal", "ProductAccessRequired.tsx"), "utf8", @@ -30,6 +38,19 @@ export function runTests() { "account page must support /account?checkout=1 for direct upgrade entry", ); + assert( + accountCenter.includes('href="/auth/login?next=%2Faccount%3Fcheckout%3D1"'), + "account page sign-in button must preserve the checkout entry for unauthenticated subscription recovery", + ); + + assert( + landingAuthActions.includes('href="/account?checkout=1"') && + landingPage.includes('href="/account?checkout=1"') && + !/href="\/account"(?![/?#])/.test(landingAuthActions) && + !/href="\/account"(?![/?#])/.test(landingPage), + "landing subscription/account CTAs must open the checkout account entry instead of a generic account page", + ); + assert( productAccess.includes('href="/account?checkout=1"') && productAccess.includes("Renew and restore access") && diff --git a/frontend/components/auth/LoginClient.tsx b/frontend/components/auth/LoginClient.tsx index bddbc698..ba5a10c7 100644 --- a/frontend/components/auth/LoginClient.tsx +++ b/frontend/components/auth/LoginClient.tsx @@ -51,13 +51,13 @@ async function resolvePostLoginRedirect({ Authorization: `Bearer ${token}`, }, }); - if (!response.ok) return nextPath; + if (!response.ok) return TERMINAL_CHECKOUT_PATH; const payload = await response.json(); - return payload?.subscription_active === false - ? TERMINAL_CHECKOUT_PATH - : nextPath; + return payload?.subscription_active === true + ? nextPath + : TERMINAL_CHECKOUT_PATH; } catch { - return nextPath; + return TERMINAL_CHECKOUT_PATH; } } diff --git a/frontend/components/auth/__tests__/terminalCheckoutRedirect.test.ts b/frontend/components/auth/__tests__/terminalCheckoutRedirect.test.ts index 40eaa53f..8d2ba8e6 100644 --- a/frontend/components/auth/__tests__/terminalCheckoutRedirect.test.ts +++ b/frontend/components/auth/__tests__/terminalCheckoutRedirect.test.ts @@ -20,19 +20,22 @@ export function runTests() { loginClientSource.includes("TERMINAL_CHECKOUT_PATH") && loginClientSource.includes('"/account?checkout=1"') && loginClientSource.includes("resolvePostLoginRedirect") && - loginClientSource.includes("subscription_active === false") && + loginClientSource.includes("subscription_active === true") && + loginClientSource.includes("return TERMINAL_CHECKOUT_PATH") && loginClientSource.includes("router.replace(redirectPath)") && + !loginClientSource.includes("if (!response.ok) return nextPath;") && + !loginClientSource.includes("catch {\n return nextPath;") && !loginClientSource.includes("router.replace(nextPath);\n return;"), - "email/password terminal login must send non-members to the checkout account page instead of blindly returning to /terminal", + "email/password terminal login must only enter /terminal after confirmed active subscription; unknown or inactive users go to checkout", ); assert( authCallbackSource.includes("resolvePostAuthRedirect") && authCallbackSource.includes("TERMINAL_CHECKOUT_PATH") && authCallbackSource.includes('"/account?checkout=1"') && - authCallbackSource.includes("subscription_active === false") && + authCallbackSource.includes("subscription_active === true") && authCallbackSource.includes("NextResponse.redirect(redirectUrl)") && authCallbackSource.includes("await resolvePostAuthRedirect"), - "OAuth terminal callback must send non-members to the checkout account page instead of blindly returning to /terminal", + "OAuth terminal callback must only enter /terminal after confirmed active subscription; unknown or inactive users go to checkout", ); } diff --git a/frontend/components/landing/InstitutionalLandingPage.tsx b/frontend/components/landing/InstitutionalLandingPage.tsx index fa4dd8c1..eb2431b1 100644 --- a/frontend/components/landing/InstitutionalLandingPage.tsx +++ b/frontend/components/landing/InstitutionalLandingPage.tsx @@ -659,7 +659,7 @@ function InstitutionalLandingScreen({ locale }: { locale: LandingLocale }) { ))} {isEn ? "Subscribe monthly" : "订阅月付 Pro"} diff --git a/frontend/components/landing/LandingAuthActions.tsx b/frontend/components/landing/LandingAuthActions.tsx index e75c4549..b6cc0d18 100644 --- a/frontend/components/landing/LandingAuthActions.tsx +++ b/frontend/components/landing/LandingAuthActions.tsx @@ -126,7 +126,7 @@ export function LandingHeaderActions({ locale }: { locale: LandingLocale }) { {isEn ? "Subscribe / account" : "订阅 / 账户"}