import 'dart:developer' as developer; import 'package:dio/dio.dart'; import '../constants/app_constants.dart'; import '../errors/exceptions.dart'; import '../storage/secure_storage.dart'; /// API client for making HTTP requests using Dio /// Includes token management, request/response logging, and error handling class ApiClient { late final Dio _dio; final SecureStorage _secureStorage; // Callback for 401 unauthorized errors (e.g., to navigate to login) void Function()? onUnauthorized; ApiClient(this._secureStorage, {this.onUnauthorized}) { _dio = Dio( BaseOptions( baseUrl: AppConstants.apiBaseUrl, connectTimeout: const Duration(milliseconds: AppConstants.connectionTimeout), receiveTimeout: const Duration(milliseconds: AppConstants.receiveTimeout), sendTimeout: const Duration(milliseconds: AppConstants.sendTimeout), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); _setupInterceptors(); } /// Setup all Dio interceptors void _setupInterceptors() { // Request interceptor - adds auth token and logs requests _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) async { // Add AccessToken header if available final token = await _secureStorage.getAccessToken(); if (token != null && token.isNotEmpty) { options.headers['AccessToken'] = token; } // Add AppID header options.headers['AppID'] = AppConstants.appId; // Log request in debug mode _logRequest(options); return handler.next(options); }, onResponse: (response, handler) { // Log response in debug mode _logResponse(response); return handler.next(response); }, onError: (error, handler) async { // Log error in debug mode _logError(error); // Handle 401 unauthorized errors if (error.response?.statusCode == 401) { await _handle401Error(); } return handler.next(error); }, ), ); } /// Make a GET request Future> get( String path, { Map? queryParameters, Options? options, CancelToken? cancelToken, }) async { try { return await _dio.get( path, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); } on DioException catch (e) { throw _handleDioError(e); } } /// Make a POST request Future> post( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, }) async { try { return await _dio.post( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); } on DioException catch (e) { throw _handleDioError(e); } } /// Make a PUT request Future> put( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, }) async { try { return await _dio.put( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); } on DioException catch (e) { throw _handleDioError(e); } } /// Make a DELETE request Future> delete( String path, { dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, }) async { try { return await _dio.delete( path, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); } on DioException catch (e) { throw _handleDioError(e); } } /// Handle 401 Unauthorized errors Future _handle401Error() async { developer.log( '401 Unauthorized - Clearing tokens and triggering logout', name: 'ApiClient', level: 900, ); // Clear all tokens from secure storage await _secureStorage.clearTokens(); // Trigger the unauthorized callback (e.g., navigate to login) if (onUnauthorized != null) { onUnauthorized!(); } } /// Handle Dio errors and convert them to custom exceptions Exception _handleDioError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: case DioExceptionType.sendTimeout: case DioExceptionType.receiveTimeout: return const NetworkException('Connection timeout. Please check your internet connection.'); case DioExceptionType.badResponse: final statusCode = error.response?.statusCode; // Try to extract error message from API response String message = 'Server error occurred'; if (error.response?.data is Map) { final data = error.response?.data as Map; // Check for standard API error format if (data['Errors'] != null && data['Errors'] is List && (data['Errors'] as List).isNotEmpty) { message = (data['Errors'] as List).first.toString(); } else if (data['message'] != null) { message = data['message'].toString(); } else if (data['error'] != null) { message = data['error'].toString(); } } if (statusCode != null) { // Handle specific status codes switch (statusCode) { case 401: return const ServerException('Unauthorized. Please login again.', code: '401'); case 403: return const ServerException('Forbidden. You do not have permission.', code: '403'); case 404: return ServerException('Resource not found: $message', code: '404'); case 422: return ServerException('Validation error: $message', code: '422'); case 429: return const ServerException('Too many requests. Please try again later.', code: '429'); case 500: case 501: case 502: case 503: case 504: return ServerException('Server error: $message (Status: $statusCode)', code: statusCode.toString()); default: if (statusCode >= 400 && statusCode < 500) { return ServerException('Client error: $message (Status: $statusCode)', code: statusCode.toString()); } return ServerException('HTTP error: $message (Status: $statusCode)', code: statusCode.toString()); } } return ServerException('HTTP error: $message'); case DioExceptionType.cancel: return const NetworkException('Request was cancelled'); case DioExceptionType.connectionError: return const NetworkException('No internet connection. Please check your network settings.'); case DioExceptionType.badCertificate: return const NetworkException('Certificate verification failed'); case DioExceptionType.unknown: return ServerException('An unexpected error occurred: ${error.message}'); } } /// Log request details void _logRequest(RequestOptions options) { developer.log( 'REQUEST[${options.method}] => ${options.uri}', name: 'ApiClient', level: 800, ); if (options.headers.isNotEmpty) { developer.log( 'Headers: ${_sanitizeHeaders(options.headers)}', name: 'ApiClient', level: 800, ); } if (options.queryParameters.isNotEmpty) { developer.log( 'Query Params: ${options.queryParameters}', name: 'ApiClient', level: 800, ); } if (options.data != null) { developer.log( 'Body: ${options.data}', name: 'ApiClient', level: 800, ); } } /// Log response details void _logResponse(Response response) { developer.log( 'RESPONSE[${response.statusCode}] => ${response.requestOptions.uri}', name: 'ApiClient', level: 800, ); developer.log( 'Data: ${response.data}', name: 'ApiClient', level: 800, ); } /// Log error details void _logError(DioException error) { developer.log( 'ERROR[${error.response?.statusCode}] => ${error.requestOptions.uri}', name: 'ApiClient', level: 1000, error: error, ); if (error.response?.data != null) { developer.log( 'Error Data: ${error.response?.data}', name: 'ApiClient', level: 1000, ); } } /// Sanitize headers to hide sensitive data in logs Map _sanitizeHeaders(Map headers) { final sanitized = Map.from(headers); // Hide Authorization token if (sanitized.containsKey('Authorization')) { sanitized['Authorization'] = '***REDACTED***'; } // Hide any other sensitive headers final sensitiveKeys = ['api-key', 'x-api-key', 'token']; for (final key in sensitiveKeys) { if (sanitized.containsKey(key)) { sanitized[key] = '***REDACTED***'; } } return sanitized; } /// Get the Dio instance (use carefully, prefer using the methods above) Dio get dio => _dio; /// Update base URL (useful for different environments) void updateBaseUrl(String newBaseUrl) { _dio.options.baseUrl = newBaseUrl; developer.log( 'Base URL updated to: $newBaseUrl', name: 'ApiClient', level: 800, ); } /// Test connection to the API Future testConnection() async { try { final response = await _dio.get('/health'); return response.statusCode == 200; } catch (e) { developer.log( 'Connection test failed: $e', name: 'ApiClient', level: 900, ); return false; } } /// Get current access token Future getAccessToken() async { return await _secureStorage.getAccessToken(); } /// Check if user is authenticated Future isAuthenticated() async { return await _secureStorage.isAuthenticated(); } /// Clear all authentication data Future clearAuth() async { await _secureStorage.clearAll(); developer.log( 'Authentication data cleared', name: 'ApiClient', level: 800, ); } }