import { useState, useEffect, useCallback } from "react"; // Set by lib/session.tsx once a token is available; read synchronously here so // callers never have to await SecureStore before every request. let authToken: string | null = null; // Whether the current token has already been reported dead. A signed-in screen // usually has several requests in flight — the driver dashboard poll, the call // watcher, a profile load — and a token that has expired fails all of them // within a few milliseconds. Without this latch each one would separately tear // the session down, and signing out is not free: it releases the push device // and stops the location foreground service. let unauthorizedNotified = false; let onUnauthorized: (() => void) | null = null; export const setAuthToken = (token: string) => { authToken = token; unauthorizedNotified = false; }; export const clearAuthToken = () => { authToken = null; }; /** * Registers what to do when the server rejects a token we actually sent. * * Lives at module scope for the same reason the token does: `fetchAPI` is a * plain function called from stores, effects and helpers that have no React * context to read. lib/session.tsx registers the real handler on mount. */ export const setUnauthorizedHandler = (handler: (() => void) | null) => { onUnauthorized = handler; }; /** Carries the HTTP status so callers can branch on it instead of on text. */ export class ApiError extends Error { status: number; /** * The parsed error body, when there was one. Routes that reject with a * recoverable state attach what the client needs to recover — a 409 on ride * creation carries the `ride_id` already in progress, so the screen can send * the rider there instead of just apologising. */ body: Record | null; constructor( status: number, message: string, body: Record | null = null, ) { super(message); this.name = "ApiError"; this.status = status; this.body = body; } } export const fetchAPI = async (url: string, options?: RequestInit) => { try { const headers = new Headers(options?.headers); if (authToken && !headers.has("Authorization")) { headers.set("Authorization", `Bearer ${authToken}`); } // Only requests that carried a token can tell us anything about that // token. Signing in is itself a 401 when the password is wrong, and that // request is unauthenticated by definition — treating it as a dead session // would sign the user out of the account they are in the middle of // signing in to. const authenticated = headers.has("Authorization"); const response = await fetch(url, { ...options, headers }); if (!response.ok) { // Every route answers with `{ error }`; keep that text so the UI can show // what actually went wrong instead of guessing from a status code. const body = await response.json().catch(() => null); // The token is gone or expired. Callers still get the ApiError — a // screen may want to stop polling or hide a spinner — but none of them // can recover from this one, and leaving the session in place is what // let an expired token look like a missing driver profile. if (response.status === 401 && authenticated && !unauthorizedNotified) { unauthorizedNotified = true; onUnauthorized?.(); } throw new ApiError( response.status, body?.error ?? `Request failed with status ${response.status}.`, body, ); } return await response.json(); } catch (error) { console.error(`Fetch error: ${url}`, error); throw error; } }; export const useFetch = (url: string, options?: RequestInit) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { const result = await fetchAPI(url, options); setData(result.data); } catch (err) { setError((err as Error).message); } finally { setLoading(false); } }, [url, options]); useEffect(() => { fetchData(); }, [fetchData]); return { data, loading, error, refetch: fetchData }; };