"use client"; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { apiFetch, ApiError } from "@/lib/api"; import { AuthResponse, AuthUser, TwoFactorChallenge, clearToken, getDeviceToken, getToken, isTwoFactorChallenge, saveDeviceToken, saveToken } from "@/lib/auth"; export type AuthContextValue = { user: AuthUser | null; token: string | null; loading: boolean; login: (email: string, password: string) => Promise; verifyTwoFactor: (challengeId: string, code: string, rememberDevice: boolean) => Promise; resendTwoFactor: (challengeId: string) => Promise; register: (data: { name: string; email: string; password: string; phoneNumber?: string; notificationPreference?: "email" | "whatsapp" | "both" }) => Promise; logout: () => void; updateToken: (token: string) => void; }; const AuthContext = createContext(undefined); // The last profile the server confirmed, so a page reloaded with no connection (e.g. the // ticket scanner in offline mode) still knows who's signed in. The server checks the // token on every API call regardless, so this never grants anything by itself. const USER_CACHE_KEY = "hope_events_user_cache"; function cacheUser(u: AuthUser | null) { try { if (u) localStorage.setItem(USER_CACHE_KEY, JSON.stringify(u)); else localStorage.removeItem(USER_CACHE_KEY); } catch { /* storage unavailable */ } } function cachedUser(): AuthUser | null { try { const raw = localStorage.getItem(USER_CACHE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } export function AuthProvider({ children }: { children: React.ReactNode }) { const [token, setToken] = useState(null); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const init = async () => { try { const t = getToken(); if (t) { // Validate the stored token by fetching the user profile. // Only update React state after a successful response so we never // hold a half-initialised (token set, user null) state. const me = await apiFetch("/api/users/profile", { authToken: t }); setToken(t); setUser(me); cacheUser(me); } } catch (e) { // Only wipe the stored token when the server explicitly rejects it (401). // Transient failures (network down, 5xx) should NOT log the user out — the // token stays in localStorage so the next page load can retry. if (e instanceof ApiError && e.status === 401) { clearToken(); cacheUser(null); setToken(null); setUser(null); } else { // No connection: carry on as the last confirmed user. const t = getToken(); const cached = cachedUser(); if (t && cached) { setToken(t); setUser(cached); } } } finally { setLoading(false); } }; init(); }, []); const startSession = useCallback((res: AuthResponse) => { const { token, deviceToken, ...loggedInUser } = res; if (deviceToken) saveDeviceToken(deviceToken); saveToken(token); setToken(token); setUser(loggedInUser); cacheUser(loggedInUser); return loggedInUser; }, []); const login = useCallback(async (email: string, password: string) => { const res = await apiFetch("/api/users/login", { method: "POST", body: { email, password, deviceToken: getDeviceToken() || undefined }, }); // 2FA accounts get a challenge back instead of a token — nothing is saved // until verifyTwoFactor exchanges the emailed/WhatsApp code for one. if (isTwoFactorChallenge(res)) return res; return startSession(res as AuthResponse); }, [startSession]); const verifyTwoFactor = useCallback(async (challengeId: string, code: string, rememberDevice: boolean) => { const res = await apiFetch("/api/users/login/2fa", { method: "POST", body: { challengeId, code, rememberDevice }, }); return startSession(res); }, [startSession]); const resendTwoFactor = useCallback((challengeId: string) => { return apiFetch("/api/users/login/2fa/resend", { method: "POST", body: { challengeId }, }); }, []); const register = useCallback(async (data: { name: string; email: string; password: string; phoneNumber?: string; notificationPreference?: "email" | "whatsapp" | "both" }) => { const res = await apiFetch("/api/users", { method: "POST", body: data, }); const { token, ...registeredUser } = res; saveToken(token); setToken(token); setUser(registeredUser); }, []); const logout = useCallback(() => { clearToken(); cacheUser(null); setToken(null); setUser(null); }, []); // Some endpoints (profile update, password change, session revocation) issue a // fresh token mid-session. Persist it AND update in-memory state so the app // keeps using a valid token without requiring a page refresh. const updateToken = useCallback((t: string) => { saveToken(t); setToken(t); }, []); const value = useMemo( () => ({ user, token, loading, login, verifyTwoFactor, resendTwoFactor, register, logout, updateToken }), [loading, login, logout, register, resendTwoFactor, token, updateToken, user, verifyTwoFactor] ); return {children}; } export function useAuthContext() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuthContext must be used within AuthProvider"); return ctx; }