Files
CityCards_Customer_Flutter/lib/networkApiServices/network_api_services.dart
2026-02-09 10:55:36 +05:30

265 lines
7.7 KiB
Dart

import 'dart:developer';
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<void Function()> _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<Response> getApi({
required String url,
Map<String, dynamic>? 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<Response> postApi({
required String url,
dynamic data,
Map<String, dynamic>? 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<Response> putApi({
required String url,
dynamic data,
Map<String, dynamic>? 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<bool> _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<void> _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<String, dynamic>) {
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<String, dynamic> headers) {
_dio.options.headers.addAll(headers);
}
}