"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react"; import { auth, type User } from "@/lib/auth"; import { feed } from "@/lib/social"; /** * AuthProvider holds the current-user state and exposes the auth actions * to the rest of the app. * * On mount it calls /auth/me to hydrate. Until that completes, `loading` * is true and consumers should render a placeholder rather than assuming * the user is logged out. * * After a successful login/signup/logout the provider re-fetches /auth/me * to keep state in sync with the cookie. */ /** * LogInResult is either a completed session (regular case) or a signal * that the BFF wants a one-time code before completing the login. Callers * should branch on `kind` and, for `otp_required`, present an OTP field * and call `verifyLoginOTP`. */ export type LogInResult = | { kind: "session"; user: User } | { kind: "otp_required" }; type AuthContextValue = { user: User | null; loading: boolean; signUp: (email: string, password: string, name: string) => Promise; logIn: (email: string, password: string) => Promise; verifyLoginOTP: (email: string, password: string, otp: string) => Promise; logOut: () => Promise; refresh: () => Promise; /** Replace the user state without a network round-trip (after profile edit). */ setUser: (u: User | null) => void; }; const AuthContext = createContext(null); export function AuthProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const refresh = useCallback(async () => { try { const me = await auth.me(); setUser(me); } finally { setLoading(false); } }, []); useEffect(() => { void refresh(); }, [refresh]); const signUp = useCallback(async (email: string, password: string, name: string) => { const u = await auth.signUp(email, password, name); setUser(u); return u; }, []); const logIn = useCallback(async (email: string, password: string) => { const res = await auth.logIn(email, password); if (res.kind === "session") { setUser(res.user); } // For `otp_required` we deliberately leave `user` null so the rest of // the app keeps showing the signed-out state until VerifyLoginOTP // completes. return res; }, []); const verifyLoginOTP = useCallback( async (email: string, password: string, otp: string) => { const u = await auth.verifyLoginOTP(email, password, otp); setUser(u); return u; }, [], ); const logOut = useCallback(async () => { // Drop the home-feed seen-set first, while the auth cookie is still set // (the reset endpoint is auth-gated). Best-effort — a stale set would // otherwise expire on its own TTL. await feed.reset().catch(() => {}); await auth.logOut(); setUser(null); }, []); const value = useMemo( () => ({ user, loading, signUp, logIn, verifyLoginOTP, logOut, refresh, setUser, }), [user, loading, signUp, logIn, verifyLoginOTP, logOut, refresh], ); return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error("useAuth must be used inside "); } return ctx; }