From 1e3614ed58000cae6f14e89cfa763bcbd5a8aed1 Mon Sep 17 00:00:00 2001 From: "mr.mojtaba" Date: Mon, 23 Jun 2025 10:42:29 +0330 Subject: [PATCH] fix : add cancel Token --- packages/auth/lib/data/di/auth_di.dart | 34 ++++++-- packages/auth/lib/data/utils/safe_call.dart | 25 +----- .../data/repositories/chicken_repository.dart | 2 +- .../repositories/chicken_repository_imp.dart | 2 +- .../lib/presentation/pages/home/logic.dart | 2 +- packages/core/lib/core.dart | 8 +- .../lib/infrastructure/infrastructure.dart | 14 ++++ .../remote/app_interceptor.dart | 51 ++++++++++++ .../lib/infrastructure/remote/dio_remote.dart | 12 ++- .../lib/utils/network/safe_call_utils.dart | 78 ++++++++----------- 10 files changed, 140 insertions(+), 88 deletions(-) create mode 100644 packages/core/lib/infrastructure/infrastructure.dart create mode 100644 packages/core/lib/infrastructure/remote/app_interceptor.dart diff --git a/packages/auth/lib/data/di/auth_di.dart b/packages/auth/lib/data/di/auth_di.dart index e6b40cd..48554de 100644 --- a/packages/auth/lib/data/di/auth_di.dart +++ b/packages/auth/lib/data/di/auth_di.dart @@ -1,6 +1,7 @@ -import 'package:rasadyar_auth/data/common/constant.dart'; import 'package:rasadyar_auth/data/common/dio_error_handler.dart'; +import 'package:rasadyar_auth/data/models/response/auth/auth_response_model.dart'; import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart'; +import 'package:rasadyar_auth/data/services/token_storage_service.dart'; import 'package:rasadyar_core/core.dart'; import '../common/dio_manager.dart'; @@ -9,8 +10,27 @@ GetIt diAuth = GetIt.instance; Future setupAuthDI() async { diAuth.registerLazySingleton(() => DioRemoteManager()); - diAuth.registerLazySingleton(() => DioRemote()); + diAuth.registerLazySingleton( + () => AppInterceptor( + refreshTokenCallback: () async { + var tokenService = Get.find(); + final authRepo = diAuth.get(); + final refreshToken = tokenService.refreshToken.value; + if (refreshToken == null) return null; + + final result = await authRepo.loginWithRefreshToken(authRequest: {"refresh_token": refreshToken}); + + if (result is AuthResponseModel) { + await tokenService.saveAccessToken(result.access!); + return result.access; + } + return null; + }, + ), + ); + + diAuth.registerLazySingleton(() => DioRemote(interceptors: [diAuth.get()])); final dioRemote = diAuth.get(); await dioRemote.init(); @@ -19,11 +39,11 @@ Future setupAuthDI() async { } Future newSetupAuthDI(String newUrl) async { - diAuth.registerLazySingleton(() => DioRemote(baseUrl: newUrl),instanceName: 'newRemote'); + diAuth.registerLazySingleton( + () => DioRemote(baseUrl: newUrl, interceptors: [diAuth.get()]), + instanceName: 'newRemote', + ); final dioRemote = diAuth.get(instanceName: 'newRemote'); await dioRemote.init(); - diAuth.registerSingleton( - AuthRepositoryImpl(dioRemote), - instanceName: 'newUrl', - ); + diAuth.registerSingleton(AuthRepositoryImpl(dioRemote), instanceName: 'newUrl'); } diff --git a/packages/auth/lib/data/utils/safe_call.dart b/packages/auth/lib/data/utils/safe_call.dart index 6d08637..fb14499 100644 --- a/packages/auth/lib/data/utils/safe_call.dart +++ b/packages/auth/lib/data/utils/safe_call.dart @@ -1,11 +1,9 @@ -import 'package:flutter/foundation.dart'; import 'package:rasadyar_auth/auth.dart'; import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart'; import 'package:rasadyar_core/core.dart'; import '../models/response/auth/auth_response_model.dart'; import '../services/token_storage_service.dart'; - Future safeCall({ required AppAsyncCallback call, Function(T result)? onSuccess, @@ -21,9 +19,6 @@ Future safeCall({ Function()? onShowSuccessMessage, Function()? onShowErrorMessage, }) { - final authRepository = diAuth.get(); - TokenStorageService tokenStorageService = Get.find(); - return gSafeCall( call: call, onSuccess: onSuccess, @@ -38,24 +33,6 @@ Future safeCall({ onHideLoading: onHideLoading, onShowSuccessMessage: onShowSuccessMessage, onShowErrorMessage: onShowErrorMessage, - retryOnAuthError: true, - onTokenRefresh: () { - var token = tokenStorageService.refreshToken.value; - authRepository - .loginWithRefreshToken(authRequest: {"refresh_token": token}) - .then((value) async { - if (value is AuthResponseModel) { - await tokenStorageService.saveAccessToken(value.access!); - } else { - throw Exception("Failed to refresh token"); - } - }) - .catchError((error) { - if (kDebugMode) { - print('Error during token refresh: $error'); - } - throw error; - }); - }, ); } + diff --git a/packages/chicken/lib/data/repositories/chicken_repository.dart b/packages/chicken/lib/data/repositories/chicken_repository.dart index a880f85..250fbd8 100644 --- a/packages/chicken/lib/data/repositories/chicken_repository.dart +++ b/packages/chicken/lib/data/repositories/chicken_repository.dart @@ -19,7 +19,7 @@ import '../models/request/create_steward_free_bar/create_steward_free_bar.dart'; abstract class ChickenRepository { Future?> getInventory({required String token}); - Future getIKillHouseDistributionInfo({required String token}); + Future getKillHouseDistributionInfo({required String token}); Future getGeneralBarInformation({required String token, Map? queryParameters}); diff --git a/packages/chicken/lib/data/repositories/chicken_repository_imp.dart b/packages/chicken/lib/data/repositories/chicken_repository_imp.dart index e1a59e9..556c6ba 100644 --- a/packages/chicken/lib/data/repositories/chicken_repository_imp.dart +++ b/packages/chicken/lib/data/repositories/chicken_repository_imp.dart @@ -35,7 +35,7 @@ class ChickenRepositoryImpl implements ChickenRepository { } @override - Future getIKillHouseDistributionInfo({required String token}) async { + Future getKillHouseDistributionInfo({required String token}) async { var res = await _httpClient.get( '/kill-house-distribution-info/?role=Steward', headers: {'Authorization': 'Bearer $token'}, diff --git a/packages/chicken/lib/presentation/pages/home/logic.dart b/packages/chicken/lib/presentation/pages/home/logic.dart index bb344e2..bcfe78b 100644 --- a/packages/chicken/lib/presentation/pages/home/logic.dart +++ b/packages/chicken/lib/presentation/pages/home/logic.dart @@ -72,7 +72,7 @@ class HomeLogic extends GetxController { Future getDistributionInformation() async { await safeCall( - call: () async => await rootLogic.chickenRepository.getIKillHouseDistributionInfo(token: rootLogic.tokenService.accessToken.value!), + call: () async => await rootLogic.chickenRepository.getKillHouseDistributionInfo(token: rootLogic.tokenService.accessToken.value!), onSuccess: (result) { if (result != null) { killHouseDistributionInfo.value = result; diff --git a/packages/core/lib/core.dart b/packages/core/lib/core.dart index 35d1a04..6f6f95b 100644 --- a/packages/core/lib/core.dart +++ b/packages/core/lib/core.dart @@ -33,12 +33,8 @@ export 'package:rasadyar_core/presentation/common/common.dart'; export 'package:rasadyar_core/presentation/utils/utils.dart'; export 'package:rasadyar_core/presentation/widget/widget.dart'; -export 'infrastructure/local/hive_local_storage.dart'; -//network -export 'infrastructure/remote/dio_form_data.dart'; -export 'infrastructure/remote/dio_remote.dart'; -export 'infrastructure/remote/dio_response.dart'; -export 'injection/di.dart'; +//infrastructure +export 'infrastructure/infrastructure.dart'; ///image picker export 'package:image_picker/image_picker.dart'; diff --git a/packages/core/lib/infrastructure/infrastructure.dart b/packages/core/lib/infrastructure/infrastructure.dart new file mode 100644 index 0000000..86f93f0 --- /dev/null +++ b/packages/core/lib/infrastructure/infrastructure.dart @@ -0,0 +1,14 @@ +// local +export 'local/hive_local_storage.dart'; +export 'local/i_local_storage.dart'; + +//remote +export 'remote/interfaces/i_form_data.dart'; +export 'remote/interfaces/i_http_client.dart'; +export 'remote/interfaces/i_http_response.dart'; +export 'remote/interfaces/i_remote.dart'; + +export 'remote/app_interceptor.dart'; +export 'remote/dio_form_data.dart'; +export 'remote/dio_remote.dart'; +export 'remote/dio_response.dart'; \ No newline at end of file diff --git a/packages/core/lib/infrastructure/remote/app_interceptor.dart b/packages/core/lib/infrastructure/remote/app_interceptor.dart new file mode 100644 index 0000000..c58cde5 --- /dev/null +++ b/packages/core/lib/infrastructure/remote/app_interceptor.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import '../../core.dart'; + +typedef RefreshTokenCallback = Future Function(); +class AppInterceptor extends Interceptor { + final RefreshTokenCallback refreshTokenCallback; + + AppInterceptor({required this.refreshTokenCallback}); + + @override + Future onError(DioException err, ErrorInterceptorHandler handler) async { + if (err.response?.statusCode == 401 && !ApiHandler.isRefreshing) { + // اول بقیه درخواست‌ها رو کنسل کن + ApiHandler.cancelAllRequests("Token expired - refreshing"); + + ApiHandler.isRefreshing = true; + + try { + final newToken = await refreshTokenCallback(); + if (newToken == null) throw Exception("Refresh failed"); + + // تولید CancelToken جدید برای درخواست‌های بعدی + ApiHandler.reset(); + + final opts = err.requestOptions; + opts.headers['Authorization'] = 'Bearer $newToken'; + + + final dio = Dio(); + final cloneReq = await dio.fetch(opts); + handler.resolve(cloneReq); + return; + } catch (e) { + if (!ApiHandler.isRedirecting) { + ApiHandler.isRedirecting = true; + ApiHandler.cancelAllRequests("Cancel All Requests - Unauthorized"); + // TODO: Navigate to login + Get.offAllNamed('/login'); + } + handler.reject(err); + } finally { + ApiHandler.isRefreshing = false; + } + } else if (err.type == DioExceptionType.cancel) { + + handler.next(err); + } else { + handler.next(err); + } + } +} \ No newline at end of file diff --git a/packages/core/lib/infrastructure/remote/dio_remote.dart b/packages/core/lib/infrastructure/remote/dio_remote.dart index bec2bf5..e87c10f 100644 --- a/packages/core/lib/infrastructure/remote/dio_remote.dart +++ b/packages/core/lib/infrastructure/remote/dio_remote.dart @@ -1,18 +1,18 @@ import 'package:flutter/foundation.dart'; import 'package:rasadyar_core/core.dart'; -import 'package:rasadyar_core/infrastructure/remote/interfaces/i_form_data.dart'; -import 'interfaces/i_http_client.dart'; class DioRemote implements IHttpClient { String? baseUrl; late final Dio _dio; + final List interceptors; - DioRemote({this.baseUrl}); + DioRemote({this.baseUrl, this.interceptors = const []}); @override Future init() async { final dio = Dio(BaseOptions(baseUrl: baseUrl ?? '')); + dio.interceptors.addAll(interceptors); if (kDebugMode) { dio.interceptors.add( PrettyDioLogger( @@ -39,6 +39,7 @@ class DioRemote implements IHttpClient { queryParameters: queryParameters, options: Options(headers: headers), onReceiveProgress: onReceiveProgress, + cancelToken: ApiHandler.globalCancelToken ); if (fromJsonList != null && response.data is List) { response.data = fromJsonList(response.data); @@ -68,6 +69,7 @@ class DioRemote implements IHttpClient { options: Options(headers: headers), onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, + cancelToken: ApiHandler.globalCancelToken ); if (fromJson != null) { @@ -98,6 +100,7 @@ class DioRemote implements IHttpClient { options: Options(headers: headers), onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, + cancelToken: ApiHandler.globalCancelToken ); return DioResponse(response); } @@ -114,6 +117,7 @@ class DioRemote implements IHttpClient { data: data, queryParameters: queryParameters, options: Options(headers: headers), + cancelToken: ApiHandler.globalCancelToken ); return DioResponse(response); } @@ -127,6 +131,7 @@ class DioRemote implements IHttpClient { url, options: Options(responseType: ResponseType.bytes), onReceiveProgress: onReceiveProgress, + cancelToken: ApiHandler.globalCancelToken ); return DioResponse(response); } @@ -143,6 +148,7 @@ class DioRemote implements IHttpClient { data: (formData as DioFormData).raw, options: Options(headers: headers, contentType: 'multipart/form-data'), onSendProgress: onSendProgress, + cancelToken: ApiHandler.globalCancelToken ); return DioResponse(response); } diff --git a/packages/core/lib/utils/network/safe_call_utils.dart b/packages/core/lib/utils/network/safe_call_utils.dart index 882a549..3b52161 100644 --- a/packages/core/lib/utils/network/safe_call_utils.dart +++ b/packages/core/lib/utils/network/safe_call_utils.dart @@ -1,7 +1,32 @@ -import 'package:flutter/foundation.dart'; - import '../../core.dart'; +class ApiHandler { + static bool _isRefreshing = false; + static bool _isRedirecting = false; + static CancelToken globalCancelToken = CancelToken(); + + static Future reset() async { + _isRefreshing = false; + _isRedirecting = false; + globalCancelToken = CancelToken(); + } + + // متد جدید برای کنسل کردن همه درخواست‌ها + static void cancelAllRequests(String reason) { + if (!globalCancelToken.isCancelled) { + globalCancelToken.cancel(reason); + } + // CancelToken جدید بساز برای درخواست‌های بعدی + globalCancelToken = CancelToken(); + } + + static bool get isRefreshing => _isRefreshing; + static set isRefreshing(bool val) => _isRefreshing = val; + + static bool get isRedirecting => _isRedirecting; + static set isRedirecting(bool val) => _isRedirecting = val; +} + typedef AppAsyncCallback = Future Function(); typedef ErrorCallback = void Function(dynamic error, StackTrace? stackTrace); typedef VoidCallback = void Function(); @@ -9,7 +34,6 @@ typedef VoidCallback = void Function(); /// this is global safe call function /// A utility function to safely cal l an asynchronous function with error /// handling and optional loading, success, and error messages. -/// Future gSafeCall({ required AppAsyncCallback call, Function(T result)? onSuccess, @@ -20,62 +44,26 @@ Future gSafeCall({ bool showSuccess = false, bool showToast = false, bool showSnackBar = false, - bool retryOnAuthError = false, - Function()? onTokenRefresh, Function()? onShowLoading, Function()? onHideLoading, Function()? onShowSuccessMessage, Function()? onShowErrorMessage, }) async { try { - if (showLoading) { - (onShowLoading ?? _defaultShowLoading)(); - } - + if (showLoading) (onShowLoading ?? _defaultShowLoading)(); final result = await call(); - - if (showSuccess) { - (onShowSuccessMessage ?? _defaultShowSuccessMessage)(); - } - + if (showSuccess) (onShowSuccessMessage ?? _defaultShowSuccessMessage)(); onSuccess?.call(result); } catch (error, stackTrace) { - if (retryOnAuthError && isTokenExpiredError(error)) { - try { - await onTokenRefresh?.call(); - final retryResult = await call(); - if (showSuccess) { - (onShowSuccessMessage ?? _defaultShowSuccessMessage)(); - } - onSuccess?.call(retryResult); - return; - } catch (retryError, retryStackTrace) { - if (showError) { - (onShowErrorMessage ?? _defaultShowErrorMessage)(); - } - onError?.call(retryError, retryStackTrace); - if (kDebugMode) { - print('safeCall retry error: $retryError\n$retryStackTrace'); - } - } - } else { - if (showError) { - (onShowErrorMessage ?? _defaultShowErrorMessage)(); - } - onError?.call(error, stackTrace); - if (kDebugMode) { - print('safeCall error: $error\n$stackTrace'); - } - } + if (showError) (onShowErrorMessage ?? _defaultShowErrorMessage)(); + onError?.call(error, stackTrace); } finally { - if (showLoading) { - (onHideLoading ?? _defaultHideLoading)(); - } - + if (showLoading) (onHideLoading ?? _defaultHideLoading)(); onComplete?.call(); } } + void _defaultShowLoading() { // پیاده‌سازی پیش‌فرض }