Files
CityCards_Customer_Flutter/lib/networkApiServices/network_api_services.dart

334 lines
10 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;
late Dio _tokenDio; // ✅ Separate Dio for token refresh (no interceptors)
2026-01-08 17:07:53 +05:30
factory NetworkApiService() {
return _instance;
}
NetworkApiService._internal() {
// ================= MAIN DIO =================
2026-01-08 17:07:53 +05:30
_dio = Dio(
BaseOptions(
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',
},
),
);
// ================= 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(
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 &&
(err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout);
if (shouldRetry) {
if (kDebugMode) {
2026-02-06 19:34:34 +05:30
print(
'🔁 Retrying request (${currentRetry + 1}/$maxRetries) => ${options.uri}',
2026-02-06 19:34:34 +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
}
return handler.next(err);
2026-01-08 17:07:53 +05:30
},
),
);
// ================= 2. MAIN INTERCEPTOR (Auth + Token Refresh) =================
_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
},
onResponse: (response, handler) {
handler.next(response);
},
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';
// ✅ Retry original request with new token
final response = await _dio.fetch(requestOptions);
return handler.resolve(response);
} else {
await _forceLogout();
return handler.reject(error);
}
} catch (e) {
if (kDebugMode) print('❌ Error during token refresh flow: $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
},
),
);
// ================= 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,
responseHeader: false,
2026-01-12 12:48:59 +05:30
error: true,
logPrint: (log) => print('📡 $log'),
2026-01-12 12:48:59 +05:30
),
);
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 =================
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-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);
}
}
// ================= REFRESH TOKEN =================
// ✅ Uses _tokenDio (no interceptors) to avoid QueuedInterceptor deadlock
Future<bool> _refreshToken() async {
try {
final refreshToken = await LocalPreference.getRefreshToken();
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(
ApiUrls.refreshToken,
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,
),
);
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');
return true;
} on DioException catch (e) {
if (kDebugMode) print('❌ Refresh token DioException: ${e.message}');
return false;
} catch (e) {
if (kDebugMode) print('❌ Refresh token unexpected error: $e');
return false;
}
}
// ================= FORCE LOGOUT =================
Future<void> _forceLogout() async {
if (kDebugMode) print('🚪 Force logout triggered');
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:
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:
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}";
} catch (_) {
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);
}
}