# UI Phase 3 — Auth & first-run

> **Mission:** the login screen is the product's only front door — the middleware redirects every
> unauthenticated hit to `/login` — and today it is a bare starter card fronted by a cartoon Twemoji
> pencil, with no consent line, no trust content, and an OTP flow that never offers SMS autofill in an
> OTP-first market. This phase rebuilds login → OTP → select-role → onboarding as a branded,
> trust-forward, Persian-native entry: a login hero that states what Balinyaar verifies and escrows,
> OTP ergonomics at Snapp/Digikala parity (WebOTP + `one-time-code`), terms/privacy consent, an
> illustrated role fork, a chrome-free focused onboarding journey, and a `returnUrl` so deep links
> survive login.
>
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
> **Unlocks:** the first impression finally sells trust
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
> and invoke the frontend-designer skill — both are mandatory.**

## 1. Context — where this sits

Balinyaar is a trust-first home-nursing marketplace; there is no public landing page (that is
[Phase 13](ui-phase-13-public-front-door.md)), so **login IS the product's first impression** — and in a
phone-OTP market, login is also signup. Mechanically the flow is strong (see the audit's keep-list); the
problems are presentation, ergonomics, and a handful of genuine defects, all verified in code:

1. **The login card carries zero trust evidence.** `client/src/components/auth/AuthCard.tsx:12-31` is an
   outlined `Paper` with a `BrandMark` and nothing else — no illustration, no mention of nurse
   verification or escrowed payment, on a product whose entire pitch is trust.
2. **No OTP autofill.** A repo-wide grep for `autoComplete`, `one-time-code`, and `OTPCredential` over
   `client/src` returns **zero hits** — `OtpInput.tsx:121-128` sets `inputMode`/`maxLength`/`aria-label`
   but never `autoComplete`, and there is no WebOTP wiring. Every login feels worse than the apps users
   compare against.
3. **No consent, no terms, no privacy.** `PhoneStep.tsx:56-102` renders title/field/CTA/role-switch only;
   `PUBLIC_PATHS = [ROUTES.LOGIN]` (`client/src/constants/routes.ts:169`) — no `/terms` or `/privacy`
   route exists anywhere. Entering a phone number creates an account with no disclosed terms.
4. **The 429 state is the least visible one.** `PhoneStep.tsx:76` renders `rate_limited` as grey
   `helperText` while `error={invalid}` stays false — the one state where the user is blocked gets no
   error styling.
5. **Bidi + digit defects on the OTP screen.** `OtpStep.tsx:100` interpolates the masked phone
   (`0912•••1234`) into an RTL sentence with no bidi isolation (the classic digits-around-neutrals
   reversal); `formatMmSs` (`OtpStep.tsx:24-28`) renders Latin `01:23` in a Persian sentence; backspace on
   an empty box (`OtpInput.tsx:89-93`) moves focus but never clears the previous digit.
6. **Select-role is semantically confusing.** `SelectRole.tsx:21-24` gives the nurse option a **house**
   icon (`{ role: 'nurse', icon: 'home' }`); selection feedback is border-color only
   (`SelectRole.tsx:74-83`) — color is the sole signal.
7. **Onboarding renders inside the full app shell.** `(customer)/onboarding/page.tsx` sits in the
   `(customer)` route group, so the A3→A4 wizard shows the 5-tab BottomBar, support icon, and bell — a
   user mid-setup can tab away. All four relation options share the same `'account'` icon
   (`onboarding/page.tsx:31`), and there is no welcome moment.
8. **Deep links die at login.** `middleware.ts:29` redirects to `/{locale}/login` discarding the attempted
   path — an SMS booking link or shared nurse profile dumps the user on their role home after login.

**What already exists (do not rebuild):**

- **The real brand mark and de-startered theme** — [Phase 0](ui-phase-0-design-language.md) owns
  `theme/`, the logotype replacing the Twemoji pencil, the icon registry, and `AppButton` (including
  killing its `margin: 1` starter default). Consume the mark via `BrandMark`/`ICONS.logo`; do not design
  a second one.
- **Shared primitives** — [Phase 1](ui-phase-1-primitives-and-states.md) owns `formatNumber` (the
  locale-digit helper), `StepperHeader`, and the state views. Consume them.
- **The de-startered public shell** — [Phase 2](ui-phase-2-shells-and-navigation.md) owns `layout/`,
  including retiring `PublicLayout`'s "Unauthorized - Balinyaar" chrome. This phase styles the **content**
  of the auth routes, not the shell.
- **The auth machinery** — `LoginFlow`/`PhoneStep`/`OtpStep`/`RoleRouter`/`AuthSplash`/`RoleGuard`/
  `SelectRole`/`useCountdown`, `services/auth` (real backend, `USE_AUTH_MOCK = false`), the middleware
  auth gate, and the resolved-vs-pending role hydration from refinement phase 2. This phase redesigns
  surfaces and adds ergonomics; the session/cookie/refresh/hydration logic is untouched.

## 2. Required reading (do this first)

- [audit/auth-first-run.md](audit/auth-first-run.md) — the full evidence, opportunities, and the
  **keep-list** this phase must not regress.
- The auth surface: `client/src/components/auth/` (`LoginFlow.tsx`, `AuthCard.tsx`, `BrandMark.tsx`,
  `PhoneStep.tsx`, `OtpStep.tsx`, `SelectRole.tsx`, `RoleRouter.tsx`, `constants.ts`),
  `client/src/components/OtpInput/OtpInput.tsx`, `client/src/components/PhoneNumberField/`,
  `client/src/services/auth/routing.ts` (`resolveRoleDestination` — pure, unit-tested).
- First-run: `client/src/app/[locale]/(private-routes)/(customer)/onboarding/page.tsx`,
  `client/src/components/RelationSelect/`, `client/src/components/StepperHeader/`, and the home redirect
  gate at `(customer)/page.tsx:60-68`.
- Routing: `client/middleware.ts`, `client/src/constants/routes.ts` (`ROUTES`, `PUBLIC_PATHS`).
- The design contract: [.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md)
  (invoke the skill) + `client/CLAUDE.md` Golden rules and the auth-cookies section (what you must not touch).
- Product truth for the trust copy: [product/overview/platform-summary.md](../../../product/overview/platform-summary.md)
  (the four ground truths), [product/business/02-nurse-verification.md](../../../product/business/02-nurse-verification.md)
  and [product/business/08-payments-and-escrow.md](../../../product/business/08-payments-and-escrow.md) —
  the trust bullets must state facts the platform actually implements, nothing aspirational.

## 3. Scope — build this

### 3.1 Login hero — a branded, trust-forward front door

Redesign `LoginFlow`/`AuthCard` as a branded moment on the cream backdrop (`background.default` /
`--bal-bg-default`): the Phase-0 logotype, the tagline «مراقبت مطمئن در خانه», and **2–3 trust bullets**
beneath the card stating what the product actually does — «پرستاران تاییدشده» (license + identity
verification), «پرداخت امن امانی» (escrow until confirmed check-out), «پشتیبانی» — each with a registry
icon. Add a calm illustration treatment: **composed CSS/SVG shapes in brand tokens are fine; no stock
photos, no raster illustrations.** Keep the card readable at 320px and let the hero breathe on desktop.
Keep the single login stack parameterized by `intendedRole` (`LoginFlow.tsx:22-24`, seeded from
`?role=nurse`) and the A1/B1 switch link — the nurse variant may re-tint copy, never fork the tree.
Keep the `' '` helperText placeholder that prevents layout jump.

### 3.2 OTP ergonomics — autofill, correction, and Persian digits

- **`autoComplete="one-time-code"`** on the `OtpInput` digit inputs (`slotProps.htmlInput`), so
  iOS/Android offer the SMS code as a keyboard suggestion.
- **WebOTP:** feature-detect `'OTPCredential' in window`, call
  `navigator.credentials.get({ otp: { transport: ['sms'] }, signal })` with an `AbortController`
  (abort on unmount and on manual completion), distribute the received code through the existing
  `onChange`/`onComplete` path so the existing auto-verify fires. Unsupported browsers silently fall back
  to manual entry — no errors, no UI difference.
- **File REQ-039** (see §4): WebOTP and native suggestion only work when the SMS **ends with the
  origin-bound line `@<domain> #<code>`** — the SMS template is server/Kavenegar-side.
- **Backspace-on-empty clears the previous box:** in `OtpInput.tsx:89-93`, when backspace is pressed on
  an empty box, clear `chars[index-1]` *and* move focus — one keypress per digit to erase.
- **Persian-digit countdown:** replace `formatMmSs`'s raw `String`/`padStart` (`OtpStep.tsx:24-28`) with
  the Phase-1 `formatNumber` helper so `/fa` renders ۰۱:۲۳ and `/en` renders 01:23. Keep the clock an
  LTR run inside the sentence.
- **429 as a real error:** in `PhoneStep.tsx:76`, the rate-limited state must set `error` on the field
  (e.g. `error={invalid || rateLimited}`) so the message renders in error styling, not grey helperText.
- **Bidi-isolate the masked phone echo:** wrap `maskIranMobile(phone)` in `OtpStep.tsx:100` in a
  `<bdi>`/`dir="ltr"` inline element (via `t.rich` or splitting the sentence) so `0912•••1234` never
  reorders inside the RTL sentence. Keep the masking itself.

### 3.3 Consent + legal pages

- Add the standard implicit-consent line under the login CTA in `PhoneStep`:
  «با ورود، [شرایط استفاده] و [حریم خصوصی] را می‌پذیرید» — the bracketed terms are `AppLink`s.
- Create `/terms` and `/privacy` under `(public-routes)` (`terms/page.tsx`, `privacy/page.tsx`):
  readable, Typography-composed legal skeletons in **both locales**, clearly flagged in the phase report
  and a code comment as **draft copy requiring human/legal review before launch**. Add
  `ROUTES.TERMS`/`ROUTES.PRIVACY` and append both to `PUBLIC_PATHS` (`routes.ts:169`) so the middleware
  lets them through logged-out.

### 3.4 Select-role — an illustrated two-card fork

Replace the generic icons (`SelectRole.tsx:21-24` — `account` for customer, a **house** for nurse) with
an illustrated two-card fork: *family receiving care* vs *nurse professional*, using the same CSS/SVG
illustration treatment as 3.1. Selected state = `--bal-primary-soft` fill **plus a check glyph** —
color must not be the only signal. Add a reassurance line that the other role can be added later
(dual-role sessions are already supported). **Keep the radio a11y semantics exactly**: `role="radio"`,
`aria-checked`, `tabIndex`, Enter/Space handlers (`SelectRole.tsx:64-72`). Admin is never offered here.
If a check/role icon is missing from the registry, extend `AppIcon/config.ts` minimally and note it in
the report (Phase 0 owns the file; later phases extend, never fork).

### 3.5 Onboarding as a focused journey

- **Move the A3→A4 wizard out of the customer shell chrome.** Today `onboarding/page.tsx` renders inside
  `CustomerLayout` (5-tab BottomBar + bell). Relocate it to a sibling route group, e.g.
  `(private-routes)/(customer-focused)/onboarding/`, whose layout keeps **`RoleGuard(expected=customer)`**
  (non-negotiable) but renders a chrome-free focused shell in the spirit of `AuthCard` — logo, progress,
  content, nothing to tab away to. Route groups don't change the URL, so `/onboarding`, the
  `ROUTES.ONBOARDING` constant, and the home redirect gate (`(customer)/page.tsx:63`) keep working
  untouched.
- **A one-screen welcome moment** before the relation step: «خوش آمدید — بگویید مراقبت برای چه کسی
  است؟»-style framing with the brand mark, one CTA into the wizard.
- **Distinct relation iconography:** the four options (`onboarding/page.tsx:31` — parent/spouse/child/
  self, currently all `'account'`) each get a distinct registry icon so the first product interaction
  isn't four identical cards.
- **Progress via `StepperHeader`** (welcome doesn't count as a step; relation → patient does).
- **Keep** the relation pre-shaping the patient form (relation field hidden on A4) and the settled-list
  redirect gate exactly as they are.
- (DEFERRED → [Phase 4](ui-phase-4-customer-storefront.md) + product decision) a «بعداً تکمیل می‌کنم»
  skip path with a browse-capable home: it changes the zero-patient home gate, which is storefront
  territory. The forced redirect stays as-is in this phase.
- (DEFERRED → [Phase 12](ui-phase-12-copy-motion-and-polish.md)) OTP delivery fallback escalation
  (voice-call OTP / support link after failed resend cycles) — needs a second delivery channel that
  doesn't exist server-side.

### 3.6 returnUrl — deep links survive login

- `middleware.ts:29`: when redirecting an unauthenticated hit to login, append the attempted
  locale-stripped path + query as `?next=<encoded>` — e.g.
  `/fa/bookings/42` → `/fa/login?next=%2Fbookings%2F42`.
- Login honors it: `LoginFlow` reads `next` and passes it to `RoleRouter`; add a **pure, unit-tested**
  helper beside `resolveRoleDestination` in `services/auth/routing.ts` (e.g.
  `resolvePostLoginDestination(me, intendedRole, next)`) that returns `next` only when it is (a) a
  same-origin **relative** path — starts with `/`, not `//`, no scheme — and (b) permitted by the
  resolved roles (a customer's `next=/nurse/...` falls through). Anything else falls back to
  `resolveRoleDestination` — which stays the single "which app" source of truth.
- Touch `middleware.ts` **surgically**: the token check, locale detection, header propagation, and
  next-intl handling are load-bearing; the auth gate semantics must not change.

### 3.7 PhoneNumberField autofill

Add `autoComplete="tel"` to the `PhoneNumberField` input so browsers/keyboards offer the user's own
number. Keep the LTR forcing and digit normalization untouched.

## 4. Mocks & seams in this phase

**None introduced.** Auth is real (`USE_AUTH_MOCK = false` since refinement phase 4); every deliverable
here is presentation, ergonomics, or client routing. Do not flip any mock flag.

**REQ posture:** backend gaps become REQ entries appended to
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
— REQ-001…038 are taken; number onward. This phase files exactly one:

- **REQ-039 — WebOTP-conformant OTP SMS template.** The OTP SMS must end with the origin-bound last line
  `@<domain> #<code>` (the [WebOTP](https://web.dev/articles/web-otp) / origin-bound one-time-code
  convention) so Chrome's WebOTP API and the iOS/Android keyboard suggestion can auto-read the code. This
  is a server/SMS-template change (the Kavenegar adapter from refinement phase 8); zero API-shape impact.
  The client work in 3.2 ships regardless and degrades to manual entry until this lands.

## 5. Critical rules you must not get wrong

- **Do not touch the auth/session machinery.** `services/auth` session logic, `persistAuthTokens`/
  `clearAuthTokens`, the fetch-layer silent refresh, `useLogout`, cookie handling, and the
  `RoleGuard`/`useRoleHydration` resolved-vs-pending behavior were hardened in refinement phases 2 and 5.
  This phase restyles screens and adds a routing helper — nothing else.
- **The no-wrong-shell-flash behavior stays.** `RoleRouter` + branded `AuthSplash` while `/me` is in
  flight, `AuthAccountError` on failure, `RoleGuard` redirect-with-toast on mismatch. The relocated
  onboarding layout must keep `RoleGuard(expected=customer)`.
- **OTP mechanics that already work stay:** paste distribution across boxes, Persian/Arabic→ASCII digit
  normalization, `dir="ltr"` on the OTP group and phone field, auto-verify on the 5th digit, the
  server-driven resend cooldown (`resendAvailableInSeconds` seeds `useCountdown`), resend staying
  available during lockout as the escape hatch, and the distinct wrong/expired/locked/429 states.
- **`next` must never become an open redirect.** Only same-origin relative paths, validated in the pure
  helper (with unit tests covering `//evil.com`, `https:` schemes, and role-forbidden paths).
- **One login stack.** `intendedRole` parameterizes; it never forks customer/nurse login trees.
- **Admin roles are never offered** in SelectRole; the middleware stays a UX gate, not a security boundary.
- **Design-contract non-negotiables:** every new string in **both** `messages/en.json` and
  `messages/fa.json`; tokens/palette keys, never hexes; RTL-safe logical props (verify `/fa` first);
  both color schemes; MUI v9 API only; illustrations are CSS/SVG in tokens — no stock photos; shared
  components (`OtpInput`, `PhoneNumberField`, `RelationSelect`, anything new in `components/`) keep/get
  co-located `.test.tsx`; `clientFetch`/cookie rules untouched.

## 6. Definition of Done

On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):

- [ ] `npm run check` green; `npm run test:ci` green for every touched shared component
      (`OtpInput`, `PhoneNumberField`, `RelationSelect`, the new routing helper's unit tests, and any
      new shared component); `en.json`/`fa.json` in sync.
- [ ] Visual verification on all four axes — `/fa` + `/en` × light + dark — and mobile (360px) +
      desktop for login, OTP, select-role, terms/privacy, and onboarding.
- [ ] The login screen shows the brand mark, tagline, trust bullets, and consent line; no starter
      chrome, no pencil, no hard-coded English anywhere in the flow.
- [ ] OTP inputs carry `autoComplete="one-time-code"`; WebOTP auto-fills on a supporting
      Android/Chrome device (or is verified degrading silently elsewhere); backspace clears the
      previous digit in one keypress.
- [ ] The resend countdown renders ۰۱:۲۳ on `/fa` and 01:23 on `/en`; the masked phone echo renders
      un-scrambled inside the Persian sentence.
- [ ] A 429 on request-OTP renders with error styling on the field, not grey helper text.
- [ ] `/terms` and `/privacy` load logged-out in both locales; the consent line links to them.
- [ ] Select-role shows the illustrated fork; the selected card has a soft fill + check glyph; keyboard
      selection (Tab, Enter/Space) still works.
- [ ] `/onboarding` renders chrome-free (no BottomBar/bell), opens with the welcome screen, shows four
      visually distinct relation cards, and still creates the patient and lands on Home.
- [ ] Visiting `/fa/bookings` logged-out → login → after OTP, landing back on `/fa/bookings`;
      `?next=//evil.com` and `?next=https://evil.com` are ignored (role-home fallback).
- [ ] REQ-039 appended to the tracker.

## 7. How to test (what a human can verify after this phase)

1. Open `/fa` logged-out → redirected to `/fa/login`. The screen is a branded hero: logotype, tagline,
   trust bullets, consent line with working «شرایط استفاده»/«حریم خصوصی» links. Toggle dark mode and
   `/en` — everything tracks.
2. Enter a phone (demo seeds `0912000000x`), request the code. On the OTP screen the masked number reads
   `0912•••…` correctly inside the Persian sentence; the countdown ticks in Persian digits on `/fa`.
3. Type a wrong digit mid-code, press backspace twice — each press erases one digit. Paste a 5-digit
   code — boxes fill and verify fires automatically (unchanged).
4. Spam request-OTP until a 429 → the message renders in error styling on the field.
5. On an Android/Chrome device with a WebOTP-formatted SMS (post REQ-039): the code fills without
   typing. On desktop Firefox/Safari: no errors in console; manual entry works.
6. Log in with a fresh phone → `/select-role` shows two illustrated cards; select the nurse card —
   soft fill + check glyph appear; the reassurance line about adding the other role later is present;
   arrow/Enter keyboard selection works.
7. As a fresh customer with zero patients → land on `/onboarding`: welcome screen → relation step with
   four distinct icons → patient form (relation pre-filled and hidden) → save → Home. During the wizard
   there is no bottom nav or bell to tab away to.
8. Logged-out, open `/fa/bookings` → login carries `?next=%2Fbookings` → after OTP you land on
   `/fa/bookings`. Repeat with `?next=https://evil.com` manually — you land on your role home.
9. Sanity: a seeded nurse still lands on `/nurse`, an admin on `/admin` (RoleRouter untouched); `?role=nurse`
   still pre-selects the nurse copy and carries into select-role.

## 8. Hand off & document (close the phase)

- Update `client/CLAUDE.md` **Project Structure**: the new `(public-routes)` `terms`/`privacy` pages, the
  relocated onboarding route group and its focused layout, any new/renamed auth components, and the new
  routing helper — in the same change.
- Append **REQ-039** to
  [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) as specified in §4.
- Write the frontend report at `dev/shared-working-context/reports/ui-phase-3-report.md`: what shipped
  per scope item, the REQ filed, the **draft-legal-copy flag** for `/terms`/`/privacy` (human/legal
  review required before launch), any icon-registry additions made under the Phase-0 ownership rule, and
  the four-axes verification evidence.
- Save a memory note per operating-rules §8: auth/first-run redesigned (login hero + consent, WebOTP +
  OTP ergonomics, illustrated select-role, focused onboarding, validated `returnUrl`), REQ-039 pending
  server-side SMS template, skip-onboarding deliberately deferred to Phase 4.
