# Flutter Auth Integration Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Integrate Flutter mobile app with backend auth APIs for signup and login flows. **Architecture:** Complete Bloc architecture with ApiClient, Repository, AuthBloc for global state, and Cubits for form state. Uses flutter_secure_storage for tokens, dio for HTTP, and go_router for navigation with auth protection. **Tech Stack:** Flutter, flutter_bloc, dio, flutter_secure_storage, formz, get_it, go_router --- ## Task 1: Core API Infrastructure **Files:** - Create: `apps/lib/core/api/api_exception.dart` - Create: `apps/lib/core/storage/token_storage.dart` - Create: `apps/lib/core/api/api_client.dart` - Create: `apps/lib/core/api/api_interceptor.dart` - Test: `apps/test/core/api/api_exception_test.dart` - Test: `apps/test/core/storage/token_storage_test.dart` ### Step 1: Write failing test for ApiException Create `apps/test/core/api/api_exception_test.dart`: ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:social_app/core/api/api_exception.dart'; void main() { group('ApiException', () { test('creates from DioException with 400 status', () { final dioException = Exception('Bad request'); final apiException = ApiException.fromDioError(dioException); expect(apiException, isA()); expect(apiException.message, contains('Request failed')); }); test('NetworkException has correct message', () { const exception = NetworkException('No internet'); expect(exception.message, 'No internet'); }); test('UnauthorizedException has default message', () { const exception = UnauthorizedException(); expect(exception.message, 'Authentication required'); }); }); } ``` ### Step 2: Run test to verify it fails Run: `cd apps && flutter test test/core/api/api_exception_test.dart` Expected: FAIL - file not found ### Step 3: Implement ApiException Create `apps/lib/core/api/api_exception.dart`: ```dart abstract class ApiException implements Exception { final String message; final int? statusCode; const ApiException(this.message, {this.statusCode}); factory ApiException.fromDioError(Object error) { if (error is ApiException) return error; return ServerException(error.toString()); } } class NetworkException extends ApiException { const NetworkException(super.message); } class ServerException extends ApiException { const ServerException(super.message, {super.statusCode}); } class UnauthorizedException extends ApiException { const UnauthorizedException([super.message = 'Authentication required']) : super(statusCode: 401); } class ValidationException extends ApiException { final Map? errors; const ValidationException(super.message, {this.errors, super.statusCode}); } ``` ### Step 4: Run test to verify it passes Run: `cd apps && flutter test test/core/api/api_exception_test.dart` Expected: PASS ### Step 5: Write failing test for TokenStorage Create `apps/test/core/storage/token_storage_test.dart`: ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:social_app/core/storage/token_storage.dart'; class MockTokenStorage extends Mock implements TokenStorage {} void main() { late TokenStorage storage; setUp(() { storage = MockTokenStorage(); }); group('TokenStorage', () { test('saves and retrieves access token', () async { when(() => storage.getAccessToken()).thenAnswer((_) async => 'test_access'); when(() => storage.saveTokens(access: 'test_access', refresh: 'test_refresh')) .thenAnswer((_) async {}); await storage.saveTokens(access: 'test_access', refresh: 'test_refresh'); final token = await storage.getAccessToken(); expect(token, 'test_access'); verify(() => storage.saveTokens(access: 'test_access', refresh: 'test_refresh')).called(1); }); test('clear removes all tokens', () async { when(() => storage.clear()).thenAnswer((_) async {}); when(() => storage.getAccessToken()).thenAnswer((_) async => null); await storage.clear(); final token = await storage.getAccessToken(); expect(token, isNull); }); }); } ``` ### Step 6: Run test to verify it fails Run: `cd apps && flutter test test/core/storage/token_storage_test.dart` Expected: FAIL - file not found ### Step 7: Implement TokenStorage Create `apps/lib/core/storage/token_storage.dart`: ```dart abstract class TokenStorage { Future getAccessToken(); Future getRefreshToken(); Future saveTokens({required String access, required String refresh}); Future clear(); } class SecureTokenStorage implements TokenStorage { static const _accessTokenKey = 'access_token'; static const _refreshTokenKey = 'refresh_token'; final dynamic _storage; SecureTokenStorage([this._storage]); @override Future getAccessToken() async { return _storage?.read(key: _accessTokenKey); } @override Future getRefreshToken() async { return _storage?.read(key: _refreshTokenKey); } @override Future saveTokens({required String access, required String refresh}) async { await _storage?.write(key: _accessTokenKey, value: access); await _storage?.write(key: _refreshTokenKey, value: refresh); } @override Future clear() async { await _storage?.delete(key: _accessTokenKey); await _storage?.delete(key: _refreshTokenKey); } } ``` ### Step 8: Run test to verify it passes Run: `cd apps && flutter test test/core/storage/token_storage_test.dart` Expected: PASS ### Step 9: Implement ApiInterceptor Create `apps/lib/core/api/api_interceptor.dart`: ```dart import 'package:dio/dio.dart'; import '../storage/token_storage.dart'; class ApiInterceptor extends Interceptor { final TokenStorage tokenStorage; final Future Function()? onTokenRefresh; ApiInterceptor({ required this.tokenStorage, this.onTokenRefresh, }); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { final token = await tokenStorage.getAccessToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); } @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode == 401 && onTokenRefresh != null) { final refreshed = await onTokenRefresh!(); if (refreshed) { final token = await tokenStorage.getAccessToken(); if (token != null) { err.requestOptions.headers['Authorization'] = 'Bearer $token'; try { final response = await Dio().fetch(err.requestOptions); handler.resolve(response); return; } catch (_) {} } } } handler.next(err); } } ``` ### Step 10: Implement ApiClient Create `apps/lib/core/api/api_client.dart`: ```dart import 'package:dio/dio.dart'; import 'api_exception.dart'; import 'api_interceptor.dart'; import '../storage/token_storage.dart'; class ApiClient { final Dio _dio; final TokenStorage _tokenStorage; ApiClient({ required String baseUrl, required TokenStorage tokenStorage, Dio? dio, }) : _tokenStorage = tokenStorage, _dio = dio ?? Dio(BaseOptions(baseUrl: baseUrl)) { _dio.interceptors.add(ApiInterceptor( tokenStorage: _tokenStorage, onTokenRefresh: _handleTokenRefresh, )); } Dio get dio => _dio; Future _handleTokenRefresh() async { return false; } Future> get(String path, {Options? options}) async { try { return await _dio.get(path, options: options); } catch (e) { throw ApiException.fromDioError(e); } } Future> post(String path, {dynamic data, Options? options}) async { try { return await _dio.post(path, data: data, options: options); } catch (e) { throw ApiException.fromDioError(e); } } } ``` ### Step 11: Commit core API infrastructure ```bash git add apps/lib/core/api/ apps/lib/core/storage/ apps/test/core/ git commit -m "feat(apps): add core API infrastructure" ``` --- ## Task 2: Auth Data Models **Files:** - Create: `apps/lib/features/auth/data/models/signup_request.dart` - Create: `apps/lib/features/auth/data/models/login_request.dart` - Create: `apps/lib/features/auth/data/models/auth_response.dart` - Test: `apps/test/features/auth/data/models/auth_models_test.dart` ### Step 1: Write failing test for auth models Create `apps/test/features/auth/data/models/auth_models_test.dart`: ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:social_app/features/auth/data/models/signup_request.dart'; import 'package:social_app/features/auth/data/models/login_request.dart'; import 'package:social_app/features/auth/data/models/auth_response.dart'; void main() { group('SignupStartRequest', () { test('serializes to JSON', () { final request = SignupStartRequest( username: 'testuser', email: 'test@example.com', password: 'password123', ); final json = request.toJson(); expect(json['username'], 'testuser'); expect(json['email'], 'test@example.com'); expect(json['password'], 'password123'); }); }); group('LoginRequest', () { test('serializes to JSON', () { final request = LoginRequest( email: 'test@example.com', password: 'password123', ); final json = request.toJson(); expect(json['email'], 'test@example.com'); expect(json['password'], 'password123'); }); }); group('AuthResponse', () { test('parses from JSON', () { final json = { 'access_token': 'test_access', 'refresh_token': 'test_refresh', 'expires_in': 3600, 'token_type': 'bearer', 'user': {'id': '123', 'email': 'test@example.com'}, }; final response = AuthResponse.fromJson(json); expect(response.accessToken, 'test_access'); expect(response.refreshToken, 'test_refresh'); expect(response.expiresIn, 3600); expect(response.user.id, '123'); expect(response.user.email, 'test@example.com'); }); }); } ``` ### Step 2: Run test to verify it fails Run: `cd apps && flutter test test/features/auth/data/models/auth_models_test.dart` Expected: FAIL - file not found ### Step 3: Implement SignupRequest Create `apps/lib/features/auth/data/models/signup_request.dart`: ```dart class SignupStartRequest { final String username; final String email; final String password; const SignupStartRequest({ required this.username, required this.email, required this.password, }); Map toJson() => { 'username': username, 'email': email, 'password': password, }; } class SignupVerifyRequest { final String email; final String token; const SignupVerifyRequest({ required this.email, required this.token, }); Map toJson() => { 'email': email, 'token': token, }; } class SignupResendRequest { final String email; const SignupResendRequest({required this.email}); Map toJson() => {'email': email}; } ``` ### Step 4: Implement LoginRequest Create `apps/lib/features/auth/data/models/login_request.dart`: ```dart class LoginRequest { final String email; final String password; const LoginRequest({ required this.email, required this.password, }); Map toJson() => { 'email': email, 'password': password, }; } class RefreshRequest { final String refreshToken; const RefreshRequest({required this.refreshToken}); Map toJson() => {'refresh_token': refreshToken}; } class LogoutRequest { final String refreshToken; const LogoutRequest({required this.refreshToken}); Map toJson() => {'refresh_token': refreshToken}; } ``` ### Step 5: Implement AuthResponse Create `apps/lib/features/auth/data/models/auth_response.dart`: ```dart class AuthUser { final String id; final String email; const AuthUser({required this.id, required this.email}); factory AuthUser.fromJson(Map json) { return AuthUser( id: json['id'] as String, email: json['email'] as String, ); } } class AuthResponse { final String accessToken; final String refreshToken; final int expiresIn; final String tokenType; final AuthUser user; const AuthResponse({ required this.accessToken, required this.refreshToken, required this.expiresIn, required this.tokenType, required this.user, }); factory AuthResponse.fromJson(Map json) { return AuthResponse( accessToken: json['access_token'] as String, refreshToken: json['refresh_token'] as String, expiresIn: json['expires_in'] as int, tokenType: json['token_type'] as String, user: AuthUser.fromJson(json['user'] as Map), ); } } class SignupStartResponse { final String status; final String email; final String message; const SignupStartResponse({ required this.status, required this.email, required this.message, }); factory SignupStartResponse.fromJson(Map json) { return SignupStartResponse( status: json['status'] as String, email: json['email'] as String, message: json['message'] as String, ); } } ``` ### Step 6: Run test to verify it passes Run: `cd apps && flutter test test/features/auth/data/models/auth_models_test.dart` Expected: PASS ### Step 7: Commit auth data models ```bash git add apps/lib/features/auth/data/models/ apps/test/features/auth/data/ git commit -m "feat(apps): add auth data models" ``` --- ## Task 3: Auth Repository **Files:** - Create: `apps/lib/features/auth/data/auth_api.dart` - Create: `apps/lib/features/auth/data/auth_repository.dart` - Create: `apps/lib/features/auth/data/auth_repository_impl.dart` - Test: `apps/test/features/auth/data/auth_repository_test.dart` ### Step 1: Write failing test for AuthRepository Create `apps/test/features/auth/data/auth_repository_test.dart`: ```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/auth_repository_impl.dart'; import 'package:social_app/features/auth/data/models/signup_request.dart'; import 'package:social_app/features/auth/data/models/login_request.dart'; import 'package:social_app/features/auth/data/models/auth_response.dart'; import 'package:social_app/core/storage/token_storage.dart'; class MockTokenStorage extends Mock implements TokenStorage {} void main() { late AuthRepository repository; late MockTokenStorage mockStorage; setUp(() { mockStorage = MockTokenStorage(); repository = AuthRepositoryImpl( tokenStorage: mockStorage, ); }); group('AuthRepository', () { test('signupStart returns SignupStartResponse', () async { final repo = _MockAuthRepository(); when(() => repo.signupStart(any())) .thenAnswer((_) async => const SignupStartResponse( status: 'pending_verification', email: 'test@example.com', message: 'Verification code sent', )); final result = await repo.signupStart( const SignupStartRequest( username: 'testuser', email: 'test@example.com', password: 'password123', ), ); expect(result.status, 'pending_verification'); expect(result.email, 'test@example.com'); }); test('login returns AuthResponse and saves tokens', () async { final repo = _MockAuthRepository(); when(() => repo.login(any())) .thenAnswer((_) async => AuthResponse( accessToken: 'access_token', refreshToken: 'refresh_token', expiresIn: 3600, tokenType: 'bearer', user: const AuthUser(id: '123', email: 'test@example.com'), )); final result = await repo.login( const LoginRequest(email: 'test@example.com', password: 'password123'), ); expect(result.accessToken, 'access_token'); expect(result.user.email, 'test@example.com'); }); }); } class _MockAuthRepository extends Mock implements AuthRepository {} ``` ### Step 2: Run test to verify it fails Run: `cd apps && flutter test test/features/auth/data/auth_repository_test.dart` Expected: FAIL - file not found ### Step 3: Implement AuthApi Create `apps/lib/features/auth/data/auth_api.dart`: ```dart import 'package:social_app/core/api/api_client.dart'; import 'models/signup_request.dart'; import 'models/login_request.dart'; import 'models/auth_response.dart'; class AuthApi { final ApiClient _client; static const _prefix = '/v1/auth'; AuthApi(this._client); Future signupStart(SignupStartRequest request) async { final response = await _client.post('$_prefix/signup/start', data: request.toJson()); return SignupStartResponse.fromJson(response.data); } Future signupVerify(SignupVerifyRequest request) async { final response = await _client.post('$_prefix/signup/verify', data: request.toJson()); return AuthResponse.fromJson(response.data); } Future signupResend(SignupResendRequest request) async { final response = await _client.post('$_prefix/signup/resend', data: request.toJson()); return SignupStartResponse.fromJson(response.data); } Future login(LoginRequest request) async { final response = await _client.post('$_prefix/login', data: request.toJson()); return AuthResponse.fromJson(response.data); } Future refresh(RefreshRequest request) async { final response = await _client.post('$_prefix/refresh', data: request.toJson()); return AuthResponse.fromJson(response.data); } Future logout(LogoutRequest request) async { await _client.post('$_prefix/logout', data: request.toJson()); } } ``` ### Step 4: Implement AuthRepository Create `apps/lib/features/auth/data/auth_repository.dart`: ```dart import 'package:social_app/features/auth/data/models/signup_request.dart'; import 'package:social_app/features/auth/data/models/login_request.dart'; import 'package:social_app/features/auth/data/models/auth_response.dart'; abstract class AuthRepository { Future signupStart(SignupStartRequest request); Future signupVerify(SignupVerifyRequest request); Future signupResend(SignupResendRequest request); Future login(LoginRequest request); Future refresh(String refreshToken); Future logout(); Future getAccessToken(); Future getRefreshToken(); Future isAuthenticated(); } ``` ### Step 5: Implement AuthRepositoryImpl Create `apps/lib/features/auth/data/auth_repository_impl.dart`: ```dart import 'package:social_app/core/storage/token_storage.dart'; import 'auth_api.dart'; import 'auth_repository.dart'; import 'models/signup_request.dart'; import 'models/login_request.dart'; import 'models/auth_response.dart'; class AuthRepositoryImpl implements AuthRepository { final AuthApi _api; final TokenStorage _tokenStorage; AuthRepositoryImpl({ required AuthApi api, required TokenStorage tokenStorage, }) : _api = api, _tokenStorage = tokenStorage; @override Future signupStart(SignupStartRequest request) { return _api.signupStart(request); } @override Future signupVerify(SignupVerifyRequest request) async { final response = await _api.signupVerify(request); await _tokenStorage.saveTokens( access: response.accessToken, refresh: response.refreshToken, ); return response; } @override Future signupResend(SignupResendRequest request) { return _api.signupResend(request); } @override Future login(LoginRequest request) async { final response = await _api.login(request); await _tokenStorage.saveTokens( access: response.accessToken, refresh: response.refreshToken, ); return response; } @override Future refresh(String refreshToken) async { final response = await _api.refresh(RefreshRequest(refreshToken: refreshToken)); await _tokenStorage.saveTokens( access: response.accessToken, refresh: response.refreshToken, ); return response; } @override Future logout() async { final refreshToken = await _tokenStorage.getRefreshToken(); if (refreshToken != null) { await _api.logout(LogoutRequest(refreshToken: refreshToken)); } await _tokenStorage.clear(); } @override Future getAccessToken() => _tokenStorage.getAccessToken(); @override Future getRefreshToken() => _tokenStorage.getRefreshToken(); @override Future isAuthenticated() async { final token = await _tokenStorage.getAccessToken(); return token != null; } } ``` ### Step 6: Run test to verify it passes Run: `cd apps && flutter test test/features/auth/data/auth_repository_test.dart` Expected: PASS ### Step 7: Commit auth repository ```bash git add apps/lib/features/auth/data/ apps/test/features/auth/data/ git commit -m "feat(apps): add auth repository" ``` --- ## Task 4: AuthBloc **Files:** - Create: `apps/lib/features/auth/presentation/bloc/auth_state.dart` - Create: `apps/lib/features/auth/presentation/bloc/auth_event.dart` - Create: `apps/lib/features/auth/presentation/bloc/auth_bloc.dart` - Test: `apps/test/features/auth/presentation/bloc/auth_bloc_test.dart` ### Step 1: Write failing test for AuthBloc Create `apps/test/features/auth/presentation/bloc/auth_bloc_test.dart`: ```dart 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/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'; class MockAuthRepository extends Mock implements AuthRepository {} void main() { late AuthBloc authBloc; late MockAuthRepository mockRepository; setUp(() { mockRepository = MockAuthRepository(); authBloc = AuthBloc(mockRepository); }); tearDown(() { authBloc.close(); }); group('AuthBloc', () { blocTest( 'emits [AuthLoading, AuthUnauthenticated] when AuthStarted and no token', build: () { when(() => mockRepository.getAccessToken()).thenAnswer((_) async => null); return authBloc; }, act: (bloc) => bloc.add(AuthStarted()), expect: () => [AuthLoading(), AuthUnauthenticated()], ); blocTest( 'emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid token', build: () { when(() => mockRepository.getAccessToken()).thenAnswer((_) async => 'valid_token'); return authBloc; }, act: (bloc) => bloc.add(AuthStarted()), expect: () => [AuthLoading(), isA()], ); blocTest( 'emits [AuthUnauthenticated] when AuthLoggedOut', build: () { when(() => mockRepository.logout()).thenAnswer((_) async {}); return authBloc; }, seed: () => AuthAuthenticated(user: const AuthUser(id: '1', email: 'test@example.com')), act: (bloc) => bloc.add(AuthLoggedOut()), expect: () => [AuthUnauthenticated()], ); }); } ``` ### Step 2: Run test to verify it fails Run: `cd apps && flutter test test/features/auth/presentation/bloc/auth_bloc_test.dart` Expected: FAIL - file not found ### Step 3: Implement AuthState Create `apps/lib/features/auth/presentation/bloc/auth_state.dart`: ```dart import 'package:equatable/equatable.dart'; class AuthUser extends Equatable { final String id; final String email; const AuthUser({required this.id, required this.email}); @override List get props => [id, email]; } abstract class AuthState extends Equatable { const AuthState(); @override List get props => []; } class AuthInitial extends AuthState {} class AuthLoading extends AuthState {} class AuthAuthenticated extends AuthState { final AuthUser user; const AuthAuthenticated({required this.user}); @override List get props => [user]; } class AuthUnauthenticated extends AuthState {} ``` ### Step 4: Implement AuthEvent Create `apps/lib/features/auth/presentation/bloc/auth_event.dart`: ```dart import 'package:equatable/equatable.dart'; import 'auth_state.dart'; abstract class AuthEvent extends Equatable { const AuthEvent(); @override List get props => []; } class AuthStarted extends AuthEvent {} class AuthLoggedIn extends AuthEvent { final AuthUser user; const AuthLoggedIn({required this.user}); @override List get props => [user]; } class AuthLoggedOut extends AuthEvent {} ``` ### Step 5: Implement AuthBloc Create `apps/lib/features/auth/presentation/bloc/auth_bloc.dart`: ```dart import 'package:flutter_bloc/flutter_bloc.dart'; import '../../data/auth_repository.dart'; import 'auth_event.dart'; import 'auth_state.dart'; class AuthBloc extends Bloc { final AuthRepository _repository; AuthBloc(this._repository) : super(AuthInitial()) { on(_onStarted); on(_onLoggedIn); on(_onLoggedOut); } Future _onStarted(AuthStarted event, Emitter emit) async { emit(AuthLoading()); final token = await _repository.getAccessToken(); if (token != null) { emit(const AuthAuthenticated( user: AuthUser(id: '', email: ''), )); } else { emit(AuthUnauthenticated()); } } void _onLoggedIn(AuthLoggedIn event, Emitter emit) { emit(AuthAuthenticated(user: event.user)); } Future _onLoggedOut(AuthLoggedOut event, Emitter emit) async { await _repository.logout(); emit(AuthUnauthenticated()); } } ``` ### Step 6: Run test to verify it passes Run: `cd apps && flutter test test/features/auth/presentation/bloc/auth_bloc_test.dart` Expected: PASS ### Step 7: Commit AuthBloc ```bash git add apps/lib/features/auth/presentation/bloc/ apps/test/features/auth/presentation/bloc/ git commit -m "feat(apps): add AuthBloc for global auth state" ``` --- ## Task 5: Register Cubit **Files:** - Create: `apps/lib/features/auth/presentation/cubits/register_cubit.dart` - Test: `apps/test/features/auth/presentation/cubits/register_cubit_test.dart` ### Step 1: Write failing test for RegisterCubit Create `apps/test/features/auth/presentation/cubits/register_cubit_test.dart`: ```dart import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:formz/formz.dart'; import 'package:mocktail/mocktail.dart'; import 'package:social_app/features/auth/data/auth_repository.dart'; import 'package:social_app/features/auth/presentation/cubits/register_cubit.dart'; class MockAuthRepository extends Mock implements AuthRepository {} void main() { late RegisterCubit cubit; late MockAuthRepository mockRepository; setUp(() { mockRepository = MockAuthRepository(); cubit = RegisterCubit(mockRepository); }); tearDown(() { cubit.close(); }); group('RegisterCubit', () { test('initial state has pure status', () { expect(cubit.state.status, FormzSubmissionStatus.initial); }); blocTest( 'usernameChanged updates username', build: () => cubit, act: (c) => c.usernameChanged('testuser'), expect: () => [isA()], ); blocTest( 'emailChanged updates email', build: () => cubit, act: (c) => c.emailChanged('test@example.com'), expect: () => [isA()], ); blocTest( 'passwordChanged updates password', build: () => cubit, act: (c) => c.passwordChanged('password123'), expect: () => [isA()], ); }); } ``` ### Step 2: Run test to verify it fails Run: `cd apps && flutter test test/features/auth/presentation/cubits/register_cubit_test.dart` Expected: FAIL - file not found ### Step 3: Implement RegisterCubit Create `apps/lib/features/auth/presentation/cubits/register_cubit.dart`: ```dart import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '../../data/auth_repository.dart'; import '../../data/models/signup_request.dart'; import '../../data/models/auth_response.dart'; class Username extends FormzInput { const Username.pure() : super.pure(''); const Username.dirty([super.value = '']) : super.dirty(); @override String? validator(String value) { if (value.isEmpty) return 'Username is required'; if (value.length < 3) return 'Username must be at least 3 characters'; if (value.length > 30) return 'Username must be at most 30 characters'; return null; } } class Email extends FormzInput { const Email.pure() : super.pure(''); const Email.dirty([super.value = '']) : super.dirty(); static final _regex = RegExp(r'^[\w.-]+@[\w.-]+\.\w+$'); @override String? validator(String value) { if (value.isEmpty) return 'Email is required'; if (!_regex.hasMatch(value)) return 'Invalid email format'; return null; } } class Password extends FormzInput { const Password.pure() : super.pure(''); const Password.dirty([super.value = '']) : super.dirty(); @override String? validator(String value) { if (value.isEmpty) return 'Password is required'; if (value.length < 6) return 'Password must be at least 6 characters'; return null; } } class VerificationCode extends FormzInput { const VerificationCode.pure() : super.pure(''); const VerificationCode.dirty([super.value = '']) : super.dirty(); @override String? validator(String value) { if (value.isEmpty) return 'Code is required'; if (!RegExp(r'^\d{6}$').hasMatch(value)) return 'Code must be 6 digits'; return null; } } class RegisterState extends Equatable { final Username username; final Email email; final Password password; final VerificationCode verificationCode; final FormzSubmissionStatus status; final String? errorMessage; final String? pendingEmail; final bool codeSent; const RegisterState({ this.username = const Username.pure(), this.email = const Email.pure(), this.password = const Password.pure(), this.verificationCode = const VerificationCode.pure(), this.status = FormzSubmissionStatus.initial, this.errorMessage, this.pendingEmail, this.codeSent = false, }); bool get isStep1Valid => username.isValid && email.isValid && password.isValid; bool get isStep2Valid => verificationCode.isValid; RegisterState copyWith({ Username? username, Email? email, Password? password, VerificationCode? verificationCode, FormzSubmissionStatus? status, String? errorMessage, String? pendingEmail, bool? codeSent, }) { return RegisterState( username: username ?? this.username, email: email ?? this.email, password: password ?? this.password, verificationCode: verificationCode ?? this.verificationCode, status: status ?? this.status, errorMessage: errorMessage, pendingEmail: pendingEmail ?? this.pendingEmail, codeSent: codeSent ?? this.codeSent, ); } @override List get props => [ username, email, password, verificationCode, status, errorMessage, pendingEmail, codeSent, ]; } class RegisterCubit extends Cubit { final AuthRepository _repository; RegisterCubit(this._repository) : super(const RegisterState()); void usernameChanged(String value) { emit(state.copyWith(username: Username.dirty(value))); } void emailChanged(String value) { emit(state.copyWith(email: Email.dirty(value))); } void passwordChanged(String value) { emit(state.copyWith(password: Password.dirty(value))); } void verificationCodeChanged(String value) { emit(state.copyWith(verificationCode: VerificationCode.dirty(value))); } Future submitStep1() async { if (!state.isStep1Valid) return false; emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { final response = await _repository.signupStart( SignupStartRequest( username: state.username.value, email: state.email.value, password: state.password.value, ), ); emit(state.copyWith( status: FormzSubmissionStatus.success, pendingEmail: response.email, codeSent: true, )); return true; } catch (e) { emit(state.copyWith( status: FormzSubmissionStatus.failure, errorMessage: e.toString(), )); return false; } } Future submitStep2() async { if (!state.isStep2Valid || state.pendingEmail == null) return null; emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { final response = await _repository.signupVerify( SignupVerifyRequest( email: state.pendingEmail!, token: state.verificationCode.value, ), ); emit(state.copyWith(status: FormzSubmissionStatus.success)); return response; } catch (e) { emit(state.copyWith( status: FormzSubmissionStatus.failure, errorMessage: e.toString(), )); return null; } } Future resendCode() async { if (state.pendingEmail == null) return; try { await _repository.signupResend( SignupResendRequest(email: state.pendingEmail!), ); emit(state.copyWith(codeSent: true)); } catch (e) { emit(state.copyWith(errorMessage: e.toString())); } } } ``` ### Step 4: Run test to verify it passes Run: `cd apps && flutter test test/features/auth/presentation/cubits/register_cubit_test.dart` Expected: PASS ### Step 5: Commit RegisterCubit ```bash git add apps/lib/features/auth/presentation/cubits/ apps/test/features/auth/presentation/cubits/ git commit -m "feat(apps): add RegisterCubit for signup form" ``` --- ## Task 6: Login Cubit **Files:** - Create: `apps/lib/features/auth/presentation/cubits/login_cubit.dart` - Test: `apps/test/features/auth/presentation/cubits/login_cubit_test.dart` ### Step 1: Write failing test for LoginCubit Create `apps/test/features/auth/presentation/cubits/login_cubit_test.dart`: ```dart import 'package:bloc_test/bloc_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:formz/formz.dart'; import 'package:mocktail/mocktail.dart'; import 'package:social_app/features/auth/data/auth_repository.dart'; import 'package:social_app/features/auth/presentation/cubits/login_cubit.dart'; class MockAuthRepository extends Mock implements AuthRepository {} void main() { late LoginCubit cubit; late MockAuthRepository mockRepository; setUp(() { mockRepository = MockAuthRepository(); cubit = LoginCubit(mockRepository); }); tearDown(() { cubit.close(); }); group('LoginCubit', () { test('initial state has pure status', () { expect(cubit.state.status, FormzSubmissionStatus.initial); }); blocTest( 'emailChanged updates email', build: () => cubit, act: (c) => c.emailChanged('test@example.com'), expect: () => [isA()], }); }); } ``` ### Step 2: Run test to verify it fails Run: `cd apps && flutter test test/features/auth/presentation/cubits/login_cubit_test.dart` Expected: FAIL - file not found ### Step 3: Implement LoginCubit Create `apps/lib/features/auth/presentation/cubits/login_cubit.dart`: ```dart import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import '../../data/auth_repository.dart'; import '../../data/models/login_request.dart'; import '../../data/models/auth_response.dart'; import 'register_cubit.dart' show Email, Password; class LoginState extends Equatable { final Email email; final Password password; final FormzSubmissionStatus status; final String? errorMessage; const LoginState({ this.email = const Email.pure(), this.password = const Password.pure(), this.status = FormzSubmissionStatus.initial, this.errorMessage, }); bool get isValid => email.isValid && password.isValid; LoginState copyWith({ Email? email, Password? password, FormzSubmissionStatus? status, String? errorMessage, }) { return LoginState( email: email ?? this.email, password: password ?? this.password, status: status ?? this.status, errorMessage: errorMessage, ); } @override List get props => [email, password, status, errorMessage]; } class LoginCubit extends Cubit { final AuthRepository _repository; LoginCubit(this._repository) : super(const LoginState()); void emailChanged(String value) { emit(state.copyWith(email: Email.dirty(value))); } void passwordChanged(String value) { emit(state.copyWith(password: Password.dirty(value))); } Future submit() async { if (!state.isValid) return null; emit(state.copyWith(status: FormzSubmissionStatus.inProgress)); try { final response = await _repository.login( LoginRequest( email: state.email.value, password: state.password.value, ), ); emit(state.copyWith(status: FormzSubmissionStatus.success)); return response; } catch (e) { emit(state.copyWith( status: FormzSubmissionStatus.failure, errorMessage: e.toString(), )); return null; } } } ``` ### Step 4: Run test to verify it passes Run: `cd apps && flutter test test/features/auth/presentation/cubits/login_cubit_test.dart` Expected: PASS ### Step 5: Commit LoginCubit ```bash git add apps/lib/features/auth/presentation/cubits/login_cubit.dart apps/test/features/auth/presentation/cubits/login_cubit_test.dart git commit -m "feat(apps): add LoginCubit for login form" ``` --- ## Task 7: Dependency Injection **Files:** - Create: `apps/lib/core/di/injection.dart` - Create: `apps/lib/core/config/env.dart` ### Step 1: Implement Env config Create `apps/lib/core/config/env.dart`: ```dart class Env { static String get apiUrl { const url = String.fromEnvironment('API_URL'); return url.isNotEmpty ? url : 'http://localhost:8000'; } } ``` ### Step 2: Implement DI configuration Create `apps/lib/core/di/injection.dart`: ```dart import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get_it/get_it.dart'; import '../api/api_client.dart'; import '../storage/token_storage.dart'; import '../config/env.dart'; import '../../features/auth/data/auth_api.dart'; import '../../features/auth/data/auth_repository.dart'; import '../../features/auth/data/auth_repository_impl.dart'; import '../../features/auth/presentation/bloc/auth_bloc.dart'; final sl = GetIt.instance; Future configureDependencies() async { final dio = Dio(BaseOptions(baseUrl: Env.apiUrl)); final secureStorage = const FlutterSecureStorage(); sl.registerSingleton(dio); sl.registerSingleton(secureStorage); sl.registerSingleton(SecureTokenStorage(secureStorage)); sl.registerSingleton(ApiClient( baseUrl: Env.apiUrl, tokenStorage: sl(), dio: sl(), )); sl.registerSingleton(AuthApi(sl())); sl.registerSingleton(AuthRepositoryImpl( api: sl(), tokenStorage: sl(), )); sl.registerSingleton(AuthBloc(sl())); } ``` ### Step 3: Commit DI configuration ```bash git add apps/lib/core/di/ apps/lib/core/config/ git commit -m "feat(apps): add dependency injection configuration" ``` --- ## Task 8: Update Register Screen **Files:** - Modify: `apps/lib/features/auth/ui/screens/register_screen.dart` - Modify: `apps/lib/features/auth/ui/screens/register_step2_screen.dart` ### Step 1: Update register_screen.dart Modify `apps/lib/features/auth/ui/screens/register_screen.dart` to: 1. Add password input field 2. Wrap with BlocProvider for RegisterCubit 3. Call submitStep1 on button press 4. Pass email to step2 via route params ### Step 2: Update register_step2_screen.dart Modify `apps/lib/features/auth/ui/screens/register_step2_screen.dart` to: 1. Remove password field (moved to step1) 2. Integrate with RegisterCubit 3. Call submitStep2 and navigate to /home on success 4. Keep invite code field but don't send ### Step 3: Commit register screen updates ```bash git add apps/lib/features/auth/ui/screens/register_screen.dart apps/lib/features/auth/ui/screens/register_step2_screen.dart git commit -m "feat(apps): update register screens with backend integration" ``` --- ## Task 9: Update Login Screen **Files:** - Modify: `apps/lib/features/auth/ui/screens/login_email_screen.dart` - Modify: `apps/lib/features/auth/ui/screens/login_password_screen.dart` ### Step 1: Update login flow Modify login screens to: 1. Pass email from step1 to step2 via route params 2. Wrap with BlocProvider for LoginCubit 3. Call submit and navigate to /home on success 4. Remove "Login with OTP" button ### Step 2: Commit login screen updates ```bash git add apps/lib/features/auth/ui/screens/login_email_screen.dart apps/lib/features/auth/ui/screens/login_password_screen.dart git commit -m "feat(apps): update login screens with backend integration" ``` --- ## Task 10: Router Auth Protection **Files:** - Modify: `apps/lib/core/router/app_router.dart` - Modify: `apps/lib/main.dart` ### Step 1: Update app_router.dart Add auth protection using GoRouter redirect: 1. Inject AuthBloc 2. Add redirect logic to check authentication 3. Protect routes requiring authentication ### Step 2: Update main.dart 1. Call configureDependencies() 2. Dispatch AuthStarted event 3. Provide AuthBloc at app level ### Step 3: Commit router and main updates ```bash git add apps/lib/core/router/app_router.dart apps/lib/main.dart git commit -m "feat(apps): add auth protection to router" ``` --- ## Task 11: Integration Testing **Files:** - Create: `apps/integration_test/auth_flow_test.dart` ### Step 1: Write integration test Create integration test for complete signup and login flows. ### Step 2: Run integration test Run: `cd apps && flutter test integration_test/auth_flow_test.dart` ### Step 3: Commit integration test ```bash git add apps/integration_test/ git commit -m "test(apps): add auth flow integration tests" ``` --- ## Final: Run Flutter analyze ```bash cd apps && flutter analyze ``` Expected: No issues found --- ## Summary - 11 tasks total - Each task follows TDD: test first, implement, verify - Frequent commits after each task - Core infrastructure first, then UI integration