import { ReactNode, createContext, useContext, useEffect, useState, } 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 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) => { setLoading(true); 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"; try { const errData = await res.json(); msg = errData?.message || errData?.error || msg; } catch { // non-JSON response, keep default msg } // Only replace the message for explicit 'not activated' cases if (status === 403 && /not activated/i.test(msg)) { msg = "Account is not activated. Please use your invitation link to set a password."; } 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); } }; 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} ); };