2026-01-08 17:07:53 +05:30
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
2026-01-27 18:47:15 +05:30
|
|
|
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();
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
late Dio _dio;
|
|
|
|
|
late Dio _tokenDio; // ✅ Separate Dio for token refresh (no interceptors)
|
2026-01-27 18:47:15 +05:30
|
|
|
|
2026-01-08 17:07:53 +05:30
|
|
|
factory NetworkApiService() {
|
|
|
|
|
return _instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NetworkApiService._internal() {
|
2026-04-01 11:50:00 +05:30
|
|
|
// ================= MAIN DIO =================
|
2026-01-08 17:07:53 +05:30
|
|
|
_dio = Dio(
|
|
|
|
|
BaseOptions(
|
2026-03-17 17:07:14 +05:30
|
|
|
connectTimeout: const Duration(seconds: 60),
|
|
|
|
|
receiveTimeout: const Duration(seconds: 60),
|
2026-01-08 17:07:53 +05:30
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-01-27 18:47:15 +05:30
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
// ================= TOKEN DIO (No interceptors — used only for refresh) =================
|
|
|
|
|
_tokenDio = Dio(
|
|
|
|
|
BaseOptions(
|
|
|
|
|
connectTimeout: const Duration(seconds: 30),
|
|
|
|
|
receiveTimeout: const Duration(seconds: 30),
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'Accept': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// ================= 1. RETRY INTERCEPTOR =================
|
2026-01-08 17:07:53 +05:30
|
|
|
_dio.interceptors.add(
|
|
|
|
|
InterceptorsWrapper(
|
2026-01-27 18:47:15 +05:30
|
|
|
onError: (err, handler) async {
|
|
|
|
|
final options = err.requestOptions;
|
|
|
|
|
const maxRetries = 2;
|
|
|
|
|
final currentRetry = options.extra['retry'] as int? ?? 0;
|
|
|
|
|
|
2026-02-06 19:34:34 +05:30
|
|
|
final shouldRetry =
|
|
|
|
|
currentRetry < maxRetries &&
|
2026-04-01 11:50:00 +05:30
|
|
|
(err.type == DioExceptionType.connectionTimeout ||
|
|
|
|
|
err.type == DioExceptionType.sendTimeout ||
|
|
|
|
|
err.type == DioExceptionType.receiveTimeout);
|
2026-01-27 18:47:15 +05:30
|
|
|
|
|
|
|
|
if (shouldRetry) {
|
|
|
|
|
if (kDebugMode) {
|
2026-02-06 19:34:34 +05:30
|
|
|
print(
|
2026-04-01 11:50:00 +05:30
|
|
|
'🔁 Retrying request (${currentRetry + 1}/$maxRetries) => ${options.uri}',
|
2026-02-06 19:34:34 +05:30
|
|
|
);
|
2026-01-27 18:47:15 +05:30
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
2026-01-27 18:47:15 +05:30
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
return handler.next(err);
|
2026-01-08 17:07:53 +05:30
|
|
|
},
|
2026-01-27 18:47:15 +05:30
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
// ================= 2. MAIN INTERCEPTOR (Auth + Token Refresh) =================
|
2026-01-27 18:47:15 +05:30
|
|
|
_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
|
|
|
}
|
2026-01-27 18:47:15 +05:30
|
|
|
handler.next(options);
|
2026-01-08 17:07:53 +05:30
|
|
|
},
|
2026-01-27 18:47:15 +05:30
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
onResponse: (response, handler) {
|
|
|
|
|
handler.next(response);
|
|
|
|
|
},
|
|
|
|
|
|
2026-01-27 18:47:15 +05:30
|
|
|
onError: (error, handler) async {
|
|
|
|
|
if (error.response?.statusCode == 401) {
|
|
|
|
|
final requestOptions = error.requestOptions;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final refreshed = await _refreshToken();
|
|
|
|
|
|
|
|
|
|
if (refreshed) {
|
|
|
|
|
final newToken = await LocalPreference.getAccessToken();
|
|
|
|
|
requestOptions.headers['Authorization'] = 'Bearer $newToken';
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
// ✅ Retry original request with new token
|
2026-01-27 18:47:15 +05:30
|
|
|
final response = await _dio.fetch(requestOptions);
|
|
|
|
|
return handler.resolve(response);
|
|
|
|
|
} else {
|
|
|
|
|
await _forceLogout();
|
|
|
|
|
return handler.reject(error);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
2026-04-01 11:50:00 +05:30
|
|
|
if (kDebugMode) print('❌ Error during token refresh flow: $e');
|
2026-01-27 18:47:15 +05:30
|
|
|
await _forceLogout();
|
|
|
|
|
return handler.reject(error);
|
|
|
|
|
}
|
2026-01-08 17:07:53 +05:30
|
|
|
}
|
2026-01-27 18:47:15 +05:30
|
|
|
|
|
|
|
|
handler.next(error);
|
2026-01-08 17:07:53 +05:30
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-01-27 18:47:15 +05:30
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
// ================= 3. 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,
|
2026-04-01 11:50:00 +05:30
|
|
|
responseHeader: false,
|
2026-01-12 12:48:59 +05:30
|
|
|
error: true,
|
2026-04-01 11:50:00 +05:30
|
|
|
logPrint: (log) => print('📡 $log'),
|
2026-01-12 12:48:59 +05:30
|
|
|
),
|
|
|
|
|
);
|
2026-01-08 17:07:53 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-01-27 18:47:15 +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 {
|
2026-01-27 18:47:15 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-27 18:47:15 +05:30
|
|
|
|
|
|
|
|
// ================= 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 {
|
2026-01-27 18:47:15 +05:30
|
|
|
return await _dio.post(
|
|
|
|
|
url,
|
|
|
|
|
data: data,
|
|
|
|
|
queryParameters: queryParameters,
|
|
|
|
|
options: options,
|
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
|
onSendProgress: onSendProgress,
|
|
|
|
|
);
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
throw _handleError(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
// ================= PUT =================
|
2026-01-27 18:47:15 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-27 18:47:15 +05:30
|
|
|
|
2026-02-13 15:25:05 +05:30
|
|
|
// ================= DELETE =================
|
|
|
|
|
Future<Response> deleteApi({
|
|
|
|
|
required String url,
|
|
|
|
|
dynamic data,
|
|
|
|
|
Map<String, dynamic>? queryParameters,
|
|
|
|
|
Options? options,
|
|
|
|
|
CancelToken? cancelToken,
|
|
|
|
|
}) async {
|
|
|
|
|
try {
|
|
|
|
|
return await _dio.delete(
|
|
|
|
|
url,
|
|
|
|
|
data: data,
|
|
|
|
|
queryParameters: queryParameters,
|
|
|
|
|
options: options,
|
|
|
|
|
cancelToken: cancelToken,
|
|
|
|
|
);
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
throw _handleError(e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 18:47:15 +05:30
|
|
|
// ================= REFRESH TOKEN =================
|
2026-04-01 11:50:00 +05:30
|
|
|
// ✅ Uses _tokenDio (no interceptors) to avoid QueuedInterceptor deadlock
|
2026-01-27 18:47:15 +05:30
|
|
|
Future<bool> _refreshToken() async {
|
|
|
|
|
try {
|
|
|
|
|
final refreshToken = await LocalPreference.getRefreshToken();
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
if (kDebugMode) print('🔍 Refresh token from storage: $refreshToken');
|
|
|
|
|
|
|
|
|
|
if (refreshToken == null || refreshToken.isEmpty) {
|
|
|
|
|
if (kDebugMode) print('❌ No refresh token found');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (kDebugMode) print('🔄 Attempting token refresh...');
|
|
|
|
|
|
|
|
|
|
final response = await _tokenDio.post(
|
2026-01-27 18:47:15 +05:30
|
|
|
ApiUrls.refreshToken,
|
2026-04-01 11:50:00 +05:30
|
|
|
data: '', // ✅ Empty body — server reads token from Cookie header
|
|
|
|
|
options: Options(
|
|
|
|
|
headers: {
|
|
|
|
|
// ✅ Manually inject refresh token as cookie header
|
|
|
|
|
'Cookie': 'refresh_token=$refreshToken',
|
|
|
|
|
},
|
|
|
|
|
validateStatus: (status) => status != null && status < 500,
|
|
|
|
|
),
|
2026-01-27 18:47:15 +05:30
|
|
|
);
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
if (kDebugMode) {
|
|
|
|
|
print("🔄 Refresh response status: ${response.statusCode}");
|
|
|
|
|
print("✅ REFRESH RESPONSE => ${response.data}");
|
|
|
|
|
|
|
|
|
|
// ✅ ADD THESE to see full response details
|
|
|
|
|
print("📋 Response Headers: ${response.headers.map}");
|
|
|
|
|
print("📋 Full Response Data: ${response.data.toString()}");
|
|
|
|
|
print("📋 Request Headers Sent: ${response.requestOptions.headers}"); // ← This shows what was actually sent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
|
|
|
|
if (kDebugMode) print('❌ Refresh failed with status: ${response.statusCode}');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final newAccessToken = response.data['accessToken'];
|
|
|
|
|
|
|
|
|
|
if (newAccessToken == null || (newAccessToken as String).isEmpty) {
|
|
|
|
|
if (kDebugMode) print('❌ Access token missing in refresh response');
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await LocalPreference.setAccessToken(newAccessToken);
|
|
|
|
|
if (kDebugMode) print('✅ Token refreshed successfully');
|
2026-01-27 18:47:15 +05:30
|
|
|
return true;
|
2026-04-01 11:50:00 +05:30
|
|
|
|
|
|
|
|
} on DioException catch (e) {
|
|
|
|
|
if (kDebugMode) print('❌ Refresh token DioException: ${e.message}');
|
|
|
|
|
return false;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (kDebugMode) print('❌ Refresh token unexpected error: $e');
|
2026-01-27 18:47:15 +05:30
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 11:50:00 +05:30
|
|
|
// ================= FORCE LOGOUT =================
|
2026-01-27 18:47:15 +05:30
|
|
|
Future<void> _forceLogout() async {
|
2026-04-01 11:50:00 +05:30
|
|
|
if (kDebugMode) print('🚪 Force logout triggered');
|
2026-01-27 18:47:15 +05:30
|
|
|
await LocalPreference.clearTokens();
|
|
|
|
|
await LocalPreference.setLogin(false);
|
|
|
|
|
await LocalPreference.resetOnboarding();
|
|
|
|
|
// TODO: navigate to login screen
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ================= ERROR HANDLER =================
|
2026-01-08 17:07:53 +05:30
|
|
|
String _handleError(DioException error) {
|
|
|
|
|
switch (error.type) {
|
|
|
|
|
case DioExceptionType.connectionTimeout:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "Connection timeout. Please try again.";
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.sendTimeout:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "Send timeout. Please try again.";
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.receiveTimeout:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "Receive timeout. Please try again.";
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.badCertificate:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "Bad certificate.";
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.badResponse:
|
2026-02-05 19:35:01 +05:30
|
|
|
try {
|
|
|
|
|
final responseData = error.response?.data;
|
|
|
|
|
if (responseData is Map<String, dynamic>) {
|
|
|
|
|
return responseData['message'] ??
|
|
|
|
|
responseData['error'] ??
|
|
|
|
|
"Invalid status code: ${error.response?.statusCode}";
|
|
|
|
|
}
|
|
|
|
|
if (responseData is String) {
|
|
|
|
|
return responseData.isNotEmpty
|
|
|
|
|
? responseData
|
|
|
|
|
: "Invalid status code: ${error.response?.statusCode}";
|
|
|
|
|
}
|
|
|
|
|
return "Invalid status code: ${error.response?.statusCode}";
|
2026-04-01 11:50:00 +05:30
|
|
|
} catch (_) {
|
2026-02-05 19:35:01 +05:30
|
|
|
return "Invalid status code: ${error.response?.statusCode}";
|
|
|
|
|
}
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.cancel:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "Request was cancelled.";
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.connectionError:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "No internet connection.";
|
2026-01-08 17:07:53 +05:30
|
|
|
case DioExceptionType.unknown:
|
2026-01-27 18:47:15 +05:30
|
|
|
return "Something went wrong. Please try again.";
|
2026-01-08 17:07:53 +05:30
|
|
|
}
|
|
|
|
|
}
|
2026-01-27 18:47:15 +05:30
|
|
|
|
|
|
|
|
// ================= UPDATE HEADERS =================
|
2026-01-08 17:07:53 +05:30
|
|
|
void updateHeaders(Map<String, dynamic> headers) {
|
|
|
|
|
_dio.options.headers.addAll(headers);
|
|
|
|
|
}
|
2026-04-01 11:50:00 +05:30
|
|
|
}
|