Files
baya-monorepo/dev/post-phase/ui/ui-phase-3-auth-and-first-run.md
T
2026-07-17 13:22:04 +03:30

21 KiB
Raw Blame History

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 · Unlocks: the first impression finally sells trust Before you start, read ../../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), 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 hitsOtpInput.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 themePhase 0 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 primitivesPhase 1 owns formatNumber (the locale-digit helper), StepperHeader, and the state views. Consume them.
  • The de-startered public shellPhase 2 owns layout/, including retiring PublicLayout's "Unauthorized - Balinyaar" chrome. This phase styles the content of the auth routes, not the shell.
  • The auth machineryLoginFlow/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 — 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 (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 (the four ground truths), product/business/02-nurse-verification.md and 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.
  • Add the standard implicit-consent line under the login CTA in PhoneStep: «با ورود، [شرایط استفاده] و [حریم خصوصی] را می‌پذیرید» — the bracketed terms are AppLinks.
  • 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-24account 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 + 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) OTP delivery fallback escalation (voice-call OTP / support link after failed resend cycles) — needs a second delivery channel that doesn't exist server-side.
  • 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 — 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 / 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:

  • 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 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.