fix : add cancel Token

This commit is contained in:
2025-06-23 10:42:29 +03:30
parent ec0e344e47
commit 1e3614ed58
10 changed files with 140 additions and 88 deletions

View File

@@ -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/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/repositories/auth_repository_imp.dart';
import 'package:rasadyar_auth/data/services/token_storage_service.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import '../common/dio_manager.dart'; import '../common/dio_manager.dart';
@@ -9,8 +10,27 @@ GetIt diAuth = GetIt.instance;
Future<void> setupAuthDI() async { Future<void> setupAuthDI() async {
diAuth.registerLazySingleton(() => DioRemoteManager()); diAuth.registerLazySingleton(() => DioRemoteManager());
diAuth.registerLazySingleton<DioRemote>(() => DioRemote()); diAuth.registerLazySingleton<AppInterceptor>(
() => AppInterceptor(
refreshTokenCallback: () async {
var tokenService = Get.find<TokenStorageService>();
final authRepo = diAuth.get<AuthRepositoryImpl>();
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>(() => DioRemote(interceptors: [diAuth.get<AppInterceptor>()]));
final dioRemote = diAuth.get<DioRemote>(); final dioRemote = diAuth.get<DioRemote>();
await dioRemote.init(); await dioRemote.init();
@@ -19,11 +39,11 @@ Future<void> setupAuthDI() async {
} }
Future<void> newSetupAuthDI(String newUrl) async { Future<void> newSetupAuthDI(String newUrl) async {
diAuth.registerLazySingleton<DioRemote>(() => DioRemote(baseUrl: newUrl),instanceName: 'newRemote'); diAuth.registerLazySingleton<DioRemote>(
() => DioRemote(baseUrl: newUrl, interceptors: [diAuth.get<AppInterceptor>()]),
instanceName: 'newRemote',
);
final dioRemote = diAuth.get<DioRemote>(instanceName: 'newRemote'); final dioRemote = diAuth.get<DioRemote>(instanceName: 'newRemote');
await dioRemote.init(); await dioRemote.init();
diAuth.registerSingleton<AuthRepositoryImpl>( diAuth.registerSingleton<AuthRepositoryImpl>(AuthRepositoryImpl(dioRemote), instanceName: 'newUrl');
AuthRepositoryImpl(dioRemote),
instanceName: 'newUrl',
);
} }

View File

@@ -1,11 +1,9 @@
import 'package:flutter/foundation.dart';
import 'package:rasadyar_auth/auth.dart'; import 'package:rasadyar_auth/auth.dart';
import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart'; import 'package:rasadyar_auth/data/repositories/auth_repository_imp.dart';
import 'package:rasadyar_core/core.dart'; import 'package:rasadyar_core/core.dart';
import '../models/response/auth/auth_response_model.dart'; import '../models/response/auth/auth_response_model.dart';
import '../services/token_storage_service.dart'; import '../services/token_storage_service.dart';
Future<void> safeCall<T>({ Future<void> safeCall<T>({
required AppAsyncCallback<T> call, required AppAsyncCallback<T> call,
Function(T result)? onSuccess, Function(T result)? onSuccess,
@@ -21,9 +19,6 @@ Future<void> safeCall<T>({
Function()? onShowSuccessMessage, Function()? onShowSuccessMessage,
Function()? onShowErrorMessage, Function()? onShowErrorMessage,
}) { }) {
final authRepository = diAuth.get<AuthRepositoryImpl>();
TokenStorageService tokenStorageService = Get.find<TokenStorageService>();
return gSafeCall( return gSafeCall(
call: call, call: call,
onSuccess: onSuccess, onSuccess: onSuccess,
@@ -38,24 +33,6 @@ Future<void> safeCall<T>({
onHideLoading: onHideLoading, onHideLoading: onHideLoading,
onShowSuccessMessage: onShowSuccessMessage, onShowSuccessMessage: onShowSuccessMessage,
onShowErrorMessage: onShowErrorMessage, 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;
});
},
); );
} }

View File

@@ -19,7 +19,7 @@ import '../models/request/create_steward_free_bar/create_steward_free_bar.dart';
abstract class ChickenRepository { abstract class ChickenRepository {
Future<List<InventoryModel>?> getInventory({required String token}); Future<List<InventoryModel>?> getInventory({required String token});
Future<KillHouseDistributionInfo?> getIKillHouseDistributionInfo({required String token}); Future<KillHouseDistributionInfo?> getKillHouseDistributionInfo({required String token});
Future<BarInformation?> getGeneralBarInformation({required String token, Map<String, dynamic>? queryParameters}); Future<BarInformation?> getGeneralBarInformation({required String token, Map<String, dynamic>? queryParameters});

View File

@@ -35,7 +35,7 @@ class ChickenRepositoryImpl implements ChickenRepository {
} }
@override @override
Future<KillHouseDistributionInfo?> getIKillHouseDistributionInfo({required String token}) async { Future<KillHouseDistributionInfo?> getKillHouseDistributionInfo({required String token}) async {
var res = await _httpClient.get( var res = await _httpClient.get(
'/kill-house-distribution-info/?role=Steward', '/kill-house-distribution-info/?role=Steward',
headers: {'Authorization': 'Bearer $token'}, headers: {'Authorization': 'Bearer $token'},

View File

@@ -72,7 +72,7 @@ class HomeLogic extends GetxController {
Future<void> getDistributionInformation() async { Future<void> getDistributionInformation() async {
await safeCall<KillHouseDistributionInfo?>( await safeCall<KillHouseDistributionInfo?>(
call: () async => await rootLogic.chickenRepository.getIKillHouseDistributionInfo(token: rootLogic.tokenService.accessToken.value!), call: () async => await rootLogic.chickenRepository.getKillHouseDistributionInfo(token: rootLogic.tokenService.accessToken.value!),
onSuccess: (result) { onSuccess: (result) {
if (result != null) { if (result != null) {
killHouseDistributionInfo.value = result; killHouseDistributionInfo.value = result;

View File

@@ -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/utils/utils.dart';
export 'package:rasadyar_core/presentation/widget/widget.dart'; export 'package:rasadyar_core/presentation/widget/widget.dart';
export 'infrastructure/local/hive_local_storage.dart'; //infrastructure
//network export 'infrastructure/infrastructure.dart';
export 'infrastructure/remote/dio_form_data.dart';
export 'infrastructure/remote/dio_remote.dart';
export 'infrastructure/remote/dio_response.dart';
export 'injection/di.dart';
///image picker ///image picker
export 'package:image_picker/image_picker.dart'; export 'package:image_picker/image_picker.dart';

View File

@@ -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';

View File

@@ -0,0 +1,51 @@
import 'dart:async';
import '../../core.dart';
typedef RefreshTokenCallback = Future<String?> Function();
class AppInterceptor extends Interceptor {
final RefreshTokenCallback refreshTokenCallback;
AppInterceptor({required this.refreshTokenCallback});
@override
Future<void> 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);
}
}
}

View File

@@ -1,18 +1,18 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:rasadyar_core/core.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 { class DioRemote implements IHttpClient {
String? baseUrl; String? baseUrl;
late final Dio _dio; late final Dio _dio;
final List<Interceptor> interceptors;
DioRemote({this.baseUrl}); DioRemote({this.baseUrl, this.interceptors = const []});
@override @override
Future<void> init() async { Future<void> init() async {
final dio = Dio(BaseOptions(baseUrl: baseUrl ?? '')); final dio = Dio(BaseOptions(baseUrl: baseUrl ?? ''));
dio.interceptors.addAll(interceptors);
if (kDebugMode) { if (kDebugMode) {
dio.interceptors.add( dio.interceptors.add(
PrettyDioLogger( PrettyDioLogger(
@@ -39,6 +39,7 @@ class DioRemote implements IHttpClient {
queryParameters: queryParameters, queryParameters: queryParameters,
options: Options(headers: headers), options: Options(headers: headers),
onReceiveProgress: onReceiveProgress, onReceiveProgress: onReceiveProgress,
cancelToken: ApiHandler.globalCancelToken
); );
if (fromJsonList != null && response.data is List) { if (fromJsonList != null && response.data is List) {
response.data = fromJsonList(response.data); response.data = fromJsonList(response.data);
@@ -68,6 +69,7 @@ class DioRemote implements IHttpClient {
options: Options(headers: headers), options: Options(headers: headers),
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress, onReceiveProgress: onReceiveProgress,
cancelToken: ApiHandler.globalCancelToken
); );
if (fromJson != null) { if (fromJson != null) {
@@ -98,6 +100,7 @@ class DioRemote implements IHttpClient {
options: Options(headers: headers), options: Options(headers: headers),
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress, onReceiveProgress: onReceiveProgress,
cancelToken: ApiHandler.globalCancelToken
); );
return DioResponse<T>(response); return DioResponse<T>(response);
} }
@@ -114,6 +117,7 @@ class DioRemote implements IHttpClient {
data: data, data: data,
queryParameters: queryParameters, queryParameters: queryParameters,
options: Options(headers: headers), options: Options(headers: headers),
cancelToken: ApiHandler.globalCancelToken
); );
return DioResponse<T>(response); return DioResponse<T>(response);
} }
@@ -127,6 +131,7 @@ class DioRemote implements IHttpClient {
url, url,
options: Options(responseType: ResponseType.bytes), options: Options(responseType: ResponseType.bytes),
onReceiveProgress: onReceiveProgress, onReceiveProgress: onReceiveProgress,
cancelToken: ApiHandler.globalCancelToken
); );
return DioResponse(response); return DioResponse(response);
} }
@@ -143,6 +148,7 @@ class DioRemote implements IHttpClient {
data: (formData as DioFormData).raw, data: (formData as DioFormData).raw,
options: Options(headers: headers, contentType: 'multipart/form-data'), options: Options(headers: headers, contentType: 'multipart/form-data'),
onSendProgress: onSendProgress, onSendProgress: onSendProgress,
cancelToken: ApiHandler.globalCancelToken
); );
return DioResponse<T>(response); return DioResponse<T>(response);
} }

View File

@@ -1,7 +1,32 @@
import 'package:flutter/foundation.dart';
import '../../core.dart'; import '../../core.dart';
class ApiHandler {
static bool _isRefreshing = false;
static bool _isRedirecting = false;
static CancelToken globalCancelToken = CancelToken();
static Future<void> 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<T> = Future<T> Function(); typedef AppAsyncCallback<T> = Future<T> Function();
typedef ErrorCallback = void Function(dynamic error, StackTrace? stackTrace); typedef ErrorCallback = void Function(dynamic error, StackTrace? stackTrace);
typedef VoidCallback = void Function(); typedef VoidCallback = void Function();
@@ -9,7 +34,6 @@ typedef VoidCallback = void Function();
/// this is global safe call function /// this is global safe call function
/// A utility function to safely cal l an asynchronous function with error /// A utility function to safely cal l an asynchronous function with error
/// handling and optional loading, success, and error messages. /// handling and optional loading, success, and error messages.
///
Future<void> gSafeCall<T>({ Future<void> gSafeCall<T>({
required AppAsyncCallback<T> call, required AppAsyncCallback<T> call,
Function(T result)? onSuccess, Function(T result)? onSuccess,
@@ -20,62 +44,26 @@ Future<void> gSafeCall<T>({
bool showSuccess = false, bool showSuccess = false,
bool showToast = false, bool showToast = false,
bool showSnackBar = false, bool showSnackBar = false,
bool retryOnAuthError = false,
Function()? onTokenRefresh,
Function()? onShowLoading, Function()? onShowLoading,
Function()? onHideLoading, Function()? onHideLoading,
Function()? onShowSuccessMessage, Function()? onShowSuccessMessage,
Function()? onShowErrorMessage, Function()? onShowErrorMessage,
}) async { }) async {
try { try {
if (showLoading) { if (showLoading) (onShowLoading ?? _defaultShowLoading)();
(onShowLoading ?? _defaultShowLoading)();
}
final result = await call(); final result = await call();
if (showSuccess) (onShowSuccessMessage ?? _defaultShowSuccessMessage)();
if (showSuccess) {
(onShowSuccessMessage ?? _defaultShowSuccessMessage)();
}
onSuccess?.call(result); onSuccess?.call(result);
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (retryOnAuthError && isTokenExpiredError(error)) { if (showError) (onShowErrorMessage ?? _defaultShowErrorMessage)();
try { onError?.call(error, stackTrace);
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');
}
}
} finally { } finally {
if (showLoading) { if (showLoading) (onHideLoading ?? _defaultHideLoading)();
(onHideLoading ?? _defaultHideLoading)();
}
onComplete?.call(); onComplete?.call();
} }
} }
void _defaultShowLoading() { void _defaultShowLoading() {
// پیاده‌سازی پیش‌فرض // پیاده‌سازی پیش‌فرض
} }