add refresh token
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/api_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
import 'api_interceptor.dart';
|
||||
import 'refresh_token_interceptor.dart';
|
||||
|
||||
/// Dio HTTP client configuration
|
||||
class DioClient {
|
||||
late final Dio _dio;
|
||||
String? _authToken;
|
||||
final SecureStorage? secureStorage;
|
||||
|
||||
DioClient() {
|
||||
DioClient({this.secureStorage}) {
|
||||
_dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: ApiConstants.fullBaseUrl,
|
||||
@@ -34,6 +37,17 @@ class DioClient {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Add refresh token interceptor (if secureStorage is provided)
|
||||
if (secureStorage != null) {
|
||||
_dio.interceptors.add(
|
||||
RefreshTokenInterceptor(
|
||||
dio: _dio,
|
||||
secureStorage: secureStorage!,
|
||||
),
|
||||
);
|
||||
print('🔧 DioClient: Refresh token interceptor added');
|
||||
}
|
||||
}
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
104
lib/core/network/refresh_token_interceptor.dart
Normal file
104
lib/core/network/refresh_token_interceptor.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../constants/api_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
/// Interceptor to handle automatic token refresh on 401 errors
|
||||
class RefreshTokenInterceptor extends Interceptor {
|
||||
final Dio dio;
|
||||
final SecureStorage secureStorage;
|
||||
|
||||
// To prevent infinite loop of refresh attempts
|
||||
bool _isRefreshing = false;
|
||||
|
||||
RefreshTokenInterceptor({
|
||||
required this.dio,
|
||||
required this.secureStorage,
|
||||
});
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
||||
// Check if error is 401 Unauthorized
|
||||
if (err.response?.statusCode == 401) {
|
||||
print('🔄 Interceptor: Got 401 error, attempting token refresh...');
|
||||
|
||||
// Avoid infinite refresh loop
|
||||
if (_isRefreshing) {
|
||||
print('❌ Interceptor: Already refreshing, skip');
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
// Check if this is NOT the refresh token endpoint itself
|
||||
final requestPath = err.requestOptions.path;
|
||||
if (requestPath.contains('refresh')) {
|
||||
print('❌ Interceptor: 401 on refresh endpoint, cannot retry');
|
||||
// Clear tokens as refresh token is invalid
|
||||
await secureStorage.deleteAllTokens();
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
try {
|
||||
_isRefreshing = true;
|
||||
|
||||
// Get refresh token from storage
|
||||
final refreshToken = await secureStorage.getRefreshToken();
|
||||
if (refreshToken == null) {
|
||||
print('❌ Interceptor: No refresh token available');
|
||||
await secureStorage.deleteAllTokens();
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
print('🔄 Interceptor: Calling refresh token API...');
|
||||
|
||||
// Call refresh token API
|
||||
final response = await dio.post(
|
||||
ApiConstants.refreshToken,
|
||||
data: {'refreshToken': refreshToken},
|
||||
options: Options(
|
||||
headers: {
|
||||
// Don't include auth header for refresh request
|
||||
ApiConstants.authorization: null,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// Extract new tokens from response
|
||||
final responseData = response.data['data'] as Map<String, dynamic>;
|
||||
final newAccessToken = responseData['access_token'] as String;
|
||||
final newRefreshToken = responseData['refresh_token'] as String;
|
||||
|
||||
print('✅ Interceptor: Got new tokens, saving...');
|
||||
|
||||
// Save new tokens
|
||||
await secureStorage.saveAccessToken(newAccessToken);
|
||||
await secureStorage.saveRefreshToken(newRefreshToken);
|
||||
|
||||
// Update the failed request with new token
|
||||
err.requestOptions.headers[ApiConstants.authorization] = 'Bearer $newAccessToken';
|
||||
|
||||
print('🔄 Interceptor: Retrying original request...');
|
||||
|
||||
// Retry the original request
|
||||
final retryResponse = await dio.fetch(err.requestOptions);
|
||||
|
||||
print('✅ Interceptor: Original request succeeded after refresh');
|
||||
_isRefreshing = false;
|
||||
return handler.resolve(retryResponse);
|
||||
} else {
|
||||
print('❌ Interceptor: Refresh token API returned ${response.statusCode}');
|
||||
await secureStorage.deleteAllTokens();
|
||||
_isRefreshing = false;
|
||||
return handler.next(err);
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Interceptor: Error during token refresh: $e');
|
||||
await secureStorage.deleteAllTokens();
|
||||
_isRefreshing = false;
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Not a 401 error, pass through
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@ part 'core_providers.g.dart';
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
/// It's configured with interceptors, timeout settings, auth token injection,
|
||||
/// and automatic token refresh on 401 errors.
|
||||
@Riverpod(keepAlive: true)
|
||||
DioClient dioClient(Ref ref) {
|
||||
return DioClient();
|
||||
final storage = ref.watch(secureStorageProvider);
|
||||
return DioClient(secureStorage: storage);
|
||||
}
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
|
||||
@@ -11,7 +11,8 @@ part of 'core_providers.dart';
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
/// It's configured with interceptors, timeout settings, auth token injection,
|
||||
/// and automatic token refresh on 401 errors.
|
||||
|
||||
@ProviderFor(dioClient)
|
||||
const dioClientProvider = DioClientProvider._();
|
||||
@@ -19,7 +20,8 @@ const dioClientProvider = DioClientProvider._();
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
/// It's configured with interceptors, timeout settings, auth token injection,
|
||||
/// and automatic token refresh on 401 errors.
|
||||
|
||||
final class DioClientProvider
|
||||
extends $FunctionalProvider<DioClient, DioClient, DioClient>
|
||||
@@ -27,7 +29,8 @@ final class DioClientProvider
|
||||
/// Provider for DioClient (singleton)
|
||||
///
|
||||
/// This is the global HTTP client used across the entire app.
|
||||
/// It's configured with interceptors, timeout settings, and auth token injection.
|
||||
/// It's configured with interceptors, timeout settings, auth token injection,
|
||||
/// and automatic token refresh on 401 errors.
|
||||
const DioClientProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
@@ -61,7 +64,7 @@ final class DioClientProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$dioClientHash() => r'895f0dc2f8d5eab562ad65390e5c6d4a1f722b0d';
|
||||
String _$dioClientHash() => r'a9edc35e0e918bfa8e6c4e3ecd72412fba383cb2';
|
||||
|
||||
/// Provider for SecureStorage (singleton)
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user