import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import type { Session, User } from "@supabase/supabase-js"; import { fetchMe, type AccountInfo } from "./api"; import { getCachedAuthConfig, getSupabase, initSupabase, type PlanCard, } from "./supabase"; const ACTIVITY_EVENTS = [ "mousedown", "mousemove", "keydown", "scroll", "touchstart", "click", "visibilitychange", ] as const; type AuthContextValue = { ready: boolean; authEnabled: boolean; session: Session | null; user: User | null; account: AccountInfo | null; plans: PlanCard[]; guestMaxWords: number; sessionIdleMinutes: number; idleSignedOut: boolean; clearIdleNotice: () => void; refreshAccount: () => Promise; setAccount: (account: AccountInfo | null) => void; signInWithPassword: (email: string, password: string) => Promise; signUp: (email: string, password: string) => Promise; signInWithGoogle: () => Promise; signOut: () => Promise; }; const AuthContext = createContext(null); function useIdleSessionLogout( enabled: boolean, idleMinutes: number, hasSession: boolean, onIdle: () => void, ) { const timerRef = useRef(null); const onIdleRef = useRef(onIdle); onIdleRef.current = onIdle; useEffect(() => { if (!enabled || !hasSession || idleMinutes <= 0) { if (timerRef.current != null) { window.clearTimeout(timerRef.current); timerRef.current = null; } return; } const ms = idleMinutes * 60 * 1000; const bump = () => { if (document.visibilityState === "hidden") return; if (timerRef.current != null) window.clearTimeout(timerRef.current); timerRef.current = window.setTimeout(() => { onIdleRef.current(); }, ms); }; bump(); for (const ev of ACTIVITY_EVENTS) { window.addEventListener(ev, bump, { passive: true }); } return () => { if (timerRef.current != null) window.clearTimeout(timerRef.current); for (const ev of ACTIVITY_EVENTS) { window.removeEventListener(ev, bump); } }; }, [enabled, hasSession, idleMinutes]); } export function AuthProvider({ children }: { children: ReactNode }) { const [ready, setReady] = useState(false); const [authEnabled, setAuthEnabled] = useState(false); const [session, setSession] = useState(null); const [account, setAccount] = useState(null); const [plans, setPlans] = useState([]); const [guestMaxWords, setGuestMaxWords] = useState(100); const [sessionIdleMinutes, setSessionIdleMinutes] = useState(30); const [idleSignedOut, setIdleSignedOut] = useState(false); const refreshAccount = useCallback(async () => { if (!authEnabled) { setAccount(null); return; } try { const me = await fetchMe(session?.access_token); setAccount(me.account); } catch { setAccount(null); } }, [authEnabled, session]); const signOut = useCallback(async () => { const sb = getSupabase(); if (!sb) return; await sb.auth.signOut(); setAccount(null); }, []); const handleIdleLogout = useCallback(async () => { setIdleSignedOut(true); await signOut(); }, [signOut]); useIdleSessionLogout( authEnabled, sessionIdleMinutes, Boolean(session), () => { void handleIdleLogout(); }, ); useEffect(() => { let unsub: (() => void) | undefined; void (async () => { const config = await initSupabase(); setAuthEnabled(config.enabled); setPlans(config.plans ?? []); setGuestMaxWords(config.guest?.max_words_per_request ?? 100); setSessionIdleMinutes(config.session_idle_minutes ?? 30); const sb = getSupabase(); if (!config.enabled || !sb) { setReady(true); return; } const { data } = await sb.auth.getSession(); setSession(data.session); const { data: listener } = sb.auth.onAuthStateChange((_event, next) => { setSession(next); if (next) setIdleSignedOut(false); }); unsub = () => listener.subscription.unsubscribe(); setReady(true); })(); return () => unsub?.(); }, []); useEffect(() => { void refreshAccount(); }, [refreshAccount]); const value = useMemo( () => ({ ready, authEnabled, session, user: session?.user ?? null, account, plans: plans.length ? plans : getCachedAuthConfig()?.plans ?? [], guestMaxWords, sessionIdleMinutes, idleSignedOut, clearIdleNotice: () => setIdleSignedOut(false), refreshAccount, setAccount, async signInWithPassword(email, password) { const sb = getSupabase(); if (!sb) throw new Error("Auth is not configured."); const { error } = await sb.auth.signInWithPassword({ email, password }); if (error) throw error; setIdleSignedOut(false); }, async signUp(email, password) { const sb = getSupabase(); if (!sb) throw new Error("Auth is not configured."); const { data, error } = await sb.auth.signUp({ email, password }); if (error) throw error; setIdleSignedOut(false); if (data.session) return "signed_in"; return "check_email"; }, async signInWithGoogle() { const sb = getSupabase(); if (!sb) throw new Error("Auth is not configured."); const { error } = await sb.auth.signInWithOAuth({ provider: "google", options: { redirectTo: window.location.origin }, }); if (error) throw error; }, async signOut() { setIdleSignedOut(false); await signOut(); await refreshAccount(); }, }), [ ready, authEnabled, session, account, plans, guestMaxWords, sessionIdleMinutes, idleSignedOut, refreshAccount, signOut, ], ); return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; }