API Client
Client API centralisé avec gestion des tokens, intercepteurs et support offline.
Overview
Le client API est basé sur Axios avec une configuration centralisée pour gérer l'authentification, les erreurs et le mode offline.
Auth
Token JWT auto
Retry
Retry automatique
Offline
Queue hors-ligne
Errors
Gestion centralisée
Configuration
Configuration de base du client Axios avec variables d'environnement.
src/services/api/client.tstypescript
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import * as SecureStore from 'expo-secure-store';
import { Platform } from 'react-native';
import Constants from 'expo-constants';
// API Configuration
const API_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.yanipay.com';
const API_TIMEOUT = 30000; // 30 seconds
// Create Axios instance
export const apiClient: AxiosInstance = axios.create({
baseURL: API_URL,
timeout: API_TIMEOUT,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Platform': Platform.OS,
'X-App-Version': Constants.expoConfig?.version || '1.0.0',
},
});
// Token storage keys
const ACCESS_TOKEN_KEY = 'yanipay_access_token';
const REFRESH_TOKEN_KEY = 'yanipay_refresh_token';
// Token management
export const tokenManager = {
async getAccessToken(): Promise<string | null> {
return await SecureStore.getItemAsync(ACCESS_TOKEN_KEY);
},
async getRefreshToken(): Promise<string | null> {
return await SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
},
async setTokens(accessToken: string, refreshToken: string): Promise<void> {
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, accessToken);
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, refreshToken);
},
async clearTokens(): Promise<void> {
await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY);
await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
},
};
// Export configured client
export default apiClient;Interceptors
Intercepteurs pour l'ajout automatique du token et le refresh token.
src/services/api/interceptors.tstypescript
import { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { apiClient, tokenManager } from './client';
// Request interceptor - Add auth token
apiClient.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const token = await tokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Add request timestamp for logging
config.metadata = { startTime: Date.now() };
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - Handle token refresh
let isRefreshing = false;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (error: Error) => void;
}> = [];
const processQueue = (error: Error | null, token: string | null = null) => {
failedQueue.forEach((promise) => {
if (error) {
promise.reject(error);
} else {
promise.resolve(token!);
}
});
failedQueue = [];
};
apiClient.interceptors.response.use(
(response) => {
// Log response time
const duration = Date.now() - (response.config.metadata?.startTime || 0);
console.log(`[${response.config.method?.toUpperCase()}] ${response.config.url} - ${duration}ms`);
return response;
},
async (error: AxiosError) => {
const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };
// Handle 401 Unauthorized
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// Queue the request while refreshing
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
})
.catch((err) => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
try {
const refreshToken = await tokenManager.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token');
}
const { data } = await apiClient.post('/auth/refresh', { refreshToken });
await tokenManager.setTokens(data.accessToken, data.refreshToken);
processQueue(null, data.accessToken);
originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
processQueue(refreshError as Error, null);
await tokenManager.clearTokens();
// Trigger logout in auth store
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);Error Handling
Gestion centralisée des erreurs API avec types personnalisés.
src/services/api/errors.tstypescript
import { AxiosError } from 'axios';
// API Error types
export interface APIError {
code: string;
message: string;
details?: Record<string, unknown>;
statusCode: number;
}
export class YaniPayAPIError extends Error {
code: string;
statusCode: number;
details?: Record<string, unknown>;
constructor(error: APIError) {
super(error.message);
this.name = 'YaniPayAPIError';
this.code = error.code;
this.statusCode = error.statusCode;
this.details = error.details;
}
}
// Error codes mapping
export const ERROR_CODES = {
// Auth errors
INVALID_CREDENTIALS: 'auth/invalid-credentials',
TOKEN_EXPIRED: 'auth/token-expired',
UNAUTHORIZED: 'auth/unauthorized',
// Validation errors
VALIDATION_ERROR: 'validation/error',
INVALID_INPUT: 'validation/invalid-input',
// Business errors
INSUFFICIENT_FUNDS: 'payment/insufficient-funds',
LIMIT_EXCEEDED: 'payment/limit-exceeded',
KYC_REQUIRED: 'kyc/required',
// System errors
NETWORK_ERROR: 'system/network-error',
SERVER_ERROR: 'system/server-error',
MAINTENANCE: 'system/maintenance',
} as const;
// Parse API error from Axios response
export function parseAPIError(error: AxiosError): YaniPayAPIError {
if (error.response) {
const data = error.response.data as Partial<APIError>;
return new YaniPayAPIError({
code: data.code || 'unknown',
message: data.message || 'An error occurred',
details: data.details,
statusCode: error.response.status,
});
}
if (error.request) {
// Network error
return new YaniPayAPIError({
code: ERROR_CODES.NETWORK_ERROR,
message: 'Network error. Please check your connection.',
statusCode: 0,
});
}
return new YaniPayAPIError({
code: 'unknown',
message: error.message,
statusCode: 0,
});
}
// User-friendly error messages (French)
export const ERROR_MESSAGES: Record<string, string> = {
[ERROR_CODES.INVALID_CREDENTIALS]: 'Email ou mot de passe incorrect',
[ERROR_CODES.TOKEN_EXPIRED]: 'Votre session a expiré. Veuillez vous reconnecter.',
[ERROR_CODES.UNAUTHORIZED]: 'Vous n\'êtes pas autorisé à effectuer cette action',
[ERROR_CODES.INSUFFICIENT_FUNDS]: 'Solde insuffisant',
[ERROR_CODES.LIMIT_EXCEEDED]: 'Limite de transaction dépassée',
[ERROR_CODES.KYC_REQUIRED]: 'Vérification d\'identité requise',
[ERROR_CODES.NETWORK_ERROR]: 'Erreur réseau. Vérifiez votre connexion.',
[ERROR_CODES.SERVER_ERROR]: 'Erreur serveur. Veuillez réessayer.',
[ERROR_CODES.MAINTENANCE]: 'Service en maintenance. Réessayez plus tard.',
};
export function getErrorMessage(code: string): string {
return ERROR_MESSAGES[code] || 'Une erreur est survenue';
}Offline Support
Queue des requêtes en mode hors-ligne avec synchronisation automatique.
src/services/api/offline-queue.tstypescript
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { apiClient } from './client';
interface QueuedRequest {
id: string;
method: 'POST' | 'PUT' | 'PATCH' | 'DELETE';
url: string;
data?: unknown;
timestamp: number;
retries: number;
}
const QUEUE_KEY = 'yanipay_offline_queue';
const MAX_RETRIES = 3;
class OfflineQueue {
private queue: QueuedRequest[] = [];
private isOnline = true;
private isSyncing = false;
async initialize() {
// Load persisted queue
const stored = await AsyncStorage.getItem(QUEUE_KEY);
if (stored) {
this.queue = JSON.parse(stored);
}
// Listen for connectivity changes
NetInfo.addEventListener(this.handleConnectivityChange);
// Check initial state
const state = await NetInfo.fetch();
this.isOnline = state.isConnected ?? true;
}
private handleConnectivityChange = (state: NetInfoState) => {
const wasOffline = !this.isOnline;
this.isOnline = state.isConnected ?? true;
// Sync when coming back online
if (wasOffline && this.isOnline) {
this.syncQueue();
}
};
async addToQueue(request: Omit<QueuedRequest, 'id' | 'timestamp' | 'retries'>) {
const queuedRequest: QueuedRequest = {
...request,
id: `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
retries: 0,
};
this.queue.push(queuedRequest);
await this.persistQueue();
console.log('Request queued for offline sync:', queuedRequest.url);
}
async syncQueue() {
if (this.isSyncing || this.queue.length === 0 || !this.isOnline) {
return;
}
this.isSyncing = true;
console.log(`Syncing ${this.queue.length} queued requests...`);
const failedRequests: QueuedRequest[] = [];
for (const request of this.queue) {
try {
await apiClient.request({
method: request.method,
url: request.url,
data: request.data,
});
console.log('Synced:', request.url);
} catch (error) {
request.retries++;
if (request.retries < MAX_RETRIES) {
failedRequests.push(request);
} else {
console.error('Max retries reached for:', request.url);
}
}
}
this.queue = failedRequests;
await this.persistQueue();
this.isSyncing = false;
}
private async persistQueue() {
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(this.queue));
}
getQueueLength(): number {
return this.queue.length;
}
isConnected(): boolean {
return this.isOnline;
}
}
export const offlineQueue = new OfflineQueue();Retry Logic
Configuration du retry automatique avec exponential backoff.
src/services/api/retry.tstypescript
import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
import { apiClient } from './client';
// Retry configuration
const retryConfig: IAxiosRetryConfig = {
retries: 3,
retryDelay: (retryCount) => {
// Exponential backoff: 1s, 2s, 4s
return Math.pow(2, retryCount - 1) * 1000;
},
retryCondition: (error) => {
// Retry on network errors or 5xx server errors
return (
axiosRetry.isNetworkError(error) ||
axiosRetry.isRetryableError(error) ||
(error.response?.status ?? 0) >= 500
);
},
onRetry: (retryCount, error, requestConfig) => {
console.log(
`Retry attempt ${retryCount} for ${requestConfig.url}: ${error.message}`
);
},
};
// Apply retry logic to client
axiosRetry(apiClient, retryConfig);
// Custom retry wrapper for critical operations
export async function withRetry<T>(
operation: () => Promise<T>,
options: { maxRetries?: number; delay?: number } = {}
): Promise<T> {
const { maxRetries = 3, delay = 1000 } = options;
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;
console.log(`Attempt ${attempt} failed: ${lastError.message}`);
if (attempt < maxRetries) {
await new Promise((resolve) =>
setTimeout(resolve, delay * Math.pow(2, attempt - 1))
);
}
}
}
throw lastError;
}
// Usage example
// const result = await withRetry(() => paymentService.sendMoney(data), { maxRetries: 3 });