diff --git a/app/page.tsx b/app/page.tsx index 97db554..2833abf 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1992,10 +1992,16 @@ export default function HomePage() { start(); } }} - // Snapshot the form the instant the OAuth redirect begins — the - // round-trip unmounts the page and discards in-memory state. Fires - // only for Google/GitHub (signInWithOAuth), not OTP. - onBeforeOAuth={persistPendingStart} + // + // Only snapshot when the user is mid-start: the OAuth redirect also + // fires for bare logins (UserChip / StyleModal onRequireAuth), where + // 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(); + }} /> )} diff --git a/app/play/page.tsx b/app/play/page.tsx index 00db2d7..c2db30c 100644 --- a/app/play/page.tsx +++ b/app/play/page.tsx @@ -1478,16 +1478,24 @@ function PlayInner() { // re-bootstrapping from `?card=…` (which would restart the story). OTP // login never writes a snapshot — its onSuccess retry keeps state // in-memory. - if (AUTH_ENABLED) { - // Let the async resume run; on failure (no snapshot / not signed in / - // corrupt) it relinquishes the slot so the normal bootstrap below - // re-runs. Either way return here — the sync body must not run while the - // OAuth return is being reconciled. + // + // Peek before awaiting: when there's no snapshot (the common case — + // normal card/preset/custom entry), fall straight through to the + // bootstrap below. Only when a snapshot exists do we enter the async + // 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 () => { const snap = await consumeResumeSnapshot( PLAY_RESUME_KEY, ); 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; setRetryBootstrap((n) => n + 1); return;