import type { LoginInput, SafeUserProfile, SignupInput } from "@batch-cooking/shared"; import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react"; import { apiClient } from "../../api/client"; /** Shape of the auth state/actions exposed via {@link useAuth}. */ interface AuthContextValue { /** Currently authenticated profile, or `null` if no active session. */ user: SafeUserProfile | null; /** True only while the initial `/auth/me` check (on app load) is pending — lets route guards avoid a premature redirect. */ isLoading: boolean; /** Creates a profile (+ household) and updates `user` on success. Throws `ApiError` on failure. */ signup: (input: SignupInput) => Promise; /** Verifies credentials and updates `user` on success. Throws `ApiError` on failure. */ login: (input: LoginInput) => Promise; /** Ends the session and clears `user`. */ logout: () => Promise; /** Permanently deletes the current account and clears `user`. Throws `ApiError` (e.g. wrong password) on failure. */ deleteAccount: (password: string) => Promise; /** * Re-fetches the current profile and updates `user`. Needed after * anything that changes profile fields `user` carries (e.g. `dietId`) * outside of `signup`/`login` — `PATCH /profile/diet` (see * `PreferencesPage.tsx`) updates the database directly via `apiClient`, * which doesn't touch this context on its own. */ refreshUser: () => Promise; } /** React context carrying {@link AuthContextValue} — always accessed through {@link useAuth}, never directly. */ const AuthContext = createContext(null); /** * Provides authentication state to the whole app. On mount, calls * `GET /auth/me` once to restore the session from the httpOnly cookie (if * any) — this is what lets a page reload keep the user logged in. */ export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { apiClient .me() .then(setUser) // No session cookie (or it's invalid/expired) — that's the normal // state for a first-time visitor, not an error to surface. .catch(() => setUser(null)) .finally(() => setIsLoading(false)); }, []); const signup = useCallback(async (input: SignupInput) => { try { setUser(await apiClient.signup(input)); } catch (err) { // Rethrown as-is — the caller (`SignupPage`'s `handleSubmit`) is what // actually catches and displays this (see this context value's doc // comment, "Throws ApiError on failure"); this callback just isn't // allowed a bare `await` per the repo's async/try-catch convention. throw err; } }, []); const login = useCallback(async (input: LoginInput) => { try { setUser(await apiClient.login(input)); } catch (err) { throw err; // see signup()'s catch comment above } }, []); const logout = useCallback(async () => { try { await apiClient.logout(); setUser(null); } catch (err) { throw err; // see signup()'s catch comment above } }, []); const deleteAccount = useCallback(async (password: string) => { try { await apiClient.deleteAccount(password); setUser(null); } catch (err) { throw err; // see signup()'s catch comment above } }, []); const refreshUser = useCallback(async () => { try { setUser(await apiClient.me()); } catch (err) { throw err; // see signup()'s catch comment above } }, []); return ( {children} ); } /** Reads the current auth state/actions. Must be called from within an {@link AuthProvider}. */ export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error("useAuth must be used within an AuthProvider"); } return ctx; }