// src/api/client.ts import axios, { AxiosError } from 'axios'; import { Platform } from 'react-native'; import * as SecureStore from 'expo-secure-store'; import AsyncStorage from '@react-native-async-storage/async-storage'; const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://maia.depaoli.id.au/api'; const ACCESS_TOKEN_KEY = 'maia_access_token'; const REFRESH_TOKEN_KEY = 'maia_refresh_token'; console.log("Using API Base URL:", API_BASE_URL); // Helper functions for storage const storeToken = async (key: string, token: string): Promise => { if (Platform.OS === 'web') { await AsyncStorage.setItem(key, token); } else { await SecureStore.setItemAsync(key, token); } }; const getToken = async (key: string): Promise => { if (Platform.OS === 'web') { return await AsyncStorage.getItem(key); } else { return await SecureStore.getItemAsync(key).catch(() => null); } }; const deleteToken = async (key: string): Promise => { if (Platform.OS === 'web') { await AsyncStorage.removeItem(key); } else { await SecureStore.deleteItemAsync(key).catch(() => {}); } }; const apiClient = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, timeout: 10000, // Remove withCredentials from default config if not needed globally // ...(Platform.OS === 'web' ? { withCredentials: true } : {}), }); // --- Request Interceptor --- apiClient.interceptors.request.use( async (config) => { const token = await getToken(ACCESS_TOKEN_KEY); // Use helper if (token) { config.headers.Authorization = `Bearer ${token}`; } console.log('[API Client] Starting Request', config.method?.toUpperCase(), config.url); return config; }, (error) => { console.error('[API Client] Request Setup Error:', error); return Promise.reject(error); } ); // --- Modified Response Interceptor --- apiClient.interceptors.response.use( (response) => { // Success case return response; }, async (error: AxiosError) => { // Explicitly type error as AxiosError const originalRequest = error.config; if (error.response && originalRequest) { console.error('[API Client] Response Error Status:', error.response.status); console.error('[API Client] Response Error Data:', error.response.data); if (error.response.status === 401) { console.warn('[API Client] Unauthorized (401). Token might be expired or invalid.'); // Prevent refresh loops if the refresh endpoint itself returns 401 if (originalRequest.url === '/auth/refresh') { console.error('[API Client] Refresh token attempt failed with 401. Clearing tokens.'); await deleteToken(ACCESS_TOKEN_KEY); await deleteToken(REFRESH_TOKEN_KEY); // Clear refresh token too delete apiClient.defaults.headers.common['Authorization']; // TODO: Trigger logout flow in UI return Promise.reject(error); } // Check if we haven't already tried to refresh for this request // Need to declare _retry on AxiosRequestConfig interface or use a different check // Using a simple check here, consider a more robust solution if needed if (!(originalRequest as any)._retry) { (originalRequest as any)._retry = true; try { const storedRefreshToken = await getToken(REFRESH_TOKEN_KEY); if (!storedRefreshToken) { console.error('[API Client] No refresh token found to attempt refresh.'); await deleteToken(ACCESS_TOKEN_KEY); // Clear potentially invalid access token // TODO: Trigger logout flow in UI return Promise.reject(error); // Reject if no refresh token } console.log('[API Client] Attempting token refresh...'); const refreshResponse = await apiClient.post('/auth/refresh', { refresh_token: storedRefreshToken }, // Send token in body { headers: { 'Content-Type': 'application/json' }, } ); if (refreshResponse.status === 200) { const newAccessToken = refreshResponse.data?.access_token; // Expecting only access token if (newAccessToken) { console.log('[API Client] Token refreshed successfully.'); await storeToken(ACCESS_TOKEN_KEY, newAccessToken); // Store new access token // Update the default Authorization header for subsequent requests apiClient.defaults.headers.common['Authorization'] = `Bearer ${newAccessToken}`; // Update the Authorization header in the original request config if (originalRequest.headers) { originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; } // Retry the original request with the new access token return apiClient(originalRequest); } else { console.error('[API Client] Invalid token structure received during refresh:', refreshResponse.data); throw new Error('Invalid access token received from server during refresh.'); } } else { // Handle non-200 responses from refresh endpoint if necessary console.error(`[API Client] Token refresh endpoint returned status ${refreshResponse.status}`); throw new Error(`Refresh failed with status ${refreshResponse.status}`); } } catch (refreshError: any) { console.error('[API Client] Token refresh failed:', refreshError); // Clear tokens if refresh fails await deleteToken(ACCESS_TOKEN_KEY); await deleteToken(REFRESH_TOKEN_KEY); delete apiClient.defaults.headers.common['Authorization']; // TODO: Trigger logout flow in UI // Propagate the refresh error instead of the original 401 return Promise.reject(refreshError); } } else { console.warn('[API Client] Already retried this request. Not attempting refresh again.'); } // If retry flag was already set, or if refresh attempt failed and fell through, // we might still end up here. Ensure tokens are cleared if a 401 persists. console.log('[API Client] Clearing tokens due to persistent 401 or failed refresh.'); await deleteToken(ACCESS_TOKEN_KEY); await deleteToken(REFRESH_TOKEN_KEY); delete apiClient.defaults.headers.common['Authorization']; // TODO: Trigger logout flow in UI } // End of 401 handling } else if (error.request) { console.error('[API Client] Network Error or No Response:', error.message); if (error.message.toLowerCase().includes('network error') && Platform.OS === 'web') { console.warn('[API Client] Hint: A "Network Error" on web often masks a CORS issue. Check browser console & backend CORS config.'); } } else { console.error('[API Client] Request Setup Error (Interceptor):', error.message); } return Promise.reject(error); } ); export default apiClient; // Add functions to manage tokens globally if needed by UI components export const storeTokens = async (accessToken: string, refreshToken?: string) => { await storeToken(ACCESS_TOKEN_KEY, accessToken); if (refreshToken) { await storeToken(REFRESH_TOKEN_KEY, refreshToken); } apiClient.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`; // Update client instance }; export const clearTokens = async () => { await deleteToken(ACCESS_TOKEN_KEY); await deleteToken(REFRESH_TOKEN_KEY); delete apiClient.defaults.headers.common['Authorization']; // Clear client instance header }; export const getRefreshToken = () => getToken(REFRESH_TOKEN_KEY); export const getAccessToken = () => getToken(ACCESS_TOKEN_KEY);