# 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 `@ #`** — 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 ``/`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=` — 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 `@ #` (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.