import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import '../localPreference/local_preference.dart'; import '../networkApiServices/api_urls.dart'; class NetworkApiService { static final NetworkApiService _instance = NetworkApiService._internal(); late Dio _dio; bool _isRefreshing = false; final List _retryQueue = []; factory NetworkApiService() { return _instance; } NetworkApiService._internal() { _dio = Dio( BaseOptions( connectTimeout: const Duration(seconds: 30), receiveTimeout: const Duration(seconds: 30), headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, ), ); // ================= RETRY INTERCEPTOR ================= _dio.interceptors.add( InterceptorsWrapper( onError: (err, handler) async { final options = err.requestOptions; const maxRetries = 2; final currentRetry = options.extra['retry'] as int? ?? 0; final shouldRetry = currentRetry < maxRetries && (err.type == DioExceptionType.connectionTimeout || err.type == DioExceptionType.sendTimeout || err.type == DioExceptionType.receiveTimeout); if (shouldRetry) { if (kDebugMode) { print('🔁 Retrying request (${currentRetry + 1}) => ${options.uri}'); } options.extra['retry'] = currentRetry + 1; try { final response = await _dio.fetch(options); return handler.resolve(response); } on DioException catch (e) { return handler.reject(e); } } return handler.reject(err); }, ), ); // ================= MAIN INTERCEPTOR ================= // Use Dio's built-in QueuedInterceptor for better concurrency control _dio.interceptors.add( QueuedInterceptorsWrapper( onRequest: (options, handler) async { final token = await LocalPreference.getAccessToken(); if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) async { if (error.response?.statusCode == 401) { final requestOptions = error.requestOptions; try { // QueuedInterceptor handles concurrency automatically final refreshed = await _refreshToken(); if (refreshed) { final newToken = await LocalPreference.getAccessToken(); requestOptions.headers['Authorization'] = 'Bearer $newToken'; final response = await _dio.fetch(requestOptions); return handler.resolve(response); } else { await _forceLogout(); return handler.reject(error); } } catch (e) { await _forceLogout(); return handler.reject(error); } } handler.next(error); }, ), ); // ================= LOGGING INTERCEPTOR ================= if (kDebugMode) { _dio.interceptors.add( LogInterceptor( request: true, requestHeader: true, requestBody: true, responseBody: true, error: true, ), ); } } // ================= GET ================= Future getApi({ required String url, Map? queryParameters, Options? options, CancelToken? cancelToken, }) async { try { return await _dio.get( url, queryParameters: queryParameters, options: options, cancelToken: cancelToken, ); } on DioException catch (e) { throw _handleError(e); } } // ================= POST ================= Future postApi({ required String url, dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, }) async { try { return await _dio.post( url, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, onSendProgress: onSendProgress, ); } on DioException catch (e) { throw _handleError(e); } } // ================= PUT (NEW) ================= Future putApi({ required String url, dynamic data, Map? queryParameters, Options? options, CancelToken? cancelToken, ProgressCallback? onSendProgress, }) async { try { return await _dio.put( url, data: data, queryParameters: queryParameters, options: options, cancelToken: cancelToken, onSendProgress: onSendProgress, ); } on DioException catch (e) { throw _handleError(e); } } // ================= REFRESH TOKEN ================= Future _refreshToken() async { try { final refreshToken = await LocalPreference.getRefreshToken(); if (refreshToken == null) return false; final response = await _dio.post( ApiUrls.refreshToken, data: {"refreshToken": refreshToken}, options: Options( headers: {'Authorization': null}, ), ); await LocalPreference.setAccessToken(response.data['accessToken']); return true; } catch (_) { return false; } } // ================= LOGOUT ================= Future _forceLogout() async { await LocalPreference.clearTokens(); await LocalPreference.setLogin(false); await LocalPreference.resetOnboarding(); // TODO: navigate to login screen } // ================= ERROR HANDLER ================= // ================= ERROR HANDLER ================= String _handleError(DioException error) { switch (error.type) { case DioExceptionType.connectionTimeout: return "Connection timeout. Please try again."; case DioExceptionType.sendTimeout: return "Send timeout. Please try again."; case DioExceptionType.receiveTimeout: return "Receive timeout. Please try again."; case DioExceptionType.badCertificate: return "Bad certificate."; case DioExceptionType.badResponse: // 🔥 FIXED: Safely handle different response data types try { final responseData = error.response?.data; // If it's a Map, try to get the message if (responseData is Map) { return responseData['message'] ?? responseData['error'] ?? "Invalid status code: ${error.response?.statusCode}"; } // If it's a String, return it directly if (responseData is String) { return responseData.isNotEmpty ? responseData : "Invalid status code: ${error.response?.statusCode}"; } // For any other type, return generic error return "Invalid status code: ${error.response?.statusCode}"; } catch (e) { return "Invalid status code: ${error.response?.statusCode}"; } case DioExceptionType.cancel: return "Request was cancelled."; case DioExceptionType.connectionError: return "No internet connection."; case DioExceptionType.unknown: return "Something went wrong. Please try again."; } } // ================= UPDATE HEADERS ================= void updateHeaders(Map headers) { _dio.options.headers.addAll(headers); } }