ui phase 3
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# 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.
|
||||
|
||||
### 3.3 Consent + legal pages
|
||||
`PhoneStep` renders a consent line under the CTA («با ورود، شرایط استفاده و حریم خصوصی را
|
||||
میپذیرید») via `t.rich` with `<terms>`/`<privacy>` tag functions rendering `AppLink`s. 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).
|
||||
|
||||
### 3.6 returnUrl — deep links survive login
|
||||
`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:ci`** — **98 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).
|
||||
Reference in New Issue
Block a user