Files
social-app/docs/plans/2026-02-25-flutter-auth-integration-design.md
T
2026-02-25 14:00:22 +08:00

6.7 KiB

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 <access_token>
  2. On 401 response, auto-refresh token and retry
  3. If refresh fails, trigger logout

AuthApi interface:

abstract class AuthApi {
  Future<AuthResponse> signupStart(SignupStartRequest request);
  Future<AuthResponse> signupVerify(SignupVerifyRequest request);
  Future<AuthResponse> signupResend(String email);
  Future<AuthResponse> login(LoginRequest request);
  Future<AuthResponse> refresh(String refreshToken);
  Future<void> logout(String refreshToken);
}

TokenStorage interface:

abstract class TokenStorage {
  Future<String?> getAccessToken();
  Future<String?> getRefreshToken();
  Future<void> saveTokens({required String access, required String refresh});
  Future<void> clear();
}

Error handling:

  • ApiException wraps network errors, timeouts, 4xx/5xx
  • UI layer displays user-friendly messages via ApiException

State Management

AuthBloc (global auth state):

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):

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):

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
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:

final sl = GetIt.instance;

Future<void> configureDependencies() async {
  // Core
  sl.registerSingleton<Dio>(Dio(BaseOptions(baseUrl: env.apiUrl)));
  sl.registerSingleton<TokenStorage>(SecureTokenStorage());
  sl.registerSingleton<ApiClient>(ApiClient(sl(), sl()));
  
  // Auth
  sl.registerSingleton<AuthApi>(AuthApiImpl(sl()));
  sl.registerSingleton<AuthRepository>(AuthRepositoryImpl(sl(), sl()));
  sl.registerSingleton<AuthBloc>(AuthBloc(sl()));
}

App initialization flow:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await configureDependencies();
  
  final authBloc = sl<AuthBloc>();
  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