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 {
|
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>()));
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user