Files
baya-monorepo/archive/post-phase/ui/ui-phase-3-auth-and-first-run.md
T
2026-08-02 18:48:32 +03:30

287 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 02](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 **23 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.