From 56e5c662913634406b45b72021bca92f3ec3a9f3 Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 25 Feb 2026 14:00:22 +0800 Subject: [PATCH] docs: add Flutter auth integration design --- ...6-02-25-flutter-auth-integration-design.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/plans/2026-02-25-flutter-auth-integration-design.md diff --git a/docs/plans/2026-02-25-flutter-auth-integration-design.md b/docs/plans/2026-02-25-flutter-auth-integration-design.md new file mode 100644 index 0000000..84b363d --- /dev/null +++ b/docs/plans/2026-02-25-flutter-auth-integration-design.md @@ -0,0 +1,235 @@ +# 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