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 { class ApiClient {
final Dio _dio; final Dio _dio;
final TokenStorage _tokenStorage; final TokenStorage _tokenStorage;
final Future<AuthResponse> Function(String)? _refreshToken;
ApiClient({ ApiClient({
required String baseUrl, required String baseUrl,
required TokenStorage tokenStorage, required TokenStorage tokenStorage,
Dio? dio, Dio? dio,
Future<AuthResponse> Function(String)? refreshToken,
}) : _tokenStorage = tokenStorage, }) : _tokenStorage = tokenStorage,
_refreshToken = refreshToken,
_dio = dio ?? Dio(BaseOptions(baseUrl: baseUrl)) { _dio = dio ?? Dio(BaseOptions(baseUrl: baseUrl)) {
_dio.interceptors.add(ApiInterceptor( _dio.interceptors.add(ApiInterceptor(
tokenStorage: _tokenStorage, tokenStorage: _tokenStorage,
@@ -270,7 +273,18 @@ class ApiClient {
Dio get dio => _dio; Dio get dio => _dio;
Future<bool> _handleTokenRefresh() async { 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 { 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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:social_app/features/auth/data/auth_repository.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_bloc.dart';
import 'package:social_app/features/auth/presentation/bloc/auth_event.dart'; import 'package:social_app/features/auth/presentation/bloc/auth_event.dart';
import 'package:social_app/features/auth/presentation/bloc/auth_state.dart'; import 'package:social_app/features/auth/presentation/bloc/auth_state.dart';
@@ -842,15 +857,34 @@ void main() {
); );
blocTest<AuthBloc, AuthState>( blocTest<AuthBloc, AuthState>(
'emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid token', 'emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid refresh token',
build: () { 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; return authBloc;
}, },
act: (bloc) => bloc.add(AuthStarted()), act: (bloc) => bloc.add(AuthStarted()),
expect: () => [AuthLoading(), isA<AuthAuthenticated>()], 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>( blocTest<AuthBloc, AuthState>(
'emits [AuthUnauthenticated] when AuthLoggedOut', 'emits [AuthUnauthenticated] when AuthLoggedOut',
build: () { build: () {
@@ -960,14 +994,19 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
Future<void> _onStarted(AuthStarted event, Emitter<AuthState> emit) async { Future<void> _onStarted(AuthStarted event, Emitter<AuthState> emit) async {
emit(AuthLoading()); emit(AuthLoading());
final token = await _repository.getAccessToken(); final refreshToken = await _repository.getRefreshToken();
if (token != null) { if (refreshToken != null) {
emit(const AuthAuthenticated( try {
user: AuthUser(id: '', email: ''), final response = await _repository.refresh(refreshToken);
)); emit(AuthAuthenticated(
} else { user: AuthUser(id: response.user.id, email: response.user.email),
emit(AuthUnauthenticated()); ));
return;
} catch (_) {
await _repository.logout();
}
} }
emit(AuthUnauthenticated());
} }
void _onLoggedIn(AuthLoggedIn event, Emitter<AuthState> emit) { void _onLoggedIn(AuthLoggedIn event, Emitter<AuthState> emit) {
@@ -1474,21 +1513,35 @@ final sl = GetIt.instance;
Future<void> configureDependencies() async { Future<void> configureDependencies() async {
final dio = Dio(BaseOptions(baseUrl: Env.apiUrl)); final dio = Dio(BaseOptions(baseUrl: Env.apiUrl));
final secureStorage = const FlutterSecureStorage(); final secureStorage = const FlutterSecureStorage();
final tokenStorage = SecureTokenStorage(secureStorage);
sl.registerSingleton<Dio>(dio); sl.registerSingleton<Dio>(dio);
sl.registerSingleton<FlutterSecureStorage>(secureStorage); 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( sl.registerSingleton<ApiClient>(ApiClient(
baseUrl: Env.apiUrl, baseUrl: Env.apiUrl,
tokenStorage: sl<TokenStorage>(), tokenStorage: tokenStorage,
dio: sl<Dio>(), dio: dio,
refreshToken: (token) => authRepository.refresh(token),
)); ));
sl.registerSingleton<AuthApi>(AuthApi(sl<ApiClient>()));
sl.registerSingleton<AuthRepository>(AuthRepositoryImpl( sl.registerSingleton<AuthBloc>(AuthBloc(authRepository));
api: sl<AuthApi>(),
tokenStorage: sl<TokenStorage>(),
));
sl.registerSingleton<AuthBloc>(AuthBloc(sl<AuthRepository>()));
} }
``` ```