import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import type { LoginInput, User } from '../schemas/auth' import { services } from '../services' import { session } from './session' import { activity } from './activity' import { refreshAuth } from '../api' type AuthContextValue = { user: User | null authenticated: boolean loading: boolean login: (input: LoginInput) => Promise logout: () => Promise } const AuthContext = createContext(null) export function AuthProvider({ children }: { children: ReactNode }) { const queryClient = useQueryClient() const [token, setToken] = useState(() => session.get()) useEffect(() => session.subscribe(() => setToken(session.get())), []) useEffect(() => token ? activity.start(() => { void refreshAuth().catch(() => undefined) }) : undefined, [token]) const me = useQuery({ queryKey: ['auth', 'me'], queryFn: services.auth.me, enabled: Boolean(token), retry: false, }) const value = useMemo( () => ({ user: me.data || null, authenticated: Boolean(token && me.data), loading: Boolean(token) && me.isPending, login: async (input) => { session.set(await services.auth.login(input)) activity.record(true) await queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }) }, logout: async () => { try { await services.auth.logout() } finally { session.clear() activity.clear() queryClient.clear() } }, }), [me.data, me.isPending, queryClient, token], ) return {children} } // Provider and hook intentionally share the private context. // eslint-disable-next-line react-refresh/only-export-components export function useAuth() { const value = useContext(AuthContext) if (!value) throw new Error('useAuth must be used inside AuthProvider') return value }