fix(auth): harden snapshot paths per PR agent review
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.
This commit is contained in:
@@ -40,7 +40,14 @@ export function AuthModal({
|
|||||||
setError("");
|
setError("");
|
||||||
// Snapshot before navigating away — the redirect below unmounts the app,
|
// Snapshot before navigating away — the redirect below unmounts the app,
|
||||||
// so any host state must be persisted to sessionStorage *now*.
|
// 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 supabase = createClient();
|
||||||
const { error: oauthError } = await supabase.auth.signInWithOAuth({
|
const { error: oauthError } = await supabase.auth.signInWithOAuth({
|
||||||
provider,
|
provider,
|
||||||
|
|||||||
+18
-1
@@ -59,11 +59,28 @@ export function writeResumeSnapshot<T>(
|
|||||||
// login doesn't resurrect a half-flow). Always removes the entry — either it's
|
// 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
|
// 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.
|
// 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<T>(key: string): Promise<T | null> {
|
export async function consumeResumeSnapshot<T>(key: string): Promise<T | null> {
|
||||||
const raw = sessionStorage.getItem(key);
|
const raw = sessionStorage.getItem(key);
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
sessionStorage.removeItem(key);
|
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 {
|
try {
|
||||||
return JSON.parse(raw) as T;
|
return JSON.parse(raw) as T;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
Reference in New Issue
Block a user