Redirect terminal non-members to checkout after login

This commit is contained in:
2569718930@qq.com
2026-06-14 02:49:18 +08:00
parent 285191a8be
commit c4cf69c67f
3 changed files with 125 additions and 7 deletions
+42 -4
View File
@@ -4,6 +4,7 @@ import { createSupabaseRouteClient, hasSupabaseServerEnv } from "@/lib/supabase/
import { getConfiguredSiteUrl } from "@/lib/site-url"; import { getConfiguredSiteUrl } from "@/lib/site-url";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const TERMINAL_CHECKOUT_PATH = "/account?checkout=1";
function normalizeNextPath(input: string | null) { function normalizeNextPath(input: string | null) {
const fallback = "/"; const fallback = "/";
@@ -23,6 +24,14 @@ function normalizeAuthError(input: string | null) {
return raw.slice(0, 240); return raw.slice(0, 240);
} }
function isTerminalNextPath(pathname: string) {
return pathname === "/terminal" || pathname.startsWith("/terminal/");
}
type AuthProfilePayload = {
subscription_active?: boolean | null;
};
function redirectToLoginWithError({ function redirectToLoginWithError({
baseUrl, baseUrl,
error, error,
@@ -39,9 +48,9 @@ function redirectToLoginWithError({
return NextResponse.redirect(loginUrl); return NextResponse.redirect(loginUrl);
} }
async function warmSignupTrial(accessToken: string) { async function warmSignupTrial(accessToken: string): Promise<AuthProfilePayload | null> {
const token = String(accessToken || "").trim(); const token = String(accessToken || "").trim();
if (!API_BASE || !token) return; if (!API_BASE || !token) return null;
const headers = new Headers({ const headers = new Headers({
Accept: "application/json", Accept: "application/json",
@@ -55,18 +64,42 @@ async function warmSignupTrial(accessToken: string) {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 2500); const timeoutId = setTimeout(() => controller.abort(), 2500);
try { try {
await fetch(`${API_BASE.replace(/\/+$/, "")}/api/auth/me?scope=entitlement`, { const response = await fetch(`${API_BASE.replace(/\/+$/, "")}/api/auth/me?scope=entitlement`, {
cache: "no-store", cache: "no-store",
headers, headers,
signal: controller.signal, signal: controller.signal,
}); });
if (!response.ok) {
return { subscription_active: false };
}
return await response.json();
} catch { } catch {
// The account/terminal bootstrap will retry. Callback must not strand login. // The account/terminal bootstrap will retry. Callback must not strand login.
return null;
} finally { } finally {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
} }
async function resolvePostAuthRedirect({
accessToken,
baseUrl,
nextPath,
}: {
accessToken: string;
baseUrl: string;
nextPath: string;
}) {
const profile = await warmSignupTrial(accessToken);
if (!isTerminalNextPath(nextPath)) {
return new URL(nextPath, baseUrl);
}
const redirectPath =
profile?.subscription_active === false ? TERMINAL_CHECKOUT_PATH : nextPath;
return new URL(redirectPath, baseUrl);
}
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
const siteUrl = getConfiguredSiteUrl(); const siteUrl = getConfiguredSiteUrl();
if (siteUrl) { if (siteUrl) {
@@ -122,7 +155,12 @@ export async function GET(request: NextRequest) {
nextPath, nextPath,
}); });
} }
await warmSignupTrial(session?.access_token || ""); const finalRedirectUrl = await resolvePostAuthRedirect({
accessToken: session.access_token,
baseUrl,
nextPath,
});
response.headers.set("Location", finalRedirectUrl.toString());
} }
return response; return response;
+45 -3
View File
@@ -27,6 +27,40 @@ type LoginClientProps = {
initialMode?: Mode; initialMode?: Mode;
}; };
const TERMINAL_CHECKOUT_PATH = "/account?checkout=1";
function isTerminalNextPath(pathname: string) {
return pathname === "/terminal" || pathname.startsWith("/terminal/");
}
async function resolvePostLoginRedirect({
accessToken,
nextPath,
}: {
accessToken?: string | null;
nextPath: string;
}) {
if (!isTerminalNextPath(nextPath)) return nextPath;
const token = String(accessToken || "").trim();
if (!token || typeof fetch !== "function") return TERMINAL_CHECKOUT_PATH;
try {
const response = await fetch("/api/auth/me?scope=entitlement", {
cache: "no-store",
headers: {
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) return nextPath;
const payload = await response.json();
return payload?.subscription_active === false
? TERMINAL_CHECKOUT_PATH
: nextPath;
} catch {
return nextPath;
}
}
export function LoginClient({ nextPath, initialError, initialMode }: LoginClientProps) { export function LoginClient({ nextPath, initialError, initialMode }: LoginClientProps) {
const router = useRouter(); const router = useRouter();
const { locale } = useI18n(); const { locale } = useI18n();
@@ -214,7 +248,7 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
try { try {
const supabase = getSupabaseBrowserClient(); const supabase = getSupabaseBrowserClient();
if (mode === "login") { if (mode === "login") {
const { error } = await supabase.auth.signInWithPassword({ const { data, error } = await supabase.auth.signInWithPassword({
email: email.trim(), email: email.trim(),
password, password,
}); });
@@ -222,7 +256,11 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
setErrorText(error.message); setErrorText(error.message);
return; return;
} }
router.replace(nextPath); const redirectPath = await resolvePostLoginRedirect({
accessToken: data.session?.access_token,
nextPath,
});
router.replace(redirectPath);
return; return;
} }
@@ -241,7 +279,11 @@ export function LoginClient({ nextPath, initialError, initialMode }: LoginClient
return; return;
} }
if (data.session?.user) { if (data.session?.user) {
router.replace(nextPath); const redirectPath = await resolvePostLoginRedirect({
accessToken: data.session.access_token,
nextPath,
});
router.replace(redirectPath);
return; return;
} }
setInfoText(copy.signupCheckEmail); setInfoText(copy.signupCheckEmail);
@@ -0,0 +1,38 @@
import fs from "node:fs";
import path from "node:path";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
}
export function runTests() {
const projectRoot = process.cwd();
const loginClientSource = fs.readFileSync(
path.join(projectRoot, "components", "auth", "LoginClient.tsx"),
"utf8",
);
const authCallbackSource = fs.readFileSync(
path.join(projectRoot, "app", "auth", "callback", "route.ts"),
"utf8",
);
assert(
loginClientSource.includes("TERMINAL_CHECKOUT_PATH") &&
loginClientSource.includes('"/account?checkout=1"') &&
loginClientSource.includes("resolvePostLoginRedirect") &&
loginClientSource.includes("subscription_active === false") &&
loginClientSource.includes("router.replace(redirectPath)") &&
!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",
);
assert(
authCallbackSource.includes("resolvePostAuthRedirect") &&
authCallbackSource.includes("TERMINAL_CHECKOUT_PATH") &&
authCallbackSource.includes('"/account?checkout=1"') &&
authCallbackSource.includes("subscription_active === false") &&
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",
);
}