213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
import axios, {
|
|
AxiosInstance,
|
|
AxiosRequestConfig,
|
|
InternalAxiosRequestConfig,
|
|
AxiosResponse,
|
|
AxiosError,
|
|
} from 'axios';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
import { Store } from '@reduxjs/toolkit';
|
|
|
|
// ─── Storage Keys ────────────────────────────────────────────────────────────
|
|
const STORAGE_KEYS = {
|
|
ACCESS_TOKEN: '@auth_access_token',
|
|
REFRESH_TOKEN: '@auth_refresh_token',
|
|
} as const;
|
|
|
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
const BASE_URL = 'https://db1d-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
|
|
|
|
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
|
export const tokenManager = {
|
|
async getAccessToken(): Promise<string | null> {
|
|
return AsyncStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
},
|
|
|
|
async getRefreshToken(): Promise<string | null> {
|
|
return AsyncStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
},
|
|
|
|
async setTokens(accessToken: string, refreshToken: string): Promise<void> {
|
|
await AsyncStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
await AsyncStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
|
},
|
|
|
|
async clearTokens(): Promise<void> {
|
|
await AsyncStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
await AsyncStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
},
|
|
};
|
|
|
|
// ─── Axios Instance ──────────────────────────────────────────────────────────
|
|
const axiosInstance: AxiosInstance = axios.create({
|
|
baseURL: BASE_URL,
|
|
timeout: 15000,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
},
|
|
});
|
|
|
|
// ─── Request Interceptor — Attach Access Token ──────────────────────────────
|
|
axiosInstance.interceptors.request.use(
|
|
async (config: InternalAxiosRequestConfig) => {
|
|
// Skip attaching token for auth endpoints (login, register, refresh)
|
|
const publicPaths = [
|
|
'/auth/otp/request',
|
|
'/auth/otp/verify',
|
|
'/auth/refresh-token',
|
|
];
|
|
const isPublic = publicPaths.some(path => config.url?.includes(path));
|
|
|
|
if (!isPublic) {
|
|
const token = await tokenManager.getAccessToken();
|
|
if (token) {
|
|
config.headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
// Debug logging (remove in production)
|
|
if (__DEV__) {
|
|
// console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);s
|
|
}
|
|
|
|
return config;
|
|
},
|
|
(error: AxiosError) => {
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
|
|
// ─── Response Interceptor — Handle Token Refresh ────────────────────────────
|
|
let isRefreshing = false;
|
|
let failedQueue: Array<{
|
|
resolve: (token: string) => void;
|
|
reject: (error: unknown) => void;
|
|
}> = [];
|
|
|
|
const processQueue = (error: unknown, token: string | null = null) => {
|
|
failedQueue.forEach(({ resolve, reject }) => {
|
|
if (error) {
|
|
reject(error);
|
|
} else if (token) {
|
|
resolve(token);
|
|
}
|
|
});
|
|
failedQueue = [];
|
|
};
|
|
|
|
axiosInstance.interceptors.response.use(
|
|
(response: AxiosResponse) => {
|
|
return response;
|
|
},
|
|
async (error: AxiosError) => {
|
|
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
|
_retry?: boolean;
|
|
};
|
|
|
|
// If 401 and we haven't already retried this request
|
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
// If already refreshing, queue this request
|
|
if (isRefreshing) {
|
|
return new Promise<AxiosResponse>((resolve, reject) => {
|
|
failedQueue.push({
|
|
resolve: (token: string) => {
|
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
resolve(axiosInstance(originalRequest));
|
|
},
|
|
reject,
|
|
});
|
|
});
|
|
}
|
|
|
|
originalRequest._retry = true;
|
|
isRefreshing = true;
|
|
|
|
try {
|
|
const refreshToken = await tokenManager.getRefreshToken();
|
|
|
|
if (!refreshToken) {
|
|
throw new Error('No refresh token available');
|
|
}
|
|
|
|
// Call refresh endpoint (uses a fresh axios call to avoid interceptor loop)
|
|
const { data } = await axios.post(`${BASE_URL}/auth/refresh-token`, {
|
|
refreshToken,
|
|
});
|
|
|
|
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
|
|
data;
|
|
|
|
await tokenManager.setTokens(newAccessToken, newRefreshToken);
|
|
|
|
// Retry all queued requests with the new token
|
|
processQueue(null, newAccessToken);
|
|
|
|
// Retry the original request
|
|
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
return axiosInstance(originalRequest);
|
|
} catch (refreshError) {
|
|
processQueue(refreshError, null);
|
|
await tokenManager.clearTokens();
|
|
|
|
// TODO: Navigate to login screen or dispatch a logout action
|
|
// e.g. store.dispatch(logout());
|
|
|
|
return Promise.reject(refreshError);
|
|
} finally {
|
|
isRefreshing = false;
|
|
}
|
|
}
|
|
|
|
// For all other errors, reject as-is
|
|
return Promise.reject(error);
|
|
},
|
|
);
|
|
let storeInstance: Store | null = null;
|
|
export const injectStore = (_store: Store | null): void => {
|
|
storeInstance = _store;
|
|
};
|
|
|
|
// ─── API Methods ─────────────────────────────────────────────────────────────
|
|
export const apiClient = {
|
|
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
const response = await axiosInstance.get<T>(url, config);
|
|
return response.data;
|
|
},
|
|
|
|
async post<T>(
|
|
url: string,
|
|
body?: unknown,
|
|
config?: AxiosRequestConfig,
|
|
): Promise<T> {
|
|
const response = await axiosInstance.post<T>(url, body, config);
|
|
return response.data;
|
|
},
|
|
|
|
async put<T>(
|
|
url: string,
|
|
body?: unknown,
|
|
config?: AxiosRequestConfig,
|
|
): Promise<T> {
|
|
const response = await axiosInstance.put<T>(url, body, config);
|
|
return response.data;
|
|
},
|
|
|
|
async patch<T>(
|
|
url: string,
|
|
body?: unknown,
|
|
config?: AxiosRequestConfig,
|
|
): Promise<T> {
|
|
const response = await axiosInstance.patch<T>(url, body, config);
|
|
return response.data;
|
|
},
|
|
|
|
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
|
const response = await axiosInstance.delete<T>(url, config);
|
|
return response.data;
|
|
},
|
|
};
|
|
|
|
// Export the raw instance if needed for advanced usage
|
|
export { axiosInstance };
|