import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'; import * as SecureStore from 'expo-secure-store'; import { API_BASE_URL, SECURE_STORE_ACCESS_TOKEN, SECURE_STORE_REFRESH_TOKEN } from '@shared/constants'; /** * Cliente HTTP centralizado. * * - Injeta o Bearer token automaticamente em todas as requisições. * - Intercepta 401 para tentar refresh do token automaticamente. * - Garante que todas as chamadas apontem para a base URL da API. */ const httpClient: AxiosInstance = axios.create({ baseURL: API_BASE_URL, timeout: 15_000, headers: { 'Content-Type': 'application/json', }, }); // ── Interceptor de request: anexa Bearer token ────────────── httpClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => { const token = await SecureStore.getItemAsync(SECURE_STORE_ACCESS_TOKEN); if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // ── Interceptor de response: refresh automático em 401 ────── let isRefreshing = false; let pendingQueue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void; }> = []; function processQueue(error: unknown, token: string | null): void { pendingQueue.forEach(({ resolve, reject }) => { if (error) reject(error); else if (token) resolve(token); }); pendingQueue = []; } httpClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { return new Promise((resolve, reject) => { pendingQueue.push({ resolve, reject }); }).then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return httpClient(originalRequest); }); } originalRequest._retry = true; isRefreshing = true; try { const refreshToken = await SecureStore.getItemAsync(SECURE_STORE_REFRESH_TOKEN); if (!refreshToken) throw new Error('No refresh token'); const { data } = await axios.post(`${API_BASE_URL}/auth/refresh`, { refresh_token: refreshToken }); await SecureStore.setItemAsync(SECURE_STORE_ACCESS_TOKEN, data.access_token); await SecureStore.setItemAsync(SECURE_STORE_REFRESH_TOKEN, data.refresh_token); processQueue(null, data.access_token); originalRequest.headers.Authorization = `Bearer ${data.access_token}`; return httpClient(originalRequest); } catch (refreshError) { processQueue(refreshError, null); // Limpa tokens inválidos — a store de auth detecta e redireciona para login await SecureStore.deleteItemAsync(SECURE_STORE_ACCESS_TOKEN); await SecureStore.deleteItemAsync(SECURE_STORE_REFRESH_TOKEN); return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); }, ); export { httpClient };