docs: update Flutter auth plan with refresh token auto-refresh

This commit is contained in:
qzl
2026-02-25 14:25:17 +08:00
parent 070ee9b122
commit 53c72e48e6
@@ -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));
}
```