Files
baya-monorepo/dev/shared-working-context/reports/ui-phase-3-report.md
T
2026-07-18 01:56:17 +03:30

14 KiB

UI Phase 3 — Auth & first-run — Report (2026-07-17)

What was built

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

AuthCard is redesigned as a hero: on ≥md a calm abstract illustration (AuthIllustration, new — layered soft-tint circles in --bal-primary-soft/--bal-secondary-soft with a centered family glyph and a floating verified trust badge, all CSS/SVG in brand tokens, no stock art) sits beside the card; below the card, TrustBullets (new) renders three facts the platform actually implements — licensed + identity-verified nurses, escrow held until a confirmed check-out, support — each with a registry icon (verification/lock/support, all pre-existing). The card stays a single AUTH_CARD_MAX_WIDTH (420px) column at xs, readable at 320px; the illustration is display:none below md. LoginFlow's single stack (intendedRole parameter, never a forked tree) and the ' ' helperText placeholder are untouched.

3.2 OTP ergonomics

  • OtpInput: autoComplete="one-time-code" on every digit box; backspace on an empty box now clears the previous box and moves focus there in one keypress (was: focus-only, needing two presses to erase a digit).
  • components/auth/useWebOtp.ts (new): feature-detects 'OTPCredential' in window, calls navigator.credentials.get({ otp: { transport: ['sms'] }, signal }) with an AbortController, aborted on unmount and whenever verification starts (active flips false). Wired into OtpStep so a WebOTP read feeds the same setCode/verify path manual entry uses. Unsupported browsers (desktop, Firefox, Safari) silently no-op.
  • OtpStep: the masked-phone echo and the resend countdown are now rendered via t.rich with a <Box component="bdi" dir="ltr"> wrapper — bidi-isolated so the RTL sentence's bidi algorithm can't reorder the digit/bullet runs around them. The countdown uses the Phase-1 formatClock helper (Persian digits on /fa, Latin on /en) instead of the old raw String/padStart.
  • PhoneStep: error={invalid || rateLimited} — the 429 rate-limit message now renders in error styling on the field instead of grey helper text.

PhoneStep renders a consent line under the CTA («با ورود، شرایط استفاده و حریم خصوصی را می‌پذیرید») via t.rich with <terms>/<privacy> tag functions rendering AppLinks. New ROUTES.TERMS/ROUTES.PRIVACY (/terms, /privacy), both appended to PUBLIC_PATHS. New pages (public-routes)/terms/page.tsx and .../privacy/page.tsx — Server Components (no 'use client' needed; matches the existing not-found.tsx pattern of RSCs rendering MUI/BrandMark directly), data-driven from a new legal i18n namespace (terms_sections/privacy_sections are arrays of {title, body} read via t.raw — the one namespace using structured JSON, everywhere else stays flat keys). Each page opens with an AppAlert severity="info" draft-copy banner.

⚠️ Draft legal copy — human/legal review required before launch. The Terms of Service and Privacy Policy text (8 sections + 7 sections, both locales) was written by this agent from the product docs (platform-summary.md, 02-nurse-verification.md, 08-payments-and-escrow.md) to be factually accurate to what the platform does today, but it is not attorney-reviewed and must not be treated as binding before a legal pass.

3.4 Select-role — illustrated fork

ROLE_OPTIONS icons changed from {customer: 'account', nurse: 'home'} (a house for "I am a nurse" was the defect) to {customer: 'family', nurse: 'visits'} (FamilyRestroomRounded/ MedicalServicesRounded, both pre-existing registry entries). Each card gets a 56px circular icon badge (paper background + --bal-shadow-1, echoing the login hero's illustration language). Selected state = --bal-primary-soft fill and a verified check glyph at the row's end (a fixed-width placeholder Box holds its position when unselected, so nothing shifts) — color is no longer the only signal. Added a reassurance line (role_add_later_note) below the cards. Radio a11y semantics (role="radio", aria-checked, tabIndex, Enter/Space) are unchanged.

3.5 Onboarding as a focused journey

Relocated /onboarding from (private-routes)/(customer)/onboarding/ to a new sibling route group (private-routes)/(customer-focused)/onboarding/ (the old file is deleted outright, not shimmed — two onboarding/page.tsx at the same URL would conflict). The new route group's layout.tsx keeps RoleGuard(expected=customer) and wraps children in a new FocusedLayout (src/layout/) — a slim logo-only header, no BottomBar/bell/sidebar, in the spirit of AuthCard/PublicLayout. Route groups add no URL segment, so /onboarding, ROUTES.ONBOARDING, and the home redirect gate ((customer)/page.tsx isEmpty effect) all keep working untouched.

OnboardingScreen.tsx (new, replaces the old inline onboarding/page.tsx body) adds a 'welcome' phase before 'relation'/'patient' — brand mark, «خوش آمدید — مراقبت برای چه کسی است؟» framing, one CTA. StepperHeader only renders for the two counted steps (welcome doesn't count). The four relation options get distinct icons instead of all sharing 'account': parent→elderly, spouse→favorite (new registry icon, FavoriteRounded — noted per the Phase-0 ownership rule), child→infant, self→account (the latter three already registered). The relation-pre-shapes-patient-form behavior and the settled-list redirect gate are untouched.

Deferred, per phase scope: a «بعداً تکمیل می‌کنم» skip path (→ Phase 4, changes the zero-patient home gate) and OTP voice-call fallback (→ Phase 12, needs a second delivery channel server-side).

middleware.ts: on the redirect-to-login it now appends ?next=<pathWithoutLocale + search> (new RETURN_URL_PARAM = 'next' constant) — e.g. /fa/bookings/42/fa/login?next=%2Fbookings%2F42. The token check, locale detection, header propagation, and next-intl handling are untouched; only the redirect branch gained the extra searchParams.set.

services/auth/routing.ts adds resolvePostLoginDestination(me, intendedRole, next) (pure, exported, unit-tested) beside the existing resolveRoleDestination: returns next only when (a) isSafeRelativePath — starts with /, not //, not /\ (rejects protocol-relative and absolute-URL values; an absolute URL like https://evil.com already fails the startsWith('/') check) — and (b) appRoleForPath(next) resolves to a role the session's toAppRoles(me.roles) actually holds (a customer's next=/nurse/... falls through; the partner portal, not an AppRole, always falls through). Otherwise defers to resolveRoleDestination, which stays the single "which app" source of truth. LoginFlow reads next from useSearchParams() and passes it to RoleRouter, which now calls resolvePostLoginDestination instead of resolveRoleDestination directly (the select-role-with-nurse-intent carry-through logic is unchanged, layered on top of the resolved destination).

3.7 PhoneNumberField autofill

autoComplete="tel" added to the default slotProps.htmlInput; LTR forcing and digit normalization untouched.

What is now testable (and exactly how)

  1. /fa logged-out → /fa/login: branded hero (logotype, tagline, illustration on desktop, 3 trust bullets, consent line with working «شرایط استفاده»/«حریم خصوصی» links). Toggle dark mode and /en — tracks.
  2. Enter a phone, request the code → OTP screen: masked number renders un-scrambled inside the Persian sentence; the resend countdown ticks in Persian digits on /fa, Latin on /en.
  3. Wrong digit + backspace twice on the OTP boxes — each press erases one digit. Paste a 6-digit code — auto-verifies (unchanged).
  4. Spam request-OTP to trigger a 429 → the phone field renders in error styling, not grey helper text.
  5. /terms and /privacy load logged-out, both locales — draft-copy banner + 7-8 sections each.
  6. Fresh phone → /select-role: two illustrated cards (family/nurse icons, not house); selecting shows soft fill + check glyph; reassurance line present; Tab/Enter/Space still work.
  7. Zero-patient customer → /onboarding: welcome screen (brand mark + CTA, no stepper) → relation (4 distinct icons) → patient form (relation hidden) → save → Home. No BottomBar/bell visible at any point in the wizard.
  8. A seeded nurse still lands on /nurse; ?role=nurse still pre-selects nurse copy through to select-role (both unchanged branches, exercised by the existing + new RoleRouter tests).

Verification performed this session

  • npm run check (type + lint) — clean.
  • npm run test:ci98 suites / 420 tests pass (was 98/408 going in — this phase added a resolvePostLoginDestination test block to routing.test.ts, two RoleRouter tests, an autoComplete/backspace test to OtpInput.test.tsx, and an autoComplete test to PhoneNumberField.test.tsx; OtpStep.test.tsx's next-intl mock was extended with useLocale and a t.rich stub so the existing suite kept passing against the rewritten component).
  • npm run dev + SSR smoke (via curl --noproxy "*", working around the same loopback-proxy 502 Phase 2's report already documented): /fa/login, /en/login, /fa/terms, /fa/privacy all return 200 with no error-boundary/MISSING_MESSAGE markers in the HTML; /fa/terms was grepped for its own distinctive section title («ماهیت خدمت») and the draft banner («این متن پیش‌نویس است») to confirm real translated content renders, not just the hydration payload.
  • Not conclusively verified this session: the ?next= middleware redirect itself. Requesting a private route with no cookie (curl, and independently a .NET HttpClient from PowerShell with UseProxy=$false — two unrelated HTTP stacks, ruling out the proxy explanation) returned 200 with the page's own content instead of a 3xx to /login, for both routes this phase never touches (/fa/nurse/profile, /fa/bookings/999 — the latter a dynamic segment, so not a static-cache artifact either) and routes it does. A console.log placed at the very top of middleware.ts never printed for any of these requests, even after a full dev-server restart — the middleware bundle compiles (.next/dev/server/middleware/middleware-manifest.json lists it with a correct matcher regex) but does not appear to run for a bare HTTP GET in this next dev/Turbopack session. Since the exact same non-redirect happens on untouched pre-existing private routes with byte-identical, unmodified redirect logic (confirmed via git show HEAD:client/middleware.ts), this reads as an environment/toolchain anomaly specific to this Next.js 16.2.9-canary + Turbopack dev session, not a regression from this phase's diff — but I could not get a real browser in front of it to confirm the alternate hypothesis (that actual browser navigation, unlike a cold HTTP client request, does trigger it correctly). A production build (npm run build) also could not confirm this independently: it fails during static-page prerendering on /fa/addresses, /fa/partner/bookings, /fa/nurse/earnings with Error: Missing .env variable! inside src/components/notifications/index.ts — a pre-existing production-build/env-config gap unrelated to this phase (not investigated further, out of scope). Confidence in the next-handling code itself comes from resolvePostLoginDestination's unit tests (open-redirect guards for //evil.com and https://evil.com, role-forbidden-path fallback, role-less-user fallback, happy path) and RoleRouter's two new tests exercising the same function through the component — not from an end-to-end browser confirmation. Flagging this explicitly per the "don't claim UI success you can't see" rule — recommend a human re-verify item 8 in "How to test" (§7 of the phase doc) in an actual browser before merging.

What is mocked / waiting on a real service

None introduced. Auth stays real (USE_AUTH_MOCK = false); every deliverable here is presentation, ergonomics, or client-side routing — no mock flag was flipped.

Contracts

None produced or consumed as an endpoint change. One request filed:

  • REQ-039 — WebOTP-conformant OTP SMS template (@<domain> #<code> origin-bound last line) — a server/Kavenegar-adapter SMS-template change, zero API-shape impact. The client-side WebOTP wiring (useWebOtp + autoComplete="one-time-code") ships regardless and degrades to manual entry until the template lands.

Docs updated

  • client/CLAUDE.md — Project Structure: the new (customer-focused)/onboarding/ route group, the deleted (customer)/onboarding/, the new terms//privacy/ pages, FocusedLayout.tsx, the components/auth/ bullet (added useWebOtp, AuthIllustration, TrustBullets); the i18n namespace list (auth additions, new legal namespace, onboarding's welcome_*); the Middleware section (returnUrl behavior + resolvePostLoginDestination).

Follow-ups for later phases

  • Human/legal review of /terms and /privacy before launch — flagged in-page (draft banner) and here; not a code follow-up, a legal one.
  • Re-verify the ?next= middleware redirect in an actual browser (see "Verification performed" above) — the code is unit-tested and unchanged in its core logic, but this session's tooling couldn't get a conclusive live HTTP confirmation.
  • The production-build failure (Missing .env variable! in src/components/notifications/index.ts during static prerendering of /fa/addresses, /fa/partner/bookings, /fa/nurse/earnings) is pre-existing and unrelated to this phase — worth a ticket so npm run build is verifiable end-to-end by a future phase.
  • Skip-onboarding («بعداً تکمیل می‌کنم») → Phase 4 (changes the zero-patient home gate, storefront territory). OTP voice-call fallback → Phase 12 (needs a second server-side delivery channel).