feat : new injection logic
test : some file :) chore : upgrade android gradle
This commit is contained in:
168
packages/core/lib/infrastructure/remote/app_interceptor_n.dart
Normal file
168
packages/core/lib/infrastructure/remote/app_interceptor_n.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'dart:async';
|
||||
|
||||
import '../../core.dart';
|
||||
|
||||
/// Callback to refresh the authentication token.
|
||||
/// Typically used to request a new token from the server.
|
||||
typedef RefreshTokenCallback = Future<String?> Function();
|
||||
|
||||
/// Callback to save a new authentication token.
|
||||
typedef SaveTokenCallback = Future<void> Function(String token);
|
||||
|
||||
/// Callback to clear the authentication token, e.g., on logout or failure.
|
||||
typedef ClearTokenCallback = Future<void> Function();
|
||||
|
||||
/// Callback invoked when token refresh fails.
|
||||
/// Typically used to redirect the user to login or show a logout message.
|
||||
typedef OnRefreshFailedCallback = Future<void> Function();
|
||||
|
||||
/// Represents a queued request waiting for token refresh.
|
||||
class QueuedRequest {
|
||||
/// The original request options.
|
||||
final RequestOptions options;
|
||||
|
||||
/// Completer used to complete the response once the request is retried.
|
||||
final Completer<Response> completer;
|
||||
|
||||
/// Constructs a queued request.
|
||||
QueuedRequest(this.options, this.completer);
|
||||
}
|
||||
|
||||
/// An interceptor for automatic token management and refresh handling.
|
||||
///
|
||||
/// Features:
|
||||
/// - Queues requests while a token refresh is in progress.
|
||||
/// - Saves and clears tokens via provided callbacks.
|
||||
/// - Calls [OnRefreshFailedCallback] if token refresh fails.
|
||||
class AppInterceptorN extends Interceptor {
|
||||
/// Callback to refresh the authentication token.
|
||||
final RefreshTokenCallback? refreshTokenCallback;
|
||||
|
||||
/// Callback to save the new token.
|
||||
final SaveTokenCallback saveTokenCallback;
|
||||
|
||||
/// Callback to clear the token.
|
||||
final ClearTokenCallback clearTokenCallback;
|
||||
|
||||
/// Callback executed when token refresh fails.
|
||||
final OnRefreshFailedCallback onRefreshFailed;
|
||||
|
||||
/// Optional additional arguments for authentication.
|
||||
final dynamic authArguments;
|
||||
|
||||
/// The Dio instance used to send requests.
|
||||
final Dio dio;
|
||||
|
||||
/// Maximum number of retry attempts for failed requests.
|
||||
final int maxRetries;
|
||||
|
||||
/// Whether a token refresh is currently in progress.
|
||||
bool _isRefreshing = false;
|
||||
|
||||
/// Queue of requests waiting for a new token.
|
||||
final List<QueuedRequest> _queue = [];
|
||||
|
||||
/// Current token in use.
|
||||
String? _currentToken;
|
||||
|
||||
/// Constructs the interceptor.
|
||||
AppInterceptorN({
|
||||
required this.dio,
|
||||
required this.saveTokenCallback,
|
||||
required this.clearTokenCallback,
|
||||
required this.onRefreshFailed,
|
||||
this.refreshTokenCallback,
|
||||
this.authArguments,
|
||||
this.maxRetries = 3,
|
||||
});
|
||||
|
||||
/// Called before sending a request.
|
||||
/// If a token refresh is in progress, the request is added to the queue.
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
|
||||
if (_isRefreshing) {
|
||||
final completer = Completer<Response>();
|
||||
_queue.add(QueuedRequest(options, completer));
|
||||
return handler.resolve(await completer.future);
|
||||
}
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
/// Called when an error occurs during a request.
|
||||
///
|
||||
/// - If the error is a 401 (unauthorized) and retry count is below `maxRetries`,
|
||||
/// the token is refreshed and queued requests are retried.
|
||||
/// - If the token refresh fails, all queued requests are cancelled and
|
||||
/// [onRefreshFailed] is executed.
|
||||
@override
|
||||
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
int currentRetry = err.requestOptions.extra['retryCount'] ?? 0;
|
||||
|
||||
if (err.response?.statusCode == 401 &&
|
||||
err.type != DioExceptionType.cancel &&
|
||||
currentRetry < maxRetries) {
|
||||
final completer = Completer<Response>();
|
||||
final updatedOptions = err.requestOptions.copyWith(
|
||||
extra: {...err.requestOptions.extra, 'retryCount': currentRetry + 1},
|
||||
);
|
||||
_queue.add(QueuedRequest(updatedOptions, completer));
|
||||
|
||||
if (!_isRefreshing) {
|
||||
_isRefreshing = true;
|
||||
try {
|
||||
final newToken = await refreshTokenCallback?.call();
|
||||
if (newToken != null && newToken.isNotEmpty) {
|
||||
_currentToken = newToken;
|
||||
await saveTokenCallback(newToken);
|
||||
|
||||
for (var req in _queue) {
|
||||
final newOptions = req.options.copyWith(
|
||||
headers: {...req.options.headers, 'Authorization': 'Bearer $newToken'},
|
||||
);
|
||||
dio
|
||||
.fetch(newOptions)
|
||||
.then(req.completer.complete)
|
||||
.catchError(req.completer.completeError);
|
||||
}
|
||||
} else {
|
||||
await clearTokenCallback();
|
||||
await _handleRefreshFailure();
|
||||
for (var req in _queue) {
|
||||
req.completer.completeError(
|
||||
DioException(requestOptions: req.options, type: DioExceptionType.cancel),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
await clearTokenCallback();
|
||||
await _handleRefreshFailure();
|
||||
for (var req in _queue) {
|
||||
req.completer.completeError(e);
|
||||
}
|
||||
} finally {
|
||||
_queue.clear();
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
return handler.resolve(await completer.future);
|
||||
}
|
||||
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
/// Handles token refresh failure:
|
||||
/// - Cancels all ongoing requests via [ApiHandler].
|
||||
/// - Executes external [onRefreshFailed] callback.
|
||||
Future<void> _handleRefreshFailure() async {
|
||||
ApiHandler.cancelAllRequests("Token refresh failed");
|
||||
|
||||
await onRefreshFailed.call();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
set isRefreshingForTest(bool value) => _isRefreshing = value;
|
||||
|
||||
@visibleForTesting
|
||||
List<QueuedRequest> get queue => _queue;
|
||||
}
|
||||
Reference in New Issue
Block a user