fix(android): Use CapacitorHttp for ALL API requests in native mode
This commit is contained in:
parent
813e4ac70c
commit
36705cd257
1 changed files with 229 additions and 99 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { CapacitorHttp, HttpOptions, HttpResponse } from '@capacitor/core';
|
||||
|
||||
// Detect if running in Capacitor native environment
|
||||
const isCapacitor = typeof window !== 'undefined' && !!(window as any).Capacitor?.isNativePlatform?.();
|
||||
const isCapacitor = Capacitor.isNativePlatform();
|
||||
|
||||
// Production API URL for native app, relative URL for web
|
||||
const API_BASE_URL = isCapacitor
|
||||
|
|
@ -30,110 +32,238 @@ const processQueue = (error: unknown = null) => {
|
|||
failedQueue = [];
|
||||
};
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 30000, // 30 second timeout
|
||||
});
|
||||
// Helper to get auth token
|
||||
const getAuthToken = (): string | null => {
|
||||
return localStorage.getItem('token');
|
||||
};
|
||||
|
||||
// Request interceptor - add auth token
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
// CapacitorHttp-based API client for native platforms
|
||||
class CapacitorApiClient {
|
||||
private baseURL: string;
|
||||
|
||||
constructor(baseURL: string) {
|
||||
this.baseURL = baseURL;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor - handle errors and retries
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retryCount?: number };
|
||||
private async request<T>(method: string, url: string, data?: unknown, retryCount = 0): Promise<{ data: T; status: number }> {
|
||||
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;
|
||||
const token = getAuthToken();
|
||||
|
||||
const options: HttpOptions = {
|
||||
url: fullUrl,
|
||||
method: method as HttpOptions['method'],
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
...(data ? { data } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
const response: HttpResponse = await CapacitorHttp.request(options);
|
||||
|
||||
// Handle 401 Unauthorized - token expired
|
||||
if (response.status === 401) {
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
}).then(() => this.request<T>(method, url, data));
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (refreshToken) {
|
||||
const refreshResponse = await CapacitorHttp.post({
|
||||
url: `${this.baseURL}/auth/refresh`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: { refreshToken },
|
||||
});
|
||||
|
||||
if (refreshResponse.status === 200) {
|
||||
localStorage.setItem('token', refreshResponse.data.accessToken);
|
||||
if (refreshResponse.data.refreshToken) {
|
||||
localStorage.setItem('refreshToken', refreshResponse.data.refreshToken);
|
||||
}
|
||||
processQueue();
|
||||
return this.request<T>(method, url, data);
|
||||
}
|
||||
}
|
||||
} catch (refreshError) {
|
||||
processQueue(refreshError);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
throw refreshError;
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle errors (4xx, 5xx)
|
||||
if (response.status >= 400) {
|
||||
const shouldRetry = response.status >= 500 && response.status < 600;
|
||||
|
||||
if (shouldRetry && retryCount < MAX_RETRIES) {
|
||||
const delay = RETRY_DELAY_BASE * Math.pow(2, retryCount);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
console.log(`Retrying request (${retryCount + 1}/${MAX_RETRIES}):`, url);
|
||||
return this.request<T>(method, url, data, retryCount + 1);
|
||||
}
|
||||
|
||||
// Handle rate limiting
|
||||
if (response.status === 429) {
|
||||
window.dispatchEvent(new CustomEvent('api:rate-limited', {
|
||||
detail: { waitTime: 60000, url }
|
||||
}));
|
||||
}
|
||||
|
||||
const error = new Error(response.data?.message || `HTTP ${response.status}`);
|
||||
(error as any).response = { status: response.status, data: response.data };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data: response.data as T, status: response.status };
|
||||
} catch (networkError: any) {
|
||||
// Network error - retry
|
||||
if (!networkError.response && retryCount < MAX_RETRIES) {
|
||||
const delay = RETRY_DELAY_BASE * Math.pow(2, retryCount);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
console.log(`Retrying request due to network error (${retryCount + 1}/${MAX_RETRIES}):`, url);
|
||||
return this.request<T>(method, url, data, retryCount + 1);
|
||||
}
|
||||
throw networkError;
|
||||
}
|
||||
}
|
||||
|
||||
get<T>(url: string) {
|
||||
return this.request<T>('GET', url);
|
||||
}
|
||||
|
||||
post<T>(url: string, data?: unknown) {
|
||||
return this.request<T>('POST', url, data);
|
||||
}
|
||||
|
||||
put<T>(url: string, data?: unknown) {
|
||||
return this.request<T>('PUT', url, data);
|
||||
}
|
||||
|
||||
patch<T>(url: string, data?: unknown) {
|
||||
return this.request<T>('PATCH', url, data);
|
||||
}
|
||||
|
||||
delete<T>(url: string) {
|
||||
return this.request<T>('DELETE', url);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the appropriate client based on platform
|
||||
let api: CapacitorApiClient | ReturnType<typeof axios.create>;
|
||||
|
||||
if (isCapacitor) {
|
||||
// Use CapacitorHttp for native platforms
|
||||
api = new CapacitorApiClient(API_BASE_URL);
|
||||
} else {
|
||||
// Use axios for web
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Request interceptor - add auth token
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Response interceptor - handle errors and retries
|
||||
axiosInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error: AxiosError) => {
|
||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retryCount?: number };
|
||||
|
||||
if (!originalRequest) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// Handle 401 Unauthorized - token expired
|
||||
if (error.response?.status === 401) {
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
}).then(() => axiosInstance(originalRequest));
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (refreshToken) {
|
||||
const { data } = await axios.post(
|
||||
`${API_BASE_URL}/auth/refresh`,
|
||||
{ refreshToken }
|
||||
);
|
||||
|
||||
localStorage.setItem('token', data.accessToken);
|
||||
if (data.refreshToken) {
|
||||
localStorage.setItem('refreshToken', data.refreshToken);
|
||||
}
|
||||
|
||||
processQueue();
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
processQueue(refreshError);
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Retry logic for network errors and 5xx errors
|
||||
const shouldRetry = (
|
||||
!error.response ||
|
||||
(error.response.status >= 500 && error.response.status < 600)
|
||||
);
|
||||
|
||||
if (shouldRetry) {
|
||||
originalRequest._retryCount = originalRequest._retryCount || 0;
|
||||
|
||||
if (originalRequest._retryCount < MAX_RETRIES) {
|
||||
originalRequest._retryCount++;
|
||||
const delay = RETRY_DELAY_BASE * Math.pow(2, originalRequest._retryCount - 1);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
console.log(`Retrying request (${originalRequest._retryCount}/${MAX_RETRIES}):`, originalRequest.url);
|
||||
return axiosInstance(originalRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rate limiting (429)
|
||||
if (error.response?.status === 429) {
|
||||
const retryAfter = error.response.headers['retry-after'];
|
||||
const waitTime = retryAfter ? parseInt(retryAfter as string, 10) * 1000 : 60000;
|
||||
window.dispatchEvent(new CustomEvent('api:rate-limited', {
|
||||
detail: { waitTime, url: originalRequest.url }
|
||||
}));
|
||||
}
|
||||
|
||||
if (!originalRequest) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Handle 401 Unauthorized - token expired
|
||||
if (error.response?.status === 401) {
|
||||
// If already refreshing, queue this request
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
}).then(() => api(originalRequest));
|
||||
}
|
||||
|
||||
// Try to refresh token
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const refreshToken = localStorage.getItem('refreshToken');
|
||||
if (refreshToken) {
|
||||
const { data } = await axios.post(
|
||||
`${API_BASE_URL}/auth/refresh`,
|
||||
{ refreshToken }
|
||||
);
|
||||
|
||||
localStorage.setItem('token', data.accessToken);
|
||||
if (data.refreshToken) {
|
||||
localStorage.setItem('refreshToken', data.refreshToken);
|
||||
}
|
||||
|
||||
processQueue();
|
||||
return api(originalRequest);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
processQueue(refreshError);
|
||||
// Clear tokens and redirect to login
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
window.location.href = '/login';
|
||||
return Promise.reject(refreshError);
|
||||
} finally {
|
||||
isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Retry logic for network errors and 5xx errors
|
||||
const shouldRetry = (
|
||||
!error.response || // Network error
|
||||
(error.response.status >= 500 && error.response.status < 600) // Server error
|
||||
);
|
||||
|
||||
if (shouldRetry) {
|
||||
originalRequest._retryCount = originalRequest._retryCount || 0;
|
||||
|
||||
if (originalRequest._retryCount < MAX_RETRIES) {
|
||||
originalRequest._retryCount++;
|
||||
|
||||
// Exponential backoff: 1s, 2s, 4s
|
||||
const delay = RETRY_DELAY_BASE * Math.pow(2, originalRequest._retryCount - 1);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
console.log(`Retrying request (${originalRequest._retryCount}/${MAX_RETRIES}):`, originalRequest.url);
|
||||
return api(originalRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rate limiting (429)
|
||||
if (error.response?.status === 429) {
|
||||
const retryAfter = error.response.headers['retry-after'];
|
||||
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : 60000;
|
||||
|
||||
// Dispatch event so UI can show a message
|
||||
window.dispatchEvent(new CustomEvent('api:rate-limited', {
|
||||
detail: { waitTime, url: originalRequest.url }
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
api = axiosInstance;
|
||||
}
|
||||
|
||||
export default api;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue