fix: avoid terminal cold-start paywall race

This commit is contained in:
2569718930@qq.com
2026-05-30 14:48:09 +08:00
parent 1c250906ed
commit c79619898a
4 changed files with 58 additions and 2 deletions
@@ -967,8 +967,13 @@ function ScanTerminalScreen() {
}
} catch {}
setProAccess(createEmptyAccess(false));
} else if (event === "TOKEN_REFRESHED" || event === "SIGNED_IN") {
} else if (
event === "INITIAL_SESSION" ||
event === "TOKEN_REFRESHED" ||
event === "SIGNED_IN"
) {
try {
if (!session?.access_token) return;
const payload = await loadAuthProfile(session?.access_token);
setProAccess((prev) => mergeAccessStateWithAuthPayload(prev, payload));
} catch {}
@@ -79,4 +79,43 @@ export async function runTests() {
cookieOnlyResult.user_id === "cookie-user",
"terminal auth bootstrap should accept an authenticated cookie profile immediately",
);
const delayedBearerSession = deferred<{ data: { session: { access_token: string } } }>();
let coldStartSettled = false;
const coldStartResultPromise = loadTerminalAuthProfile({
hasSupabasePublicEnv: true,
getSession: () => delayedBearerSession.promise,
loadAuthProfile: (accessToken) => {
const token = String(accessToken || "");
if (!token) {
return Promise.resolve({
authenticated: true,
user_id: "cookie-user",
subscription_active: null,
degraded_auth_profile: true,
});
}
return Promise.resolve({
authenticated: true,
user_id: "bearer-paid-user",
subscription_active: true,
});
},
});
coldStartResultPromise.then(() => {
coldStartSettled = true;
});
await flushMicrotasks();
assert(
coldStartSettled === false,
"terminal auth bootstrap must not show a cold-start paywall from a degraded cookie profile before bearer auth has a chance to confirm Pro",
);
delayedBearerSession.resolve({ data: { session: { access_token: "paid-token" } } });
const coldStartResult = await coldStartResultPromise;
assert(
coldStartResult.user_id === "bearer-paid-user" &&
coldStartResult.subscription_active === true,
"terminal auth bootstrap should prefer the bearer-confirmed active Pro profile over a degraded cookie profile",
);
}
@@ -76,4 +76,10 @@ export function runTests() {
signedOutBlock.indexOf("setProAccess(createEmptyAccess(false))"),
"terminal auth listener must re-check the current Supabase session before clearing access on SIGNED_OUT events",
);
assert(
dashboardSource.includes('event === "INITIAL_SESSION"') &&
dashboardSource.indexOf('event === "INITIAL_SESSION"') <
dashboardSource.indexOf('event === "TOKEN_REFRESHED"'),
"terminal auth listener must hydrate access from Supabase INITIAL_SESSION events during first navigation from the landing page",
);
}
@@ -41,6 +41,12 @@ function firstKnownProfile(cookieResult: SettledProfile, bearerResult: SettledPr
};
}
function canResolveProfileImmediately(
payload: TerminalAuthProfilePayload | null,
): payload is TerminalAuthProfilePayload {
return payload?.authenticated === true && payload.subscription_active === true;
}
export async function loadTerminalAuthProfile({
getSession,
hasSupabasePublicEnv,
@@ -56,7 +62,7 @@ export async function loadTerminalAuthProfile({
});
const resolveIfAuthenticated = (payload: TerminalAuthProfilePayload | null) => {
if (!payload?.authenticated || resolvedAuthenticated) return;
if (!canResolveProfileImmediately(payload) || resolvedAuthenticated) return;
resolvedAuthenticated = true;
resolveAuthenticated?.(payload);
};