Skip to Content
FeaturesOnboardingOnboarding journey

Onboarding journey

The user-facing path from “first launch” to “logged in on the home dashboard”: signup → verify email → first login (with inline FCM + timezone capture) → dashboard load. The auth subsystem that backs every step (JWT, refresh, password reset, token storage) is documented separately at Auth subsystem.

Journey at a glance

Every transition is driven by AuthViewModel collecting AuthState in shared/commonMain. The same screens render on Android, iOS, and desktop — no per-platform onboarding code.

What happens at each step

1. Signup

The user submits name, email, password. The client sends POST /api/v1/auth/register (gated by the SIGN_UP feature flag per X-Platform — disabled platforms see a 403 with FEATURE_DISABLED). On success the server inserts the user with email_verified_at = null and immediately fires a 6-digit PIN to their email via EmailVerificationService.

The UI transitions to Email-pending:

  • Shows the email the PIN was sent to (so the user can spot typos).
  • “Resend” button calls POST /api/v1/auth/request-verification-email (rate-limited under forgot — 5/10 min per IP).
  • “Wrong email?” → back to signup.

2. Email verification

The user enters the 6-digit PIN. POST /api/v1/auth/verify-email {email, code} flips Users.emailVerifiedAt to now. The UI auto-advances to the Login screen (or directly to login-and-land if the password is still in memory — see note below).

Login attempts before verification return 403 EMAIL_NOT_VERIFIED, which AuthViewModel maps to Unauthenticated(pendingVerificationEmail = email) so the UI can route back to the Email-pending screen. This is the recovery path for users who close the app before verifying.

3. First login

POST /api/v1/auth/login is a single transaction that does three things:

  1. Verifies credentials → returns AuthResponse{token, refreshToken, tier}.
  2. Captures the device’s FCM token if the client passed fcmToken in the body — the server calls timerService.registerDeviceToken(userId, fcmToken, "android"). ⚠️ The platform string is hardcoded to "android" in AuthRouting.kt; iOS / desktop clients must follow up with POST /api/v1/auth/device-token and the correct platform after login.
  3. Captures the device’s IANA timezone if the client passed timezone — server validates and updates Users.timeZone (silent if invalid).

On success the client stores both tokens in TokenStorage (per-platform DataStore on Android/iOS/desktop, localStorage on web targets) and routes to the dashboard.

4. Dashboard load

Immediately after Authenticated(tier), the dashboard view-model fires:

The event bus decouples the dashboard refresh from the action that triggered it — no explicit callback wiring needed between features.

Password reset (user-facing)

Three screens. The server mechanics (PIN hashing, opaque session token, refresh-session sweep) are in Auth subsystem → Password reset.

  1. Forgot password — user enters email → POST /auth/forgot-password. Server always returns 200 (anti-enumeration). UI advances unconditionally.
  2. PIN entry — user receives a 6-digit code valid for 10 minutes, 5 attempts. On success → POST /auth/reset-password/verify returns a short-lived reset session token the UI holds in memory only.
  3. New password — UI submits POST /auth/reset-password {resetToken, newPassword}. Server hashes the new password, marks the session used, and revokes every active refresh session for the user. The UI routes back to login.

Returning user path

On every app launch AuthViewModel.init dispatches CheckAuth:

  • TokenStorage.getToken() is non-null → AuthService.fetchMe() validates against GET /api/v1/auth/me. If 200, state → Authenticated(tier) and the dashboard loads. If 401, the HTTP client validator tries /auth/refresh once; success keeps the user signed in transparently, failure broadcasts SessionExpiredNotifier → state → Unauthenticated(errorMessage="Your session expired…").
  • TokenStorage.getToken() is null → state → Unauthenticated. Login screen.

The refresh token persists across app restart and process kill in the same DataStore/localStorage slot as the access token; the user only re-enters credentials when the refresh chain is revoked (logout, password reset, replay-detection, refresh expiry after 30 days).

Logout

User taps Logout in settings:

  1. Client calls AuthService.logout():
    • Fires POST /api/v1/auth/logout with the stored refresh token (best-effort; network errors are swallowed).
    • Clears both auth_token and refresh_token from TokenStorage.
  2. AuthState transitions to Unauthenticated and the UI returns to the login screen.

Server-side, the supplied refresh token is marked revoked_at = now so any leaked copy stops working immediately. Other devices the same user is logged in on are not affected — those have their own refresh sessions.

Client building blocks

ModuleRole
AuthViewModelSingle source of AuthState; collects SessionExpiredNotifier; reacts to AuthIntents
AuthStateLoading · Authenticated(tier) · Unauthenticated(email, password, name, errorMessage, isLoading, pendingVerificationEmail)
AuthIntentCheckAuth · Login · Register · Logout · SessionExpired
AuthServiceHTTP client for all /auth/* endpoints; persists tokens via TokenStorage; returns ApiOutcome<T>

Troubleshooting (onboarding-specific)

“I entered the right PIN but verify-email rejects it”

  • PIN expires after 10 minutes. Tap Resend (rate-limited 5/10 min per IP).
  • 5 wrong attempts on the same PIN locks it — new PIN required.

“I never received the verification email”

  • Check spam / promotional inboxes. SES sender is noreply@<your-domain>.
  • For SES domain verification issues, see FCM troubleshooting (same SES infrastructure).

“After verify-email, login still says EMAIL_NOT_VERIFIED”

  • Stale 403 cached in AuthState.errorMessage. The next manual login attempt clears it.

“Logged in on desktop, not getting push notifications”

  • Desktop never sends an FCM token. Push is mobile-only by design today. See Notifications for the device-token wiring and platform support matrix.

← Back to features index