import { ReactNode, createContext, useContext, useEffect, useState, useRef, } from "react"; import * as WebBrowser from "expo-web-browser"; import { getApiUrl, baseApi } from "@/store/api/baseApi"; import { getToken, setToken, loadPersistedToken } from "@/auth/token"; import { store } from "@/store/index"; import { User } from "@/store/models/User.model"; WebBrowser.maybeCompleteAuthSession(); type AuthContextType = { user: User | null; token: string | null; loading: boolean; bootstrapping: boolean; isAuthenticated: boolean; login: (email: string, password: string) => Promise; register: (p: { firstName: string; lastName: string; email: string; password: string; }) => Promise; logout: () => Promise; }; const AuthContext = createContext(undefined); export const useAuthContext = () => { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuthContext must be used within AuthProvider"); return ctx; }; export type AuthProviderProps = { children: ReactNode; }; export const AuthProvider = ({ children }: AuthProviderProps) => { const [user, setUser] = useState(null); const [tokenState, setTokenState] = useState(getToken()); const [bootstrapping, setBootstrapping] = useState(true); const [loading, setLoading] = useState(false); const loginInFlightRef = useRef(false); const loginPromiseRef = useRef | null>(null); const isAuthenticated = !!tokenState && !!user; const syncUser = async (jwt?: string | null) => { const useToken = jwt ?? tokenState; if (!useToken) return; try { const res = await fetch(`${getApiUrl()?.replace(/\/$/, "")}/auth/me`, { headers: { Authorization: `Bearer ${useToken}` }, }); if (!res.ok) throw new Error("Failed to fetch current user"); const u: User = await res.json(); setUser(u); } catch (e) { console.warn("/auth/me failed:", e); } }; useEffect(() => { (async () => { try { const t = await loadPersistedToken(); setTokenState(t); if (t) await syncUser(t); } finally { setBootstrapping(false); } })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const login: AuthContextType["login"] = async (email, password) => { if (loginInFlightRef.current && loginPromiseRef.current) { return await loginPromiseRef.current; } loginInFlightRef.current = true; setLoading(true); const p = (async () => { try { const res = await fetch(`${getApiUrl()?.replace(/\/$/, "")}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!res.ok) { const status = res.status; let msg = "Invalid credentials"; let serverMsg: string | undefined; try { const errData = await res.json(); serverMsg = errData?.message || errData?.error; } catch {} if (status === 403 && serverMsg && /not activated/i.test(serverMsg)) { msg = "Account is not activated. Please use your invitation link to set a password."; } else if (status >= 400 && status < 500) { msg = serverMsg || msg; } else { // For 5xx, keep default 'Invalid credentials' to avoid leaking internals msg = "Invalid credentials"; } throw new Error(msg); } const data = await res.json(); await setToken(data.token); setTokenState(data.token); setUser(data.user); // Clear/refresh caches after login for correct user context store.dispatch(baseApi.util.resetApiState()); store.dispatch(baseApi.util.invalidateTags(["Users", "Todos"])); } finally { setLoading(false); loginInFlightRef.current = false; loginPromiseRef.current = null; } })(); loginPromiseRef.current = p; return await p; }; const register: AuthContextType["register"] = async ({ firstName, lastName, email, password, }) => { setLoading(true); try { const res = await fetch( `${getApiUrl()?.replace(/\/$/, "")}/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ firstName, lastName, email, password }), } ); if (!res.ok) throw new Error("Registration failed"); const data = await res.json(); await setToken(data.token); setTokenState(data.token); setUser(data.user); // Invalidate users cache to refresh activation statuses after register store.dispatch(baseApi.util.invalidateTags(["Users"])); } finally { setLoading(false); } }; const logout: AuthContextType["logout"] = async () => { await setToken(null); setTokenState(null); setUser(null); // Clear RTK Query caches on logout to prevent stale cross-user data store.dispatch(baseApi.util.resetApiState()); }; return ( {children} ); };