2026-02-25 14:05:29 +08:00
# 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 < ApiException > ( ) ) ;
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 < String , dynamic > ? 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 < String ? > getAccessToken ( ) ;
Future < String ? > getRefreshToken ( ) ;
Future < void > saveTokens ( { required String access , required String refresh } ) ;
Future < void > clear ( ) ;
}
class SecureTokenStorage implements TokenStorage {
static const _accessTokenKey = ' access_token ' ;
static const _refreshTokenKey = ' refresh_token ' ;
final dynamic _storage ;
SecureTokenStorage ( [ this . _storage ] ) ;
@ override
Future < String ? > getAccessToken ( ) async {
return _storage ? . read ( key: _accessTokenKey ) ;
}
@ override
Future < String ? > getRefreshToken ( ) async {
return _storage ? . read ( key: _refreshTokenKey ) ;
}
@ override
Future < void > saveTokens ( { required String access , required String refresh } ) async {
await _storage ? . write ( key: _accessTokenKey , value: access ) ;
await _storage ? . write ( key: _refreshTokenKey , value: refresh ) ;
}
@ override
Future < void > 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 < bool > 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 ;
2026-02-25 14:25:17 +08:00
final Future < AuthResponse > Function ( String ) ? _refreshToken ;
2026-02-25 14:05:29 +08:00
ApiClient ( {
required String baseUrl ,
required TokenStorage tokenStorage ,
Dio ? dio ,
2026-02-25 14:25:17 +08:00
Future < AuthResponse > Function ( String ) ? refreshToken ,
2026-02-25 14:05:29 +08:00
} ) : _tokenStorage = tokenStorage ,
2026-02-25 14:25:17 +08:00
_refreshToken = refreshToken ,
2026-02-25 14:05:29 +08:00
_dio = dio ? ? Dio ( BaseOptions ( baseUrl: baseUrl ) ) {
_dio . interceptors . add ( ApiInterceptor (
tokenStorage: _tokenStorage ,
onTokenRefresh: _handleTokenRefresh ,
) ) ;
}
Dio get dio = > _dio ;
Future < bool > _handleTokenRefresh ( ) async {
2026-02-25 14:25:17 +08:00
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 ;
}
2026-02-25 14:05:29 +08:00
}
Future < Response < T > > get < T > ( String path , { Options ? options } ) async {
try {
return await _dio . get < T > ( path , options: options ) ;
} catch ( e ) {
throw ApiException . fromDioError ( e ) ;
}
}
Future < Response < T > > post < T > ( String path , { dynamic data , Options ? options } ) async {
try {
return await _dio . post < T > ( 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 < String , dynamic > toJson ( ) = > {
' username ' : username ,
' email ' : email ,
' password ' : password ,
} ;
}
class SignupVerifyRequest {
final String email ;
final String token ;
const SignupVerifyRequest ( {
required this . email ,
required this . token ,
} ) ;
Map < String , dynamic > toJson ( ) = > {
' email ' : email ,
' token ' : token ,
} ;
}
class SignupResendRequest {
final String email ;
const SignupResendRequest ( { required this . email } ) ;
Map < String , dynamic > 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 < String , dynamic > toJson ( ) = > {
' email ' : email ,
' password ' : password ,
} ;
}
class RefreshRequest {
final String refreshToken ;
const RefreshRequest ( { required this . refreshToken } ) ;
Map < String , dynamic > toJson ( ) = > { ' refresh_token ' : refreshToken } ;
}
class LogoutRequest {
final String refreshToken ;
const LogoutRequest ( { required this . refreshToken } ) ;
Map < String , dynamic > 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 < String , dynamic > 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 < String , dynamic > 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 < String , dynamic > ) ,
) ;
}
}
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 < String , dynamic > 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 < SignupStartResponse > signupStart ( SignupStartRequest request ) async {
final response = await _client . post ( ' $ _prefix /signup/start ' , data: request . toJson ( ) ) ;
return SignupStartResponse . fromJson ( response . data ) ;
}
Future < AuthResponse > signupVerify ( SignupVerifyRequest request ) async {
final response = await _client . post ( ' $ _prefix /signup/verify ' , data: request . toJson ( ) ) ;
return AuthResponse . fromJson ( response . data ) ;
}
Future < SignupStartResponse > signupResend ( SignupResendRequest request ) async {
final response = await _client . post ( ' $ _prefix /signup/resend ' , data: request . toJson ( ) ) ;
return SignupStartResponse . fromJson ( response . data ) ;
}
Future < AuthResponse > login ( LoginRequest request ) async {
final response = await _client . post ( ' $ _prefix /login ' , data: request . toJson ( ) ) ;
return AuthResponse . fromJson ( response . data ) ;
}
Future < AuthResponse > refresh ( RefreshRequest request ) async {
final response = await _client . post ( ' $ _prefix /refresh ' , data: request . toJson ( ) ) ;
return AuthResponse . fromJson ( response . data ) ;
}
Future < void > 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 < SignupStartResponse > signupStart ( SignupStartRequest request ) ;
Future < AuthResponse > signupVerify ( SignupVerifyRequest request ) ;
Future < SignupStartResponse > signupResend ( SignupResendRequest request ) ;
Future < AuthResponse > login ( LoginRequest request ) ;
Future < AuthResponse > refresh ( String refreshToken ) ;
Future < void > logout ( ) ;
Future < String ? > getAccessToken ( ) ;
Future < String ? > getRefreshToken ( ) ;
Future < bool > 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 < SignupStartResponse > signupStart ( SignupStartRequest request ) {
return _api . signupStart ( request ) ;
}
@ override
Future < AuthResponse > signupVerify ( SignupVerifyRequest request ) async {
final response = await _api . signupVerify ( request ) ;
await _tokenStorage . saveTokens (
access: response . accessToken ,
refresh: response . refreshToken ,
) ;
return response ;
}
@ override
Future < SignupStartResponse > signupResend ( SignupResendRequest request ) {
return _api . signupResend ( request ) ;
}
@ override
Future < AuthResponse > login ( LoginRequest request ) async {
final response = await _api . login ( request ) ;
await _tokenStorage . saveTokens (
access: response . accessToken ,
refresh: response . refreshToken ,
) ;
return response ;
}
@ override
Future < AuthResponse > 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 < void > logout ( ) async {
final refreshToken = await _tokenStorage . getRefreshToken ( ) ;
if ( refreshToken ! = null ) {
await _api . logout ( LogoutRequest ( refreshToken: refreshToken ) ) ;
}
await _tokenStorage . clear ( ) ;
}
@ override
Future < String ? > getAccessToken ( ) = > _tokenStorage . getAccessToken ( ) ;
@ override
Future < String ? > getRefreshToken ( ) = > _tokenStorage . getRefreshToken ( ) ;
@ override
Future < bool > 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 ' ;
2026-02-25 14:25:17 +08:00
import ' package:social_app/features/auth/data/models/auth_response.dart ' ;
2026-02-25 14:05:29 +08:00
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 < AuthBloc , AuthState > (
' 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 < AuthBloc , AuthState > (
2026-02-25 14:25:17 +08:00
' emits [AuthLoading, AuthAuthenticated] when AuthStarted with valid refresh token ' ,
2026-02-25 14:05:29 +08:00
build: ( ) {
2026-02-25 14:25:17 +08:00
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 ' ) ,
) ) ;
2026-02-25 14:05:29 +08:00
return authBloc ;
} ,
act: ( bloc ) = > bloc . add ( AuthStarted ( ) ) ,
expect: ( ) = > [ AuthLoading ( ) , isA < AuthAuthenticated > ( ) ] ,
) ;
2026-02-25 14:25:17 +08:00
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 ( ) ] ,
) ;
2026-02-25 14:05:29 +08:00
blocTest < AuthBloc , AuthState > (
' 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 < Object ? > get props = > [ id , email ] ;
}
abstract class AuthState extends Equatable {
const AuthState ( ) ;
@ override
List < Object ? > get props = > [ ] ;
}
class AuthInitial extends AuthState { }
class AuthLoading extends AuthState { }
class AuthAuthenticated extends AuthState {
final AuthUser user ;
const AuthAuthenticated ( { required this . user } ) ;
@ override
List < Object ? > 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 < Object ? > get props = > [ ] ;
}
class AuthStarted extends AuthEvent { }
class AuthLoggedIn extends AuthEvent {
final AuthUser user ;
const AuthLoggedIn ( { required this . user } ) ;
@ override
List < Object ? > 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 < AuthEvent , AuthState > {
final AuthRepository _repository ;
AuthBloc ( this . _repository ) : super ( AuthInitial ( ) ) {
on < AuthStarted > ( _onStarted ) ;
on < AuthLoggedIn > ( _onLoggedIn ) ;
on < AuthLoggedOut > ( _onLoggedOut ) ;
}
Future < void > _onStarted ( AuthStarted event , Emitter < AuthState > emit ) async {
emit ( AuthLoading ( ) ) ;
2026-02-25 14:25:17 +08:00
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 ( ) ;
}
2026-02-25 14:05:29 +08:00
}
2026-02-25 14:25:17 +08:00
emit ( AuthUnauthenticated ( ) ) ;
2026-02-25 14:05:29 +08:00
}
void _onLoggedIn ( AuthLoggedIn event , Emitter < AuthState > emit ) {
emit ( AuthAuthenticated ( user: event . user ) ) ;
}
Future < void > _onLoggedOut ( AuthLoggedOut event , Emitter < AuthState > 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 < RegisterCubit , RegisterState > (
' usernameChanged updates username ' ,
build: ( ) = > cubit ,
act: ( c ) = > c . usernameChanged ( ' testuser ' ) ,
expect: ( ) = > [ isA < RegisterState > ( ) ] ,
) ;
blocTest < RegisterCubit , RegisterState > (
' emailChanged updates email ' ,
build: ( ) = > cubit ,
act: ( c ) = > c . emailChanged ( ' test@example.com ' ) ,
expect: ( ) = > [ isA < RegisterState > ( ) ] ,
) ;
blocTest < RegisterCubit , RegisterState > (
' passwordChanged updates password ' ,
build: ( ) = > cubit ,
act: ( c ) = > c . passwordChanged ( ' password123 ' ) ,
expect: ( ) = > [ isA < RegisterState > ( ) ] ,
) ;
} ) ;
}
```
### 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 < String , String > {
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 < String , String > {
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 < String , String > {
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 < String , String > {
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 < Object ? > get props = > [
username ,
email ,
password ,
verificationCode ,
status ,
errorMessage ,
pendingEmail ,
codeSent ,
] ;
}
class RegisterCubit extends Cubit < RegisterState > {
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 < bool > 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 < AuthResponse ? > 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 < void > 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 < LoginCubit , LoginState > (
' emailChanged updates email ' ,
build: ( ) = > cubit ,
act: ( c ) = > c . emailChanged ( ' test@example.com ' ) ,
expect: ( ) = > [ isA < LoginState > ( ) ] ,
} ) ;
} ) ;
}
```
### 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 < Object ? > get props = > [ email , password , status , errorMessage ] ;
}
class LoginCubit extends Cubit < LoginState > {
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 < AuthResponse ? > 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 < void > configureDependencies ( ) async {
final dio = Dio ( BaseOptions ( baseUrl: Env . apiUrl ) ) ;
final secureStorage = const FlutterSecureStorage ( ) ;
2026-02-25 14:25:17 +08:00
final tokenStorage = SecureTokenStorage ( secureStorage ) ;
2026-02-25 14:05:29 +08:00
sl . registerSingleton < Dio > ( dio ) ;
sl . registerSingleton < FlutterSecureStorage > ( secureStorage ) ;
2026-02-25 14:25:17 +08:00
sl . registerSingleton < TokenStorage > ( tokenStorage ) ;
// Register AuthApi and AuthRepository first (needed for ApiClient refresh)
final authApi = AuthApi ( ApiClient (
2026-02-25 14:05:29 +08:00
baseUrl: Env . apiUrl ,
2026-02-25 14:25:17 +08:00
tokenStorage: tokenStorage ,
dio: dio ,
2026-02-25 14:05:29 +08:00
) ) ;
2026-02-25 14:25:17 +08:00
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: tokenStorage ,
dio: dio ,
refreshToken: ( token ) = > authRepository . refresh ( token ) ,
2026-02-25 14:05:29 +08:00
) ) ;
2026-02-25 14:25:17 +08:00
sl . registerSingleton < AuthBloc > ( AuthBloc ( authRepository ) ) ;
2026-02-25 14:05:29 +08:00
}
```
### 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