import { useQuery, useQueryClient } from '@tanstack/react-query'; import type { CurrentUser } from '@dorfteich/shared'; import { createContext, useContext, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { ApiError, apiGet, apiPost } from '../lib/api'; interface AuthContextValue { user: CurrentUser | null; isLoading: boolean; /** Re-reads /auth/me — call after login or profile changes. */ refresh: () => Promise; logout: () => Promise; } const AuthContext = createContext(null); // The last known signed-in user, cached so the app stays usable offline after a // reload (issue #38). The session cookie is still the real authority — this is // only a fallback while `/auth/me` is unreachable, and is revalidated (or // cleared on a 401) as soon as the network returns. const CURRENT_USER_KEY = 'dorfteich:current-user'; function readCachedUser(): CurrentUser | null { try { const raw = localStorage.getItem(CURRENT_USER_KEY); return raw ? (JSON.parse(raw) as CurrentUser) : null; } catch { return null; } } function writeCachedUser(user: CurrentUser | null): void { try { if (user) localStorage.setItem(CURRENT_USER_KEY, JSON.stringify(user)); else localStorage.removeItem(CURRENT_USER_KEY); } catch { // Best-effort: ignore quota / disabled storage. } } async function fetchCurrentUser(): Promise { try { const user = await apiGet('/auth/me'); writeCachedUser(user); return user; } catch (error) { if (error instanceof ApiError && error.status === 401) { writeCachedUser(null); return null; } throw error; } } export function AuthProvider({ children }: { children: React.ReactNode }): React.JSX.Element { const queryClient = useQueryClient(); const { i18n } = useTranslation(); const query = useQuery({ queryKey: ['auth', 'me'], queryFn: fetchCurrentUser, retry: false }); // Offline (e.g. after a reload), `/auth/me` can't resolve; fall back to the // cached user so the app stays signed in until the network returns (#38). const cachedUser = query.data === undefined && !navigator.onLine ? readCachedUser() : null; const user = query.data ?? cachedUser; // The profile locale wins over browser detection (issue #17). const userLocale = user?.locale; useEffect(() => { if (userLocale && i18n.language !== userLocale) { void i18n.changeLanguage(userLocale); } }, [userLocale, i18n]); const value: AuthContextValue = { user, isLoading: query.isPending && !cachedUser, refresh: () => queryClient.invalidateQueries({ queryKey: ['auth', 'me'] }), logout: async () => { await apiPost('/auth/logout'); writeCachedUser(null); queryClient.setQueryData(['auth', 'me'], null); queryClient.clear(); }, }; return {children}; } export function useAuth(): AuthContextValue { const context = useContext(AuthContext); if (!context) throw new Error('useAuth requires an ancestor'); return context; }