# Flutter Auth Integration Design Date: 2026-02-25 Status: Approved ## Summary Integrate Flutter mobile app with backend auth APIs, including signup, login, token management, and route protection. ## Scope - Signup flow: `/auth/signup/start` + `/auth/signup/verify` - Login flow: `/auth/login` - Token management: access token, refresh token, secure storage - Route protection: redirect unauthenticated users Out of scope: - OTP login (removed from UI) - Password reset - Social login ## Architecture ### Directory Structure ``` lib/ ├── core/ │ ├── api/ │ │ ├── api_client.dart # dio wrapper │ │ ├── api_interceptor.dart # token injection, refresh logic │ │ └── api_exception.dart # unified error types │ ├── storage/ │ │ └── token_storage.dart # flutter_secure_storage wrapper │ └── di/ │ └── injection.dart # get_it configuration │ ├── features/auth/ │ ├── data/ │ │ ├── models/ │ │ │ ├── signup_request.dart │ │ │ ├── login_request.dart │ │ │ └── auth_response.dart │ │ ├── auth_api.dart # API interface │ │ ├── auth_repository.dart # abstract interface │ │ └── auth_repository_impl.dart # implementation │ ├── presentation/ │ │ ├── bloc/ │ │ │ ├── auth_bloc.dart │ │ │ ├── auth_event.dart │ │ │ └── auth_state.dart │ │ └── cubits/ │ │ ├── register_cubit.dart # signup form state │ │ └── login_cubit.dart # login form state │ └── ui/screens/ # existing UI adjustments │ └── main.dart # DI initialization ``` ### API Layer **ApiInterceptor responsibilities**: 1. Auto-inject `Authorization: Bearer ` 2. On 401 response, auto-refresh token and retry 3. If refresh fails, trigger logout **AuthApi interface**: ```dart abstract class AuthApi { Future signupStart(SignupStartRequest request); Future signupVerify(SignupVerifyRequest request); Future signupResend(String email); Future login(LoginRequest request); Future refresh(String refreshToken); Future logout(String refreshToken); } ``` **TokenStorage interface**: ```dart abstract class TokenStorage { Future getAccessToken(); Future getRefreshToken(); Future saveTokens({required String access, required String refresh}); Future clear(); } ``` **Error handling**: - `ApiException` wraps network errors, timeouts, 4xx/5xx - UI layer displays user-friendly messages via `ApiException` ### State Management **AuthBloc (global auth state)**: ```dart abstract class AuthState {} class AuthInitial extends AuthState {} class AuthAuthenticated extends AuthState { final User user; final String accessToken; } class AuthUnauthenticated extends AuthState {} class AuthLoading extends AuthState {} // Events class AuthStarted extends AuthEvent {} // Check token on app start class AuthLoggedIn extends AuthEvent {} // Login success class AuthLoggedOut extends AuthEvent {} // Logout class AuthTokenRefreshed extends AuthEvent {} // Token refreshed ``` **RegisterCubit (signup form)**: ```dart class RegisterState { final Username username; // formz input final Email email; final Password password; final FormzSubmissionStatus status; final String? errorMessage; final String? pendingEmail; // stored after signup/start success } ``` **LoginCubit (login form)**: ```dart class LoginState { final Email email; final Password password; final FormzSubmissionStatus status; final String? errorMessage; } ``` ### UI Adjustments **Signup flow changes**: | Original | New | |----------|-----| | Step1: nickname + email | Step1: username + email + password -> call `/signup/start` | | Step2: password + code + invite | Step2: code + invite(kept) -> call `/signup/verify` | **Login flow changes**: - Remove "Login with OTP" button - `login_email_screen.dart` -> `login_password_screen.dart` passes email - Password page calls `/login`, navigates to `/home` on success **Route protection**: - Use `GoRouter` `redirect` logic - Unauthenticated user accessing protected route -> redirect to `/` - Authenticated user accessing `/login` or `/register` -> redirect to `/home` ```dart redirect: (context, state) { final isAuthenticated = // check AuthBloc state; final isAuthRoute = state.matchedLocation.startsWith('/login') || state.matchedLocation.startsWith('/register'); if (!isAuthenticated && !isAuthRoute) return '/'; if (isAuthenticated && isAuthRoute) return '/home'; return null; } ``` ### Dependency Injection **DI configuration**: ```dart final sl = GetIt.instance; Future configureDependencies() async { // Core sl.registerSingleton(Dio(BaseOptions(baseUrl: env.apiUrl))); sl.registerSingleton(SecureTokenStorage()); sl.registerSingleton(ApiClient(sl(), sl())); // Auth sl.registerSingleton(AuthApiImpl(sl())); sl.registerSingleton(AuthRepositoryImpl(sl(), sl())); sl.registerSingleton(AuthBloc(sl())); } ``` **App initialization flow**: ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); await configureDependencies(); final authBloc = sl(); authBloc.add(AuthStarted()); // Check local token runApp(MyApp(authBloc: authBloc)); } ``` **Complete flow**: ``` App Start | AuthStarted event | Check TokenStorage | Has token? --No--> AuthUnauthenticated -> Show login | Yes | Validate token (call /auth/refresh) | Success? --No--> Clear token -> AuthUnauthenticated | Yes | AuthAuthenticated -> Navigate to /home ``` ## Decisions 1. **Adjust Flutter UI to match backend API** - Move password to Step1, call `/signup/start` with all data 2. **Keep invite code UI but don't send** - Backend doesn't support yet, preserve UI for future 3. **Remove OTP login entry** - Backend doesn't have OTP login API 4. **Use complete Bloc architecture** - AuthBloc + Cubits for forms, better testability and extensibility ## Risks - Token refresh race conditions: ApiInterceptor should handle concurrent requests during refresh - Secure storage availability: fallback to SharedPreferences on platforms without secure storage ## Testing Strategy - Unit tests for: Cubits, AuthBloc, Repository, API client - Widget tests for: form validation, error display - Integration tests for: complete signup/login flows