Files
CityCards_Customer_Flutter/lib/networkApiServices/network_api_services.dart

261 lines
7.6 KiB
Dart
Raw Normal View History

2026-01-08 17:07:53 +05:30
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../localPreference/local_preference.dart';
import '../networkApiServices/api_urls.dart';
2026-01-08 17:07:53 +05:30
class NetworkApiService {
static final NetworkApiService _instance = NetworkApiService._internal();
late Dio _dio;
bool _isRefreshing = false;
final List<void Function()> _retryQueue = [];
2026-01-08 17:07:53 +05:30
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 =================
2026-01-08 17:07:53 +05:30
_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);
}
2026-01-08 17:07:53 +05:30
}
return handler.reject(err);
2026-01-08 17:07:53 +05:30
},
),
);
// ================= 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';
2026-01-08 17:07:53 +05:30
}
handler.next(options);
2026-01-08 17:07:53 +05:30
},
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);
}
2026-01-08 17:07:53 +05:30
}
handler.next(error);
2026-01-08 17:07:53 +05:30
},
),
);
// ================= LOGGING INTERCEPTOR =================
2026-01-08 17:07:53 +05:30
if (kDebugMode) {
2026-01-12 12:48:59 +05:30
_dio.interceptors.add(
LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseBody: true,
error: true,
),
);
2026-01-08 17:07:53 +05:30
}
}
// ================= GET =================
2026-01-08 17:07:53 +05:30
Future<Response> getApi({
required String url,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
return await _dio.get(
2026-01-08 17:07:53 +05:30
url,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ================= POST =================
2026-01-08 17:07:53 +05:30
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(
2026-01-08 17:07:53 +05:30
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 =================
2026-01-08 17:07:53 +05:30
String _handleError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
return "Connection timeout. Please try again.";
2026-01-08 17:07:53 +05:30
case DioExceptionType.sendTimeout:
return "Send timeout. Please try again.";
2026-01-08 17:07:53 +05:30
case DioExceptionType.receiveTimeout:
return "Receive timeout. Please try again.";
2026-01-08 17:07:53 +05:30
case DioExceptionType.badCertificate:
return "Bad certificate.";
2026-01-08 17:07:53 +05:30
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}";
}
2026-01-08 17:07:53 +05:30
case DioExceptionType.cancel:
return "Request was cancelled.";
2026-01-08 17:07:53 +05:30
case DioExceptionType.connectionError:
return "No internet connection.";
2026-01-08 17:07:53 +05:30
case DioExceptionType.unknown:
return "Something went wrong. Please try again.";
2026-01-08 17:07:53 +05:30
}
}
// ================= UPDATE HEADERS =================
2026-01-08 17:07:53 +05:30
void updateHeaders(Map<String, dynamic> headers) {
_dio.options.headers.addAll(headers);
}
}