import axios, { type AxiosError, type InternalAxiosRequestConfig } from "axios"; import AsyncStorage from "@react-native-async-storage/async-storage"; const STORAGE_KEYS = { ACCESS_TOKEN: "access_token", REFRESH_TOKEN: "refresh_token", } as const; interface RetryConfig extends InternalAxiosRequestConfig { _retry?: boolean; } const api = axios.create({ baseURL: process.env.EXPO_PUBLIC_API_URL || "http://localhost:8000", headers: { "Content-Type": "application/json", }, }); // ── Token helpers ──────────────────────────────────────────────────── async function getAccessToken(): Promise { return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN); } async function getRefreshToken(): Promise { return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN); } async function setTokens(access: string, refresh: string): Promise { await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, access); await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refresh); } async function clearTokens(): Promise { await AsyncStorage.multiRemove([ STORAGE_KEYS.ACCESS_TOKEN, STORAGE_KEYS.REFRESH_TOKEN, ]); } // ── Request interceptor ───────────────────────────────────────────── api.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { const token = await getAccessToken(); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // ── Response interceptor: auto-refresh on 401 ─────────────────────── let isRefreshing = false; let failedQueue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void; }> = []; function processQueue(error: unknown, token: string | null = null): void { failedQueue.forEach((prom) => { if (error) { prom.reject(error); } else if (token) { prom.resolve(token); } }); failedQueue = []; } api.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as RetryConfig | undefined; if (!originalRequest) { return Promise.reject(error); } if ( error.response?.status !== 401 || originalRequest._retry || originalRequest.url?.includes("/api/auth/token/refresh/") || originalRequest.url?.includes("/api/auth/login/") || originalRequest.url?.includes("/api/auth/register/") || originalRequest.url?.includes("/api/auth/logout/") ) { return Promise.reject(error); } if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then((token) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } return api(originalRequest); }); } originalRequest._retry = true; isRefreshing = true; const refreshToken = await getRefreshToken(); if (!refreshToken) { isRefreshing = false; await clearTokens(); return Promise.reject(error); } try { const response = await axios.post( `${api.defaults.baseURL}/api/auth/token/refresh/`, { refresh: refreshToken }, ); const newAccess = response.data.access as string; const newRefresh = response.data.refresh as string; await setTokens(newAccess, newRefresh); processQueue(null, newAccess); if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${newAccess}`; } return api(originalRequest); } catch (refreshError) { processQueue(refreshError, null); await clearTokens(); return Promise.reject(refreshError); } finally { isRefreshing = false; } }, ); export { getAccessToken, getRefreshToken, setTokens, clearTokens }; export default api;