feat : change app inspector and exception handling
This commit is contained in:
@@ -1,62 +1,101 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../core.dart';
|
||||
|
||||
typedef RefreshTokenCallback = Future<String?> Function();
|
||||
typedef SaveTokenCallback = Future<void> Function(String token);
|
||||
typedef ClearTokenCallback = Future<void> Function();
|
||||
|
||||
class AppInterceptor extends Interceptor {
|
||||
final RefreshTokenCallback refreshTokenCallback;
|
||||
Completer<String?>? _refreshCompleter;
|
||||
final RefreshTokenCallback? refreshTokenCallback;
|
||||
final SaveTokenCallback saveTokenCallback;
|
||||
final ClearTokenCallback clearTokenCallback;
|
||||
late final Dio dio;
|
||||
static Completer<String?>? _refreshCompleter;
|
||||
static bool _isRefreshing = false;
|
||||
|
||||
AppInterceptor({required this.refreshTokenCallback});
|
||||
AppInterceptor({
|
||||
required this.saveTokenCallback,
|
||||
required this.clearTokenCallback,
|
||||
this.refreshTokenCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
if (_isRefreshing && _refreshCompleter != null) {
|
||||
try {
|
||||
final newToken = await _refreshCompleter!.future;
|
||||
if (newToken != null && newToken.isNotEmpty) {
|
||||
options.headers['Authorization'] = 'Bearer $newToken';
|
||||
}
|
||||
} catch (_) {
|
||||
handler.reject(DioException(requestOptions: options, type: DioExceptionType.cancel));
|
||||
return;
|
||||
}
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
if (err.response?.statusCode == 401 && !ApiHandler.isRefreshing) {
|
||||
ApiHandler.cancelAllRequests("Token expired - refreshing");
|
||||
|
||||
if (_refreshCompleter == null) {
|
||||
_refreshCompleter = Completer<String?>();
|
||||
ApiHandler.isRefreshing = true;
|
||||
|
||||
try {
|
||||
final newToken = await refreshTokenCallback();
|
||||
if (newToken == null) throw Exception("Refresh failed");
|
||||
|
||||
ApiHandler.reset();
|
||||
_refreshCompleter?.complete(newToken);
|
||||
} catch (e) {
|
||||
_refreshCompleter?.completeError(e);
|
||||
if (!ApiHandler.isRedirecting) {
|
||||
ApiHandler.isRedirecting = true;
|
||||
ApiHandler.cancelAllRequests("Cancel All Requests - Unauthorized");
|
||||
if (Get.currentRoute != '/Auth') {
|
||||
Get.offAllNamed('/Auth');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ApiHandler.isRefreshing = false;
|
||||
_refreshCompleter = null;
|
||||
}
|
||||
if (err.response?.statusCode == 401) {
|
||||
final retryResult = await _handleUnauthorizedError(err);
|
||||
if (retryResult != null) {
|
||||
handler.resolve(retryResult);
|
||||
return;
|
||||
}
|
||||
}
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
Future<Response?> _handleUnauthorizedError(DioException err) async {
|
||||
if (_isRefreshing && _refreshCompleter != null) {
|
||||
try {
|
||||
final newToken = await _refreshCompleter!.future;
|
||||
if (newToken != null) {
|
||||
final opts = err.requestOptions;
|
||||
opts.headers['Authorization'] = 'Bearer $newToken';
|
||||
|
||||
final dio = Dio();
|
||||
final cloneReq = await dio.fetch(opts);
|
||||
handler.resolve(cloneReq);
|
||||
return;
|
||||
}
|
||||
return newToken != null ? await _retryRequest(err.requestOptions, newToken) : null;
|
||||
} catch (_) {
|
||||
handler.reject(err);
|
||||
return null;
|
||||
}
|
||||
} else if (err.type == DioExceptionType.cancel) {
|
||||
handler.next(err);
|
||||
} else {
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
_refreshCompleter = Completer<String?>();
|
||||
|
||||
try {
|
||||
final newToken = await refreshTokenCallback!();
|
||||
|
||||
if (!_refreshCompleter!.isCompleted) _refreshCompleter!.complete(newToken);
|
||||
|
||||
if (newToken != null) {
|
||||
await saveTokenCallback(newToken); // ✅ ذخیره توکن جدید
|
||||
return await _retryRequest(err.requestOptions, newToken);
|
||||
} else {
|
||||
await clearTokenCallback(); // ✅ پاککردن توکنهای قبلی
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
if (!_refreshCompleter!.isCompleted) _refreshCompleter!.completeError(e);
|
||||
await clearTokenCallback(); // ✅ پاککردن توکن در صورت خطا
|
||||
_handleRefreshFailure();
|
||||
return null;
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
_refreshCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response> _retryRequest(RequestOptions options, String token) async {
|
||||
final newOptions = options.copyWith();
|
||||
newOptions.headers['Authorization'] = 'Bearer $token';
|
||||
return dio.fetch(newOptions);
|
||||
}
|
||||
|
||||
void _handleRefreshFailure() {
|
||||
ApiHandler.cancelAllRequests("Token refresh failed");
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
if (Get.currentRoute != '/Auth') {
|
||||
Get.offAllNamed('/Auth');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:rasadyar_core/core.dart';
|
||||
|
||||
|
||||
class DioRemote implements IHttpClient {
|
||||
String? baseUrl;
|
||||
late final Dio _dio;
|
||||
final List<Interceptor> interceptors;
|
||||
late Dio dio;
|
||||
final AppInterceptor interceptors;
|
||||
|
||||
DioRemote({this.baseUrl, this.interceptors = const []});
|
||||
|
||||
DioRemote({this.baseUrl, required this.interceptors});
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
final dio = Dio(BaseOptions(baseUrl: baseUrl ?? ''));
|
||||
dio.interceptors.addAll(interceptors);
|
||||
dio = Dio(BaseOptions(baseUrl: baseUrl ?? ''));
|
||||
dio.interceptors.add(interceptors);
|
||||
if (kDebugMode) {
|
||||
dio.interceptors.add(
|
||||
PrettyDioLogger(
|
||||
requestHeader: true,
|
||||
responseHeader: true,
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
_dio = dio;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DioResponse<T>> get<T>(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Map<String, String>? headers,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
T Function(Map<String, dynamic> json)? fromJson,
|
||||
T Function(List<dynamic> json)? fromJsonList,
|
||||
}) async {
|
||||
final response = await _dio.get(
|
||||
String path, {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Map<String, String>? headers,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
T Function(Map<String, dynamic> json)? fromJson,
|
||||
T Function(List<dynamic> json)? fromJsonList,
|
||||
}) async {
|
||||
final response = await dio.get(
|
||||
path,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(headers: headers),
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: ApiHandler.globalCancelToken
|
||||
cancelToken: ApiHandler.globalCancelToken,
|
||||
);
|
||||
if (fromJsonList != null && response.data is List) {
|
||||
response.data = fromJsonList(response.data);
|
||||
@@ -62,21 +62,19 @@ class DioRemote implements IHttpClient {
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final response = await _dio.post(
|
||||
final response = await dio.post(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(headers: headers),
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: ApiHandler.globalCancelToken
|
||||
cancelToken: ApiHandler.globalCancelToken,
|
||||
);
|
||||
|
||||
if (fromJson != null) {
|
||||
final rawData = response.data;
|
||||
final parsedData = rawData is Map<String, dynamic>
|
||||
? fromJson(rawData)
|
||||
: null;
|
||||
final parsedData = rawData is Map<String, dynamic> ? fromJson(rawData) : null;
|
||||
response.data = parsedData;
|
||||
return DioResponse<T>(response);
|
||||
}
|
||||
@@ -93,14 +91,14 @@ class DioRemote implements IHttpClient {
|
||||
ProgressCallback? onSendProgress,
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final response = await _dio.put(
|
||||
final response = await dio.put(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(headers: headers),
|
||||
onSendProgress: onSendProgress,
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: ApiHandler.globalCancelToken
|
||||
cancelToken: ApiHandler.globalCancelToken,
|
||||
);
|
||||
return DioResponse<T>(response);
|
||||
}
|
||||
@@ -112,12 +110,12 @@ class DioRemote implements IHttpClient {
|
||||
Map<String, dynamic>? queryParameters,
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
final response = await _dio.delete<T>(
|
||||
final response = await dio.delete<T>(
|
||||
path,
|
||||
data: data,
|
||||
queryParameters: queryParameters,
|
||||
options: Options(headers: headers),
|
||||
cancelToken: ApiHandler.globalCancelToken
|
||||
cancelToken: ApiHandler.globalCancelToken,
|
||||
);
|
||||
return DioResponse<T>(response);
|
||||
}
|
||||
@@ -127,11 +125,11 @@ class DioRemote implements IHttpClient {
|
||||
String url, {
|
||||
ProgressCallback? onReceiveProgress,
|
||||
}) async {
|
||||
final response = await _dio.get<Uint8List>(
|
||||
final response = await dio.get<Uint8List>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
onReceiveProgress: onReceiveProgress,
|
||||
cancelToken: ApiHandler.globalCancelToken
|
||||
cancelToken: ApiHandler.globalCancelToken,
|
||||
);
|
||||
return DioResponse(response);
|
||||
}
|
||||
@@ -143,12 +141,12 @@ class DioRemote implements IHttpClient {
|
||||
Map<String, String>? headers,
|
||||
ProgressCallback? onSendProgress,
|
||||
}) async {
|
||||
final response = await _dio.post(
|
||||
final response = await dio.post(
|
||||
path,
|
||||
data: (formData as DioFormData).raw,
|
||||
options: Options(headers: headers, contentType: 'multipart/form-data'),
|
||||
onSendProgress: onSendProgress,
|
||||
cancelToken: ApiHandler.globalCancelToken
|
||||
cancelToken: ApiHandler.globalCancelToken,
|
||||
);
|
||||
return DioResponse<T>(response);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ Future<void> _setUpLogger() async{
|
||||
|
||||
Future<void> _setupLocalStorage() async {
|
||||
diCore.registerSingleton<HiveLocalStorage>(HiveLocalStorage());
|
||||
print('====> HiveLocalStorage registered');
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,84 +1,169 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core.dart';
|
||||
|
||||
class ApiHandler {
|
||||
static bool _isRefreshing = false;
|
||||
static bool _isRedirecting = false;
|
||||
static CancelToken globalCancelToken = CancelToken();
|
||||
static CancelToken _globalCancelToken = CancelToken();
|
||||
|
||||
static CancelToken get globalCancelToken => _globalCancelToken;
|
||||
|
||||
static Future<void> reset() async {
|
||||
_isRefreshing = false;
|
||||
_isRedirecting = false;
|
||||
globalCancelToken = CancelToken();
|
||||
_globalCancelToken = CancelToken();
|
||||
}
|
||||
|
||||
|
||||
static void cancelAllRequests(String reason) {
|
||||
if (!globalCancelToken.isCancelled) {
|
||||
globalCancelToken.cancel(reason);
|
||||
if (!_globalCancelToken.isCancelled) {
|
||||
_globalCancelToken.cancel(reason);
|
||||
}
|
||||
globalCancelToken = CancelToken();
|
||||
reset();
|
||||
}
|
||||
|
||||
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 ErrorCallback = Function(dynamic error, StackTrace? stackTrace);
|
||||
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<void> gSafeCall<T>({
|
||||
Future<T?> gSafeCall<T>({
|
||||
required AppAsyncCallback<T> call,
|
||||
Function(T result)? onSuccess,
|
||||
ErrorCallback? onError,
|
||||
VoidCallback? onComplete,
|
||||
bool showLoading = false,
|
||||
bool showError = false,
|
||||
bool showError = true,
|
||||
bool showSuccess = false,
|
||||
bool showToast = false,
|
||||
bool showSnackBar = false,
|
||||
Function()? onShowLoading,
|
||||
Function()? onHideLoading,
|
||||
Function()? onShowSuccessMessage,
|
||||
Function()? onShowErrorMessage,
|
||||
Function(String message)? onShowErrorMessage,
|
||||
Function(String message)? onShowSuccessMessage,
|
||||
int maxRetries = 0,
|
||||
Duration retryDelay = const Duration(seconds: 1),
|
||||
}) async {
|
||||
try {
|
||||
if (showLoading) (onShowLoading ?? _defaultShowLoading)();
|
||||
final result = await call();
|
||||
if (showSuccess) (onShowSuccessMessage ?? _defaultShowSuccessMessage)();
|
||||
onSuccess?.call(result);
|
||||
} catch (error, stackTrace) {
|
||||
if (showError) (onShowErrorMessage ?? _defaultShowErrorMessage)();
|
||||
onError?.call(error, stackTrace);
|
||||
} finally {
|
||||
if (showLoading) (onHideLoading ?? _defaultHideLoading)();
|
||||
onComplete?.call();
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
if (showLoading && retryCount == 0) {
|
||||
(onShowLoading ?? _defaultShowLoading)();
|
||||
}
|
||||
|
||||
final result = await call();
|
||||
|
||||
if (showSuccess) {
|
||||
(onShowSuccessMessage ?? _defaultShowSuccessMessage)('عملیات با موفقیت انجام شد');
|
||||
}
|
||||
|
||||
onSuccess?.call(result);
|
||||
return result;
|
||||
|
||||
} catch (error, stackTrace) {
|
||||
retryCount++;
|
||||
|
||||
|
||||
if (error is DioException && error.response?.statusCode == 401) {
|
||||
if (showError) {
|
||||
(onShowErrorMessage ?? _defaultShowErrorMessage)('خطا در احراز هویت');
|
||||
}
|
||||
onError?.call(error, stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (retryCount > maxRetries || !_isRetryableError(error)) {
|
||||
if (showError) {
|
||||
final message = _getErrorMessage(error);
|
||||
(onShowErrorMessage ?? _defaultShowErrorMessage)(message);
|
||||
}
|
||||
onError?.call(error, stackTrace);
|
||||
return null;
|
||||
}
|
||||
|
||||
// صبر قبل از retry
|
||||
if (retryCount <= maxRetries) {
|
||||
await Future.delayed(retryDelay);
|
||||
}
|
||||
} finally {
|
||||
if (showLoading && retryCount > maxRetries) {
|
||||
(onHideLoading ?? _defaultHideLoading)();
|
||||
}
|
||||
if (retryCount > maxRetries) {
|
||||
onComplete?.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _isRetryableError(dynamic error) {
|
||||
if (error is DioException) {
|
||||
// خطاهای قابل retry
|
||||
return error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout ||
|
||||
error.type == DioExceptionType.sendTimeout ||
|
||||
(error.response?.statusCode != null &&
|
||||
error.response!.statusCode! >= 500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
String _getErrorMessage(dynamic error) {
|
||||
if (error is DioException) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
return 'خطا در اتصال - زمان اتصال تمام شد';
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return 'خطا در دریافت پاسخ';
|
||||
case DioExceptionType.sendTimeout:
|
||||
return 'خطا در ارسال درخواست';
|
||||
case DioExceptionType.badCertificate:
|
||||
return 'خطا در گواهی امنیتی';
|
||||
case DioExceptionType.connectionError:
|
||||
return 'خطا در اتصال به سرور';
|
||||
case DioExceptionType.unknown:
|
||||
return 'خطای نامشخص';
|
||||
default:
|
||||
if (error.response?.statusCode != null) {
|
||||
return 'خطا: ${error.response!.statusCode}';
|
||||
}
|
||||
return 'خطای نامشخص';
|
||||
}
|
||||
}
|
||||
return error.toString();
|
||||
}
|
||||
|
||||
void _defaultShowLoading() {
|
||||
// پیادهسازی پیشفرض
|
||||
// نمایش loading
|
||||
Get.dialog(
|
||||
Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _defaultHideLoading() {
|
||||
// پیادهسازی پیشفرض
|
||||
// مخفی کردن loading
|
||||
if (Get.isDialogOpen == true) {
|
||||
Get.back();
|
||||
}
|
||||
}
|
||||
|
||||
void _defaultShowSuccessMessage() {
|
||||
// پیادهسازی پیشفرض
|
||||
void _defaultShowSuccessMessage(String message) {
|
||||
Get.snackbar(
|
||||
'موفقیت',
|
||||
message,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
void _defaultShowErrorMessage() {
|
||||
// پیادهسازی پیشفرض
|
||||
}
|
||||
|
||||
bool isTokenExpiredError(dynamic error) {
|
||||
return error is DioException && error.response?.statusCode == 401;
|
||||
void _defaultShowErrorMessage(String message) {
|
||||
Get.snackbar(
|
||||
'خطا',
|
||||
message,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user