{"release_id":"01a1c502-4ad1-0000-ec62-adc54d3c69aa","chunk_id":"01a1c502-4c3c-7fd1-a47a-18d9ddac3b2c","file":"DopJZpEb.mjs","ignoreList":[],"mappings":"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","names":["oauthInFlight","$.state","oauthError","STALLED_COPY","WATCHDOG_MS","warmClient","warmSupabase","__vitePreload","m","watchdog","clearWatchdog","armWatchdog","provider","$.set","analytics.trackOAuthFailed","$$props","$.user_effect","onMount","onShow","$.get","onHide","signInWithProvider","page","goto","analytics.trackOAuthStart","result","reason","t","err","nextWithFlag","redirectTo","supabase","error","div","$.first_child","fragment","button","$.child","span","$.sibling","$.reset","node","p","root_1","$$render","consequent","div_1","span_1"],"sources":["../../../../../../src/lib/components/auth/OAuthButtonRow.svelte"],"sourcesContent":["<script lang=\"ts\">\n\timport { t } from '$lib/i18n';\n\timport { onMount } from 'svelte';\n\timport * as analytics from '$lib/analytics';\n\timport { page } from '$app/state';\n\timport { goto } from '$app/navigation';\n\t// Type-only import — erased at compile time, so supabase-js stays out of\n\t// the bundle until warmSupabase() dynamically imports it.\n\timport type { createSupabaseBrowserClient } from '$lib/supabase';\n\ttype SupabaseBrowserClient = ReturnType<typeof createSupabaseBrowserClient>;\n\n\t// RAT-165: shared OAuth button row used above every email gate (the main\n\t// /auth/sign-in route plus the inline gates inside /rate, /d, /d/[code],\n\t// /a/submit). Lazy-imports supabase-js so the email step keeps its\n\t// first-paint cost down (RAT-158 pattern).\n\t//\n\t// `next` is forwarded to /auth/callback so we land back on the right flow\n\t// after the provider redirect. Inline gates pass their own deeplink (e.g.\n\t// `/rate?...&resume=1`); the bare /auth/sign-in passes its `data.next`.\n\t//\n\t// Reddit was originally in this row but their Responsible Builder Policy\n\t// (Nov 2025) made app creation a manual-approval gate with ~7d turnaround\n\t// and a high rejection rate for small commercial tools. Reddit OAuth is\n\t// deferred to a separate ticket; only Google ships now.\n\n\t/** Result of a `beforeRedirect` hook. A bare boolean keeps the old contract\n\t *  (true → proceed, false → abort). The object form lets a refusal carry a\n\t *  human-readable `reason` we render under the button — so a bail-out is no\n\t *  longer silent. A refusal with no reason falls back to a generic message. */\n\ttype BeforeRedirectResult = boolean | { ok: boolean; reason?: string };\n\n\tinterface Props {\n\t\tnext: string;\n\t\t/** Optional pre-redirect hook. Inline gates use it to upload any\n\t\t *  in-flight files and persist resume state to localStorage before\n\t\t *  the page is wiped by the OAuth round trip. Return false — or\n\t\t *  `{ ok: false, reason }` to say why — to abort the redirect. */\n\t\tbeforeRedirect?: () => Promise<BeforeRedirectResult>;\n\t}\n\tlet { next, beforeRedirect }: Props = $props();\n\n\tlet oauthInFlight = $state<'google' | null>(null);\n\tlet oauthError = $state('');\n\n\t// Shared copy for the \"we couldn't hand you off to Google\" states, so the\n\t// watchdog and a reasonless refusal read the same.\n\tconst STALLED_COPY = \"google sign-in didn't finish. try again, or use email below.\";\n\n\t// Watchdog window. Armed only once `signInWithOAuth` has resolved — i.e. the\n\t// provider URL is in hand and the browser has been told to navigate. From\n\t// that point the only thing left is the unload, so 12s of no pagehide really\n\t// does mean stalled.\n\t//\n\t// It deliberately does NOT cover the two slow steps before that. Arming at\n\t// tap time (the original shape) put the window over `beforeRedirect`, which\n\t// on /d is a photo upload of up to 4 MB — routinely past 12s on mobile data.\n\t// The watchdog would then flash a failure over a succeeding flow and re-enable\n\t// the button, and a second tap would start a second upload and a second PKCE\n\t// handshake: the exact double-handshake that lands people on bad_oauth_state.\n\tconst WATCHDOG_MS = 12_000;\n\n\t// Pre-warm the supabase browser client so the click→redirect gap is\n\t// minimal: the cold supabase-js dynamic-import (a few seconds cold) is\n\t// paid off in the background after mount, not synchronously between the\n\t// tap and the redirect where a dead-looking button invites double-taps.\n\tlet warmClient: Promise<SupabaseBrowserClient> | null = null;\n\tfunction warmSupabase(): Promise<SupabaseBrowserClient> {\n\t\tif (warmClient) return warmClient;\n\t\twarmClient = import('$lib/supabase').then((m) => m.createSupabaseBrowserClient());\n\t\treturn warmClient;\n\t}\n\n\t// Escape hatch for a redirect that never navigates away. The pageshow reset\n\t// below only recovers when the browser comes *back* to this page (back\n\t// button, bfcache restore) — it does nothing if signInWithOAuth hangs, or\n\t// resolves without navigating, inside a single page load. Without this, the\n\t// button sits on \"redirecting…\" forever and the user can only give up. Armed\n\t// whenever a redirect is in flight, cleared on pagehide (a real navigation is\n\t// under way) and whenever the flight ends.\n\tlet watchdog: ReturnType<typeof setTimeout> | undefined;\n\tfunction clearWatchdog() {\n\t\tif (watchdog !== undefined) {\n\t\t\tclearTimeout(watchdog);\n\t\t\twatchdog = undefined;\n\t\t}\n\t}\n\tfunction armWatchdog(provider: 'google') {\n\t\tclearWatchdog();\n\t\twatchdog = setTimeout(() => {\n\t\t\twatchdog = undefined;\n\t\t\toauthError = STALLED_COPY;\n\t\t\t// Safe to re-enable here, unlike at tap time: nothing of ours is still\n\t\t\t// running. `beforeRedirect` finished long ago and signInWithOAuth has\n\t\t\t// returned, so a retry starts one clean flow, not a second concurrent one.\n\t\t\toauthInFlight = null;\n\t\t\tanalytics.trackOAuthFailed(provider, 'watchdog_timeout', next);\n\t\t}, WATCHDOG_MS);\n\t}\n\t// Component teardown must not leave a timer that writes state into a dead tree.\n\t$effect(() => clearWatchdog);\n\n\t// Reset the in-flight state whenever the page is shown again (back button\n\t// from Google, bfcache restore, etc.). Without this the button stays\n\t// disabled with \"redirecting…\" forever after a back nav and the user\n\t// can't retry the flow.\n\t//\n\t// If we return here with a redirect still marked in-flight, the Google\n\t// round trip was abandoned or bounced back (back button, bfcache restore,\n\t// provider/flow-state error) without completing. Surface a message instead\n\t// of silently resetting the button — a silent reset reads as a dead click,\n\t// so users double-tap and stall on the gate. `oauthInFlight` is null on a\n\t// normal fresh load, so this only fires on a genuine return mid-flow.\n\tonMount(() => {\n\t\t// Warm after first paint — keeps the email step's initial paint clean\n\t\t// (the RAT-158 reason the import was lazy) while removing the cold\n\t\t// import from the click path.\n\t\twarmSupabase();\n\t\tconst onShow = () => {\n\t\t\tif (oauthInFlight) {\n\t\t\t\toauthError = STALLED_COPY;\n\t\t\t}\n\t\t\toauthInFlight = null;\n\t\t};\n\t\t// A genuine redirect to Google fires pagehide as the page unloads —\n\t\t// disarm the watchdog so a frozen (bfcached) page can't fire it on\n\t\t// restore before pageshow resets the button.\n\t\tconst onHide = () => clearWatchdog();\n\t\twindow.addEventListener('pageshow', onShow);\n\t\twindow.addEventListener('pagehide', onHide);\n\t\treturn () => {\n\t\t\twindow.removeEventListener('pageshow', onShow);\n\t\t\twindow.removeEventListener('pagehide', onHide);\n\t\t};\n\t});\n\n\tasync function signInWithProvider(provider: 'google') {\n\t\tif (oauthInFlight) return;\n\t\t// RAT-491: re-tap guard. If a session already exists (e.g. the user\n\t\t// returned authed from a prior OAuth round trip and tapped again before\n\t\t// the gate re-rendered), a fresh handshake would burn the already-\n\t\t// consumed PKCE flow_state and bounce them to bad_oauth_state despite\n\t\t// being signed in. Don't start a new handshake — just resolve to where\n\t\t// they were headed.\n\t\tif (page.data.user) {\n\t\t\toauthError = '';\n\t\t\tawait goto(next, { invalidateAll: true });\n\t\t\treturn;\n\t\t}\n\t\toauthError = '';\n\t\toauthInFlight = provider;\n\t\tanalytics.trackOAuthStart(provider, next);\n\t\tif (beforeRedirect) {\n\t\t\ttry {\n\t\t\t\tconst result = await beforeRedirect();\n\t\t\t\tconst ok = typeof result === 'boolean' ? result : result.ok;\n\t\t\t\tif (!ok) {\n\t\t\t\t\t// Give every bail-out a voice: use the hook's reason if it\n\t\t\t\t\t// gave one, else a generic message. A `false` used to reset\n\t\t\t\t\t// the button with no explanation, which reads as a dead tap.\n\t\t\t\t\tconst reason = typeof result === 'object' ? result.reason : undefined;\n\t\t\t\t\toauthError = reason ?? t(\"couldn't prepare sign-in. try email below.\");\n\t\t\t\t\toauthInFlight = null;\n\t\t\t\t\tanalytics.trackOAuthFailed(provider, 'prepare_refused', next);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tconsole.warn('[OAuthButtonRow] beforeRedirect failed:', err);\n\t\t\t\toauthError = t(\"couldn't prepare for sign-in. try email instead.\");\n\t\t\t\toauthInFlight = null;\n\t\t\t\tanalytics.trackOAuthFailed(provider, 'prepare_error', next);\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\t// Tag the destination URL with `?oauth_resume=1` so the server-rendered\n\t\t// HTML on the destination page can show the bridge loader from the\n\t\t// first paint (no upload-screen flash before client-side resume kicks\n\t\t// in). Each gate page checks page.url.searchParams.get('oauth_resume').\n\t\tconst nextWithFlag = next + (next.includes('?') ? '&' : '?') + 'oauth_resume=1';\n\t\t// RAT-204: Supabase strictly matches `redirectTo` against the\n\t\t// allowed-redirect-URLs list, and a `?next=` query string breaks the\n\t\t// match → Supabase silently falls back to the Site URL and the\n\t\t// `next` param disappears. Stash it in a short-lived cookie instead;\n\t\t// /auth/callback's server load reads + clears it.\n\t\tdocument.cookie = `oauth_next=${encodeURIComponent(nextWithFlag)}; path=/; max-age=600; samesite=lax`;\n\t\tconst redirectTo = `${window.location.origin}/auth/callback`;\n\t\t// Mirror the try/catch AuthSheet.signInWithGoogle already has: a rejected\n\t\t// warm-import (chunk fetch failed) or a throwing signInWithOAuth would\n\t\t// otherwise escape unhandled and leave the button wedged on \"redirecting…\"\n\t\t// forever — exactly the dead-button state we're fixing.\n\t\ttry {\n\t\t\t// warmSupabase() was kicked off on mount, so this resolves near-\n\t\t\t// instantly — no cold import between tap and redirect.\n\t\t\tconst supabase = await warmSupabase();\n\t\t\tconst { error } = await supabase.auth.signInWithOAuth({\n\t\t\t\tprovider,\n\t\t\t\toptions: { redirectTo }\n\t\t\t});\n\t\t\tif (error) {\n\t\t\t\toauthError = `couldn't sign in with ${provider}. try email instead.`;\n\t\t\t\toauthInFlight = null;\n\t\t\t\tanalytics.trackOAuthFailed(provider, 'provider_error', next);\n\t\t\t} else {\n\t\t\t\t// The browser is now navigating to the provider — this is the only\n\t\t\t\t// stretch the watchdog should cover. Disarmed by pagehide as the page\n\t\t\t\t// unloads, so on a healthy flow it never fires.\n\t\t\t\tarmWatchdog(provider);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tconsole.warn('[OAuthButtonRow] signInWithOAuth failed:', err);\n\t\t\toauthError = `couldn't sign in with ${provider}. try email instead.`;\n\t\t\toauthInFlight = null;\n\t\t\tanalytics.trackOAuthFailed(provider, 'redirect_error', next);\n\t\t}\n\t}\n</script>\n\n<div class=\"mx-auto flex max-w-xs flex-col gap-2.5\">\n\t<button\n\t\ttype=\"button\"\n\t\tclass=\"flex w-full items-center justify-center gap-3 rounded-xl border border-border bg-surface px-5 py-3 text-sm font-semibold text-text transition-all hover:bg-white/[.04] disabled:opacity-50\"\n\t\tonclick={() => signInWithProvider('google')}\n\t\tdisabled={oauthInFlight !== null}\n\t>\n\t\t<svg class=\"h-4 w-4 shrink-0\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n\t\t\t<path\n\t\t\t\tfill=\"#4285F4\"\n\t\t\t\td=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1Z\"\n\t\t\t/>\n\t\t\t<path\n\t\t\t\tfill=\"#34A853\"\n\t\t\t\td=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.28-1.93-6.14-4.53H2.18v2.84A11 11 0 0 0 12 23Z\"\n\t\t\t/>\n\t\t\t<path\n\t\t\t\tfill=\"#FBBC05\"\n\t\t\t\td=\"M5.86 14.1A6.6 6.6 0 0 1 5.5 12c0-.73.13-1.44.36-2.1V7.06H2.18A11 11 0 0 0 1 12c0 1.78.42 3.46 1.18 4.94l3.68-2.84Z\"\n\t\t\t/>\n\t\t\t<path\n\t\t\t\tfill=\"#EA4335\"\n\t\t\t\td=\"M12 5.38c1.62 0 3.06.56 4.21 1.65l3.15-3.15C17.45 2.1 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.68 2.84C6.72 7.31 9.14 5.38 12 5.38Z\"\n\t\t\t/>\n\t\t</svg>\n\t\t<span>{oauthInFlight === 'google' ? t('redirecting…') : t('continue with google')}</span>\n\t</button>\n\t{#if oauthError}\n\t\t<p class=\"text-[11px] text-coral\">{oauthError}</p>\n\t{/if}\n</div>\n\n<div class=\"mx-auto mt-6 flex max-w-xs items-center gap-3\">\n\t<span class=\"h-px flex-1 bg-border\"></span>\n\t<span class=\"text-[10px] tracking-[0.2em] text-muted/50 uppercase\">{t('or email')}</span>\n\t<span class=\"h-px flex-1 bg-border\"></span>\n</div>\n"],"version":3}