265 lines
7.7 KiB
Dart
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);
|
|
}
|
|
}
|