docs: update Flutter auth plan with refresh token auto-refresh
This commit is contained in:
@@ -254,12 +254,15 @@ import '../storage/token_storage.dart';
|
||||
class ApiClient {
|
||||
final Dio _dio;
|
||||
final TokenStorage _tokenStorage;
|
||||
final Future<AuthResponse> Function(String)? _refreshToken;
|
||||
|
||||
ApiClient({
|
||||
required String baseUrl,
|
||||
required TokenStorage tokenStorage,
|
||||
Dio? dio,
|
||||
Future<AuthResponse> Function(String)? refreshToken,
|
||||
}) : _tokenStorage = tokenStorage,
|
||||
_refreshToken = refreshToken,
|
||||
_dio = dio ?? Dio(BaseOptions(baseUrl: baseUrl)) {
|
||||
_dio.interceptors.add(ApiInterceptor(
|
||||
tokenStorage: _tokenStorage,
|
||||
@@ -270,7 +273,18 @@ class ApiClient {
|
||||
Dio get dio => _dio;
|
||||
|
||||
Future<bool> _handleTokenRefresh() async {
|
||||
return false;
|
||||
final refreshToken = await _tokenStorage.getRefreshToken();
|
||||
if (refreshToken == null || _refreshToken == null) return false;
|
||||
try {
|
||||
final response = await _refreshToken!(refreshToken);
|
||||
await _tokenStorage.saveTokens(
|
||||
access: response.accessToken,
|
||||
refresh: response.refreshToken,
|
||||
);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) async {
|
||||
@@ -811,6 +825,7 @@ import 'package:bloc_test/bloc_test.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:social_app/features/auth/data/auth_repository.dart';
|
||||
import 'package:social_app/features/auth/data/models/auth_response.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_event.dart';
|
||||
import 'package:social_app/features/auth/presentation/bloc/auth_state.dart';
|
||||
@@ -842,15 +857,34 @@ void main() {
|
||||
);
|
||||
|
||||
blocTest<AuthBloc, AuthState>(
|
||||
'emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid token',
|
||||
'emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid refresh token',
|
||||
build: () {
|
||||
when(() => mockRepository.getAccessToken()).thenAnswer((_) async => 'valid_token');
|
||||
when(() => mockRepository.getRefreshToken()).thenAnswer((_) async => 'valid_refresh');
|
||||
when(() => mockRepository.refresh('valid_refresh')).thenAnswer((_) async => AuthResponse(
|
||||
accessToken: 'new_access',
|
||||
refreshToken: 'new_refresh',
|
||||
expiresIn: 3600,
|
||||
tokenType: 'bearer',
|
||||
user: const AuthUser(id: '123', email: 'test@example.com'),
|
||||
));
|
||||
return authBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(AuthStarted()),
|
||||
expect: () => [AuthLoading(), isA<AuthAuthenticated>()],
|
||||
);
|
||||
|
||||
blocTest<AuthBloc, AuthState>(
|
||||
'emits [AuthLoading, AuthUnauthenticated] when refresh token expired',
|
||||
build: () {
|
||||
when(() => mockRepository.getRefreshToken()).thenAnswer((_) async => 'expired_refresh');
|
||||
when(() => mockRepository.refresh('expired_refresh')).thenThrow(Exception('Invalid refresh token'));
|
||||
when(() => mockRepository.logout()).thenAnswer((_) async {});
|
||||
return authBloc;
|
||||
},
|
||||
act: (bloc) => bloc.add(AuthStarted()),
|
||||
expect: () => [AuthLoading(), AuthUnauthenticated()],
|
||||
);
|
||||
|
||||
blocTest<AuthBloc, AuthState>(
|
||||
'emits [AuthUnauthenticated] when AuthLoggedOut',
|
||||
build: () {
|
||||
@@ -960,14 +994,19 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
|
||||
Future<void> _onStarted(AuthStarted event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
final token = await _repository.getAccessToken();
|
||||
if (token != null) {
|
||||
emit(const AuthAuthenticated(
|
||||
user: AuthUser(id: '', email: ''),
|
||||
));
|
||||
} else {
|
||||
emit(AuthUnauthenticated());
|
||||
final refreshToken = await _repository.getRefreshToken();
|
||||
if (refreshToken != null) {
|
||||
try {
|
||||
final response = await _repository.refresh(refreshToken);
|
||||
emit(AuthAuthenticated(
|
||||
user: AuthUser(id: response.user.id, email: response.user.email),
|
||||
));
|
||||
return;
|
||||
} catch (_) {
|
||||
await _repository.logout();
|
||||
}
|
||||
}
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
|
||||
void _onLoggedIn(AuthLoggedIn event, Emitter<AuthState> emit) {
|
||||
@@ -1474,21 +1513,35 @@ final sl = GetIt.instance;
|
||||
Future<void> configureDependencies() async {
|
||||
final dio = Dio(BaseOptions(baseUrl: Env.apiUrl));
|
||||
final secureStorage = const FlutterSecureStorage();
|
||||
final tokenStorage = SecureTokenStorage(secureStorage);
|
||||
|
||||
sl.registerSingleton<Dio>(dio);
|
||||
sl.registerSingleton<FlutterSecureStorage>(secureStorage);
|
||||
sl.registerSingleton<TokenStorage>(SecureTokenStorage(secureStorage));
|
||||
sl.registerSingleton<TokenStorage>(tokenStorage);
|
||||
|
||||
// Register AuthApi and AuthRepository first (needed for ApiClient refresh)
|
||||
final authApi = AuthApi(ApiClient(
|
||||
baseUrl: Env.apiUrl,
|
||||
tokenStorage: tokenStorage,
|
||||
dio: dio,
|
||||
));
|
||||
sl.registerSingleton<AuthApi>(authApi);
|
||||
|
||||
final authRepository = AuthRepositoryImpl(
|
||||
api: authApi,
|
||||
tokenStorage: tokenStorage,
|
||||
);
|
||||
sl.registerSingleton<AuthRepository>(authRepository);
|
||||
|
||||
// Re-register ApiClient with refresh capability
|
||||
sl.registerSingleton<ApiClient>(ApiClient(
|
||||
baseUrl: Env.apiUrl,
|
||||
tokenStorage: sl<TokenStorage>(),
|
||||
dio: sl<Dio>(),
|
||||
tokenStorage: tokenStorage,
|
||||
dio: dio,
|
||||
refreshToken: (token) => authRepository.refresh(token),
|
||||
));
|
||||
sl.registerSingleton<AuthApi>(AuthApi(sl<ApiClient>()));
|
||||
sl.registerSingleton<AuthRepository>(AuthRepositoryImpl(
|
||||
api: sl<AuthApi>(),
|
||||
tokenStorage: sl<TokenStorage>(),
|
||||
));
|
||||
sl.registerSingleton<AuthBloc>(AuthBloc(sl<AuthRepository>()));
|
||||
|
||||
sl.registerSingleton<AuthBloc>(AuthBloc(authRepository));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user