fix(auth): close two regressions from the resume refactor
Critical: play-page bootstrap infinite loop when AUTH_ENABLED and no resume snapshot. The refactor changed the gate from `if (AUTH_ENABLED && hasSnapshot)` to `if (AUTH_ENABLED)`, so any snapshot-less /play entry (the common case — normal card/preset/custom start) entered the async branch, got null from consumeResumeSnapshot, bumped retryBootstrap, and re-ran the effect forever. Restored the peek-before-await: only enter the async resume branch when a snapshot actually exists; otherwise fall straight through to normal bootstrap. Verified via control-flow simulation across all three paths (no snapshot / snapshot + signed in / snapshot + not signed in). Major: homepage auto-started a game after a bare OAuth login. Routing persistPendingStart through AuthModal.onBeforeOAuth fired it for every OAuth redirect, including bare logins via UserChip / StyleModal onRequireAuth (where pendingAction is null and the user only wanted to sign in). Guarded the snapshot on `pendingAction === "start"` so only the mid-start flow persists; bare logins no longer resurrect the form and auto-start on return.
This commit is contained in:
+10
-4
@@ -1992,10 +1992,16 @@ export default function HomePage() {
|
|||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
// Snapshot the form the instant the OAuth redirect begins — the
|
//
|
||||||
// round-trip unmounts the page and discards in-memory state. Fires
|
// Only snapshot when the user is mid-start: the OAuth redirect also
|
||||||
// only for Google/GitHub (signInWithOAuth), not OTP.
|
// fires for bare logins (UserChip / StyleModal onRequireAuth), where
|
||||||
onBeforeOAuth={persistPendingStart}
|
// the user just wants to sign in — not kick off a game. Guarding on
|
||||||
|
// pendingAction keeps bare logins from auto-starting a session on
|
||||||
|
// return. (start() sets pendingAction="start" right before opening
|
||||||
|
// this modal.)
|
||||||
|
onBeforeOAuth={() => {
|
||||||
|
if (pendingAction === "start") persistPendingStart();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+13
-5
@@ -1478,16 +1478,24 @@ function PlayInner() {
|
|||||||
// re-bootstrapping from `?card=…` (which would restart the story). OTP
|
// re-bootstrapping from `?card=…` (which would restart the story). OTP
|
||||||
// login never writes a snapshot — its onSuccess retry keeps state
|
// login never writes a snapshot — its onSuccess retry keeps state
|
||||||
// in-memory.
|
// in-memory.
|
||||||
if (AUTH_ENABLED) {
|
//
|
||||||
// Let the async resume run; on failure (no snapshot / not signed in /
|
// Peek before awaiting: when there's no snapshot (the common case —
|
||||||
// corrupt) it relinquishes the slot so the normal bootstrap below
|
// normal card/preset/custom entry), fall straight through to the
|
||||||
// re-runs. Either way return here — the sync body must not run while the
|
// bootstrap below. Only when a snapshot exists do we enter the async
|
||||||
// OAuth return is being reconciled.
|
// gate, which itself removes the entry. This keeps the no-snapshot path
|
||||||
|
// off the retryBootstrap re-trigger loop entirely.
|
||||||
|
if (
|
||||||
|
AUTH_ENABLED &&
|
||||||
|
sessionStorage.getItem(PLAY_RESUME_KEY) !== null
|
||||||
|
) {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const snap = await consumeResumeSnapshot<PlayResumeSnapshot>(
|
const snap = await consumeResumeSnapshot<PlayResumeSnapshot>(
|
||||||
PLAY_RESUME_KEY,
|
PLAY_RESUME_KEY,
|
||||||
);
|
);
|
||||||
if (!snap) {
|
if (!snap) {
|
||||||
|
// Snapshot existed but user isn't signed in / payload corrupt →
|
||||||
|
// consumeResumeSnapshot already removed it. Relinquish the slot so
|
||||||
|
// the normal bootstrap below re-runs on the next effect cycle.
|
||||||
startedRef.current = false;
|
startedRef.current = false;
|
||||||
setRetryBootstrap((n) => n + 1);
|
setRetryBootstrap((n) => n + 1);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user