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 axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { CapacitorHttp, HttpOptions, HttpResponse } from '@capacitor/core';
|
||||||
|
|
||||||
// Detect if running in Capacitor native environment
|
// 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
|
// Production API URL for native app, relative URL for web
|
||||||
const API_BASE_URL = isCapacitor
|
const API_BASE_URL = isCapacitor
|
||||||
|
|
@ -30,25 +32,160 @@ const processQueue = (error: unknown = null) => {
|
||||||
failedQueue = [];
|
failedQueue = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const api = axios.create({
|
// Helper to get auth token
|
||||||
|
const getAuthToken = (): string | null => {
|
||||||
|
return localStorage.getItem('token');
|
||||||
|
};
|
||||||
|
|
||||||
|
// CapacitorHttp-based API client for native platforms
|
||||||
|
class CapacitorApiClient {
|
||||||
|
private baseURL: string;
|
||||||
|
|
||||||
|
constructor(baseURL: string) {
|
||||||
|
this.baseURL = baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
timeout: 30000, // 30 second timeout
|
timeout: 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Request interceptor - add auth token
|
// Request interceptor - add auth token
|
||||||
api.interceptors.request.use((config) => {
|
axiosInstance.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Response interceptor - handle errors and retries
|
// Response interceptor - handle errors and retries
|
||||||
api.interceptors.response.use(
|
axiosInstance.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
const originalRequest = error.config as InternalAxiosRequestConfig & { _retryCount?: number };
|
const originalRequest = error.config as InternalAxiosRequestConfig & { _retryCount?: number };
|
||||||
|
|
@ -59,14 +196,12 @@ api.interceptors.response.use(
|
||||||
|
|
||||||
// Handle 401 Unauthorized - token expired
|
// Handle 401 Unauthorized - token expired
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// If already refreshing, queue this request
|
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
failedQueue.push({ resolve, reject });
|
failedQueue.push({ resolve, reject });
|
||||||
}).then(() => api(originalRequest));
|
}).then(() => axiosInstance(originalRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to refresh token
|
|
||||||
isRefreshing = true;
|
isRefreshing = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -83,11 +218,10 @@ api.interceptors.response.use(
|
||||||
}
|
}
|
||||||
|
|
||||||
processQueue();
|
processQueue();
|
||||||
return api(originalRequest);
|
return axiosInstance(originalRequest);
|
||||||
}
|
}
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
processQueue(refreshError);
|
processQueue(refreshError);
|
||||||
// Clear tokens and redirect to login
|
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('refreshToken');
|
localStorage.removeItem('refreshToken');
|
||||||
localStorage.removeItem('user');
|
localStorage.removeItem('user');
|
||||||
|
|
@ -100,8 +234,8 @@ api.interceptors.response.use(
|
||||||
|
|
||||||
// Retry logic for network errors and 5xx errors
|
// Retry logic for network errors and 5xx errors
|
||||||
const shouldRetry = (
|
const shouldRetry = (
|
||||||
!error.response || // Network error
|
!error.response ||
|
||||||
(error.response.status >= 500 && error.response.status < 600) // Server error
|
(error.response.status >= 500 && error.response.status < 600)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldRetry) {
|
if (shouldRetry) {
|
||||||
|
|
@ -109,23 +243,17 @@ api.interceptors.response.use(
|
||||||
|
|
||||||
if (originalRequest._retryCount < MAX_RETRIES) {
|
if (originalRequest._retryCount < MAX_RETRIES) {
|
||||||
originalRequest._retryCount++;
|
originalRequest._retryCount++;
|
||||||
|
|
||||||
// Exponential backoff: 1s, 2s, 4s
|
|
||||||
const delay = RETRY_DELAY_BASE * Math.pow(2, originalRequest._retryCount - 1);
|
const delay = RETRY_DELAY_BASE * Math.pow(2, originalRequest._retryCount - 1);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
|
||||||
console.log(`Retrying request (${originalRequest._retryCount}/${MAX_RETRIES}):`, originalRequest.url);
|
console.log(`Retrying request (${originalRequest._retryCount}/${MAX_RETRIES}):`, originalRequest.url);
|
||||||
return api(originalRequest);
|
return axiosInstance(originalRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle rate limiting (429)
|
// Handle rate limiting (429)
|
||||||
if (error.response?.status === 429) {
|
if (error.response?.status === 429) {
|
||||||
const retryAfter = error.response.headers['retry-after'];
|
const retryAfter = error.response.headers['retry-after'];
|
||||||
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : 60000;
|
const waitTime = retryAfter ? parseInt(retryAfter as string, 10) * 1000 : 60000;
|
||||||
|
|
||||||
// Dispatch event so UI can show a message
|
|
||||||
window.dispatchEvent(new CustomEvent('api:rate-limited', {
|
window.dispatchEvent(new CustomEvent('api:rate-limited', {
|
||||||
detail: { waitTime, url: originalRequest.url }
|
detail: { waitTime, url: originalRequest.url }
|
||||||
}));
|
}));
|
||||||
|
|
@ -133,7 +261,9 @@ api.interceptors.response.use(
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
api = axiosInstance;
|
||||||
|
}
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue