From 3a012d46bf3e23345d301cf3b652a71ef382cc8d Mon Sep 17 00:00:00 2001 From: yuanzonghao Date: Mon, 15 Jun 2026 14:32:04 +0800 Subject: [PATCH] fix(auth): harden snapshot paths per PR agent review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address two suggestions from the PR agent review: 1. lib/authResume.ts — catch isAuthed() exceptions in consumeResumeSnapshot. The network/timeout path now returns null (snapshot already removed earlier to prevent the play-page bootstrap's retryBootstrap loop from re-entering this path). Document the intentional removeItem-before-isAuthed ordering. 2. components/AuthModal.tsx — wrap onBeforeOAuth in try-catch so a snapshot failure (e.g. sessionStorage blocked in privacy mode) does not abort the OAuth flow and leave the UI stuck in loading. --- components/AuthModal.tsx | 9 ++++++++- lib/authResume.ts | 19 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/components/AuthModal.tsx b/components/AuthModal.tsx index 46df21b..3ea4c48 100644 --- a/components/AuthModal.tsx +++ b/components/AuthModal.tsx @@ -40,7 +40,14 @@ export function AuthModal({ setError(""); // Snapshot before navigating away — the redirect below unmounts the app, // so any host state must be persisted to sessionStorage *now*. - onBeforeOAuth?.(); + // Non-fatal: if the snapshot fails (e.g. sessionStorage is blocked in + // privacy mode), the OAuth flow still proceeds — the user just won't + // have their in-progress state restored on return. + try { + onBeforeOAuth?.(); + } catch { + /* snapshot failure is non-fatal */ + } const supabase = createClient(); const { error: oauthError } = await supabase.auth.signInWithOAuth({ provider, diff --git a/lib/authResume.ts b/lib/authResume.ts index 8def69d..b2358eb 100644 --- a/lib/authResume.ts +++ b/lib/authResume.ts @@ -59,11 +59,28 @@ export function writeResumeSnapshot( // login doesn't resurrect a half-flow). Always removes the entry — either it's // consumed here, or it's stale and must not linger. Returns null when there's // nothing to resume, the user isn't signed in, or the payload is corrupt. +// +// `removeItem` intentionally runs before `isAuthed()` so that a network error +// during the auth check does not leave a zombie snapshot behind. Without this +// ordering, callers that guard on the snapshot's presence (play-page bootstrap) +// would re-enter this path on every effect cycle, producing an infinite retry +// loop. Dropping the snapshot on a transient network glitch is an acceptable +// trade-off — the worst case is the user lands on the first scene instead of +// resuming mid-story, which is the same experience as before this feature. export async function consumeResumeSnapshot(key: string): Promise { const raw = sessionStorage.getItem(key); if (!raw) return null; sessionStorage.removeItem(key); - if (!(await isAuthed())) return null; + let authed: boolean; + try { + authed = await isAuthed(); + } catch { + // Network / unexpected error during auth check. Snapshot already removed + // (prevents the caller's retry loop); return null so callers fall back to + // their default path (normal bootstrap). + return null; + } + if (!authed) return null; try { return JSON.parse(raw) as T; } catch {