arch-atlas

Login Notifier

presentation/providers · Notifier<LoginState>

domain

Overview

Riverpod Notifier orchestrating email/password, Google and Apple sign-in. On password login it issues a 'dual login' (customer + seller endpoints) to discover whether the same credentials also resolve a seller account, then persists both tokens and forces the user role accordingly.

User Flow

  1. Screen calls login(email, password); state flips to isLoading + isPasswordLoading.
  2. AuthApiService.dualLogin posts /auth/customer/emailpass-verified, then /auth/seller/emailpass-verified.
  3. Seller 401 is swallowed and returned as null — the user is treated as customer-only.
  4. _persistAuthenticatedSessions saves customer + (optional) seller tokens via AuthStorageService.
  5. userRoleProvider.notifier.setRole(freelancer|client) forces the active role based on which tokens exist.
  6. State updates to isAuthenticated = true; the GoRouter redirect then bounces /login → /home.

Cases & Edge Cases

  • Seller endpoint returns 401 → treat as 'not a seller', proceed as customer-only (loginSeller swallows the 401).
  • Google/Apple cancellation throws GoogleSignInExceptionCode.canceled / AuthorizationErrorCode.canceled — clear loading flags but do not surface as an error.
  • Apple only sends name/email on the first authorization — subsequent logins must rely on the backend mapping by Apple subject ID.
  • skipTermsConsent flag short-circuits the (currently commented-out) ToS pre-check; the router still enforces ToS post-login.
  • Token may be expired when restored from storage — JwtHelper.isTokenExpired is logged but the UI does not yet trigger a refresh.

Code References

  • lib/src/features/shared/auth/login/presentation/providers/auth/login_provider.dart:16 // LoginNotifier class
  • lib/src/features/shared/auth/login/presentation/providers/auth/login_provider.dart:47 // login(email,password)
  • lib/src/features/shared/auth/login/presentation/providers/auth/login_provider.dart:154 // loginWithGoogle
  • lib/src/features/shared/auth/login/presentation/providers/auth/login_provider.dart:271 // loginWithApple
  • lib/src/features/shared/auth/login/presentation/providers/auth/login_provider.dart:370 // _persistAuthenticatedSessions
  • lib/src/features/shared/auth/login/presentation/providers/auth/login_state.dart

API Calls

  • POST/auth/customer/emailpass-verifiedno status

    Authenticate as a customer with email + password and receive a session token (mandatory leg of the dual-login).

    Request
    { email: string, password: string }
    Response
    { token: string }
    lib/src/features/shared/auth/login/presentation/login.dart

    auth: none. First leg of dualLogin; any non-401 customer error throws.

  • POST/auth/seller/emailpass-verifiedno status

    Best-effort seller login using the same credentials — used to detect multi-role users.

    Request
    { email: string, password: string }
    Response
    { token: string }
    Errors
    • ·401 treated as 'not a seller' — swallowed and returned as null
    lib/src/features/shared/auth/login/presentation/login.dart

    auth: none. Second leg of dualLogin; 401 is swallowed.

  • POST/auth/customer/google-mobileno status

    Exchange a Google OAuth payload (id_token + access_token) for a customer session token.

    Request
    { id_token: string, access_token: string, expires_in: number, token_type: string, scope: string, refresh_token: string }
    Response
    { token: string }
    lib/src/features/shared/auth/login/presentation/login.dart

    auth: none. No seller path yet — sellerToken is null.

  • POST/auth/apple/callbackno status

    Exchange an Apple Sign-In identity payload for a customer session token.

    Request
    { id_token: string, access_token: string, email?: string, first_name?: string, last_name?: string }
    Response
    { token: string }
    lib/src/features/shared/auth/login/presentation/login.dart

    auth: none. Apple only sends name/email on first authorization; later logins rely on backend mapping by Apple subject ID.

Notes

The commented-out termsConsentService.getConsentRequirement block is the previous design — ToS is now enforced exclusively by the GoRouter redirect (see auth-routes-guard). Calls are delegated to AuthApiService — see the auth-api-service node for the wire-level inventory.