fix(play): resume in-progress game after OAuth full-page redirect

Google/GitHub OAuth is a full-page round-trip that unmounts the app and
destroys the in-memory Session (the server is stateless). Returning to
/play?card=m0 re-bootstrapped from the first-act JSON, restarting the
story from scene 1 — the user lost all progress. OTP login kept state
in-memory (no redirect) and was unaffected.

Mirror the homepage 89a5c54 OAuth state-loss fix: snapshot the exact
scene/beat/visited-beats/orientation/image into sessionStorage just
before the redirect, then restore it on mount after the round-trip
(verified signed in). Re-resolve the remote image URL to a fresh blob
(blob: URLs are revoked on unmount). The pending action that hit the
401 (choice / freeform / background-click) is replayed once the restored
state commits, so the player lands exactly where they were headed.

Quota fallback drops the user-uploaded style-reference image (~100KB)
and retries; voices are kept (continuity over rare quota miss). Failure
to restore (corrupt snapshot / not signed in) relinquishes the bootstrap
slot and falls back to normal card/preset/custom start instead of a
blank loading screen.

AuthModal gains an optional onBeforeOAuth callback fired synchronously
before signInWithOAuth navigates away (sessionStorage.setItem is sync).
This commit is contained in:
yuanzonghao
2026-06-15 13:18:56 +08:00
parent 7f263b2b14
commit 99ad8d111e
2 changed files with 219 additions and 10 deletions
+11 -1
View File
@@ -9,9 +9,16 @@ type AuthStep = "pick" | "email-input" | "otp-verify";
export function AuthModal({
onClose,
onSuccess,
onBeforeOAuth,
}: {
onClose: () => void;
onSuccess: () => void;
// Fires synchronously before the OAuth full-page redirect (signInWithOAuth
// navigates the browser away, unmounting the whole React tree). Hosts that
// need to survive the round-trip (e.g. play page carrying in-memory game
// state) snapshot into sessionStorage here — sessionStorage.setItem is
// synchronous, so it completes before the navigation begins.
onBeforeOAuth?: () => void;
}) {
const [step, setStep] = useState<AuthStep>("pick");
const [email, setEmail] = useState("");
@@ -31,6 +38,9 @@ export function AuthModal({
async (provider: "google" | "github") => {
setLoading(true);
setError("");
// Snapshot before navigating away — the redirect below unmounts the app,
// so any host state must be persisted to sessionStorage *now*.
onBeforeOAuth?.();
const supabase = createClient();
const { error: oauthError } = await supabase.auth.signInWithOAuth({
provider,
@@ -43,7 +53,7 @@ export function AuthModal({
setLoading(false);
}
},
[],
[onBeforeOAuth],
);
const handleSendOtp = useCallback(async () => {