Skip to main content

[Employer Sync 6.3] Honest login states (provisioning / needs-attention) on the login page

TL;DR: Drive two new login banner states (provisioning, needs_attention) from typed API error codes (MigrationInProgress 409, AccountNotVerified 403) — never page-side guessing. All other failures stay the inline "invalid email, phone, or password" error.

Context

The current login page renders the same generic Invalid email or password for every failure — wrong password, account being set up by the JIT (issue 5.2), account needs verification. A user who types the correct password and waits a moment for the JIT to complete is told their password is wrong.

Problem

The page treats all failures alike. The backend does not currently return typed error codes the page could distinguish. We need both sides to change, in a coordinated way that does not leak account existence.

Direction

Backend. In Identities::Sessions::CreateManager:

  • The JIT detect-and-enqueue path (issue 5.2) raises Errors::MigrationInProgress with code: 'migration_in_progress', HTTP status 409.
  • The verification-locked case — decide whether to expose it. After 4.2 forces is_email_verified: true for migrated employers, the verification gate rarely fires for them. If the broader product still has a verification path that can land here, return a distinct Errors::AccountNotVerified with code: 'account_not_verified', HTTP 403. If exposing it would leak account existence in any product flow, keep the generic error.
  • Every other failure stays Invalid email, phone number, or password.

Frontend (app/routes/employers/employers-login-page.jsx):

  • Add a loginState state: 'normal' | 'provisioning' | 'needs_attention'.
  • In the login handler's catch, inspect error.code / error.status:
    • code === 'migration_in_progress' (or 409) → setLoginState({ kind: 'provisioning' }), do NOT set inline field errors.
    • code === 'account_not_verified' (or 403, if exposed) → setLoginState({ kind: 'needs_attention' }).
    • Anything else → setLoginState({ kind: 'normal' }) and set inline field errors on identifier AND password.
  • Render the banner above the form, conditional on state:
    • provisioningAlert variant="info", heading Setting up your account, body We are getting your account ready. This takes a minute. Please wait, then log in again.
    • needs_attentionAlert variant="warning", heading One more step to access your account, body For your security, please set your password before you log in. It only takes a minute., button Set my password linking to /forgot-password.

The full design (placement, Bootstrap classes, ASCII wireframe) is in the UI/UX review on 6.5.

Acceptance

  • Backend returns Errors::MigrationInProgress with HTTP 409 and code: migration_in_progress from the JIT path.
  • Frontend renders the provisioning banner when the API returns 409 with that code.
  • Frontend falls back to inline field errors for any other failure.
  • Tests cover all three states.

Depends on

  • 5.2 (the JIT path that emits the typed error)
  • 6.1 (the copy this banner lives alongside)