"use client"; import { useCallback, useEffect, useState } from "react"; import { createClient } from "@/lib/supabase/client"; import { track } from "@/lib/analytics"; import { useI18n } from "@/lib/i18n/client"; 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("pick"); const [email, setEmail] = useState(""); const [otp, setOtp] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const { t } = useI18n(); useEffect(() => { function onKeyDown(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [onClose]); const handleOAuth = useCallback( 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*. // 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, options: { redirectTo: `${window.location.origin}/auth/callback?next=${encodeURIComponent(window.location.pathname + window.location.search)}`, }, }); if (oauthError) { setError(oauthError.message); setLoading(false); } }, [onBeforeOAuth], ); const handleSendOtp = useCallback(async () => { const trimmed = email.trim(); if (!trimmed) return; setLoading(true); setError(""); const supabase = createClient(); const { error: otpError } = await supabase.auth.signInWithOtp({ email: trimmed, }); setLoading(false); if (otpError) { setError(otpError.message); } else { setStep("otp-verify"); } }, [email]); const handleVerifyOtp = useCallback(async () => { const trimmedOtp = otp.trim(); if (!trimmedOtp) return; setLoading(true); setError(""); const supabase = createClient(); const { error: verifyError } = await supabase.auth.verifyOtp({ email: email.trim(), token: trimmedOtp, type: "email", }); setLoading(false); if (verifyError) { setError(verifyError.message); } else { track("login_success", { provider: "email" }); onSuccess(); } }, [email, otp, onSuccess]); return (
e.stopPropagation()} style={{ background: "rgba(14, 10, 6, 0.92)", border: "1.5px solid rgba(175, 138, 72, 0.72)", borderRadius: "8px", backdropFilter: "blur(14px)", WebkitBackdropFilter: "blur(14px)", boxShadow: "0 10px 42px rgba(0,0,0,0.62), inset 0 1px 0 rgba(200,165,90,0.12)", }} role="dialog" aria-modal="true" aria-label={t("auth.ariaLabel")} > {/* header */}
{step === "pick" && t("auth.steps.pick")} {step === "email-input" && t("auth.steps.email")} {step === "otp-verify" && t("auth.steps.otp")}
{error && (

{error}

)} {step === "pick" && ( <>
{t("auth.or")}
)} {step === "email-input" && ( <> setEmail(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSendOtp()} placeholder={t("auth.emailPlaceholder")} autoFocus className="w-full rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-3.5 py-2.5 text-[13px] text-cream-50/90 placeholder:text-cream-50/30 outline-none focus:border-[rgba(175,138,72,0.6)]" /> )} {step === "otp-verify" && ( <>

{t("auth.codeSent", { email: email.trim() })}

setOtp(e.target.value.replace(/\D/g, ""))} onKeyDown={(e) => e.key === "Enter" && handleVerifyOtp()} placeholder={t("auth.codePlaceholder")} autoFocus className="w-full rounded-md border border-cream-50/15 bg-cream-50/[0.06] px-3.5 py-2.5 text-center text-[16px] tracking-[0.35em] text-cream-50/90 placeholder:text-cream-50/30 placeholder:tracking-normal outline-none focus:border-[rgba(175,138,72,0.6)]" /> )}
); }