"use client"; /** * AuthGate — a global "require login before continuing" prompt. * * Why a gate at all? When an unauthenticated user clicks something that * needs them to be logged in (Bookmark, Save to collection, etc.) we want to * * 1. Show a login/signup modal *on the same page* (no redirect). * 2. Let them finish the original action right after they log in. * 3. Keep their URL — bookmarks, scroll position, selection — untouched. * * The trick is the imperative `requireAuth()` call: it returns a Promise that * resolves `true` once the user is authenticated, or `false` if they close * the modal without doing so. Calling code reads like a sync check: * * if (!(await requireAuth())) return; * // ... proceed with the protected API call * * Implementation sketch: * - We hold a single "pending" promise resolver in a ref so a follow-on * call to requireAuth (e.g. from another button) hooks onto the same * modal session. * - When AuthProvider reports a user (via useAuth) and the modal is open, * we resolve the promise(s) with `true` and close. * - When the modal closes without a user, we resolve with `false`. */ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react"; import { useAuth } from "./auth-provider"; import { AuthModal } from "./auth-modal"; type AuthGateValue = { /** Returns true if the user is (or becomes) authenticated; false if they dismiss. */ requireAuth: () => Promise; /** Force-open the modal regardless of auth state — for explicit "Log in" buttons. */ openModal: () => void; }; const AuthGateContext = createContext(null); export function AuthGateProvider({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const [open, setOpen] = useState(false); const resolversRef = useRef<((v: boolean) => void)[]>([]); const requireAuth = useCallback(async () => { // Fast path: already logged in. if (user) return true; setOpen(true); return new Promise((resolve) => { resolversRef.current.push(resolve); }); }, [user]); const openModal = useCallback(() => { if (!user) setOpen(true); }, [user]); // Auto-resolve pending promises the moment the user becomes authenticated. // This is what flips us from "modal open, waiting" to "modal closed, action // continues" without any extra wiring at the call site. useEffect(() => { if (user && (open || resolversRef.current.length > 0)) { const pending = resolversRef.current; resolversRef.current = []; // Intentional: react to the user becoming authenticated by closing the // gate and resolving the queued promises. // eslint-disable-next-line react-hooks/set-state-in-effect setOpen(false); pending.forEach((r) => r(true)); } }, [user, open]); const onDismiss = useCallback(() => { const pending = resolversRef.current; resolversRef.current = []; setOpen(false); pending.forEach((r) => r(false)); }, []); const value = useMemo( () => ({ requireAuth, openModal }), [requireAuth, openModal], ); return ( {children} {/* Render the modal only when we genuinely need it. `!user` guards against the brief window during which the AuthProvider hasn't finished hydrating; we'd rather wait than flash the modal. */} {open && !user && !loading ? : null} ); } export function useAuthGate(): AuthGateValue { const ctx = useContext(AuthGateContext); if (!ctx) { throw new Error("useAuthGate must be used inside "); } return ctx; }