21 KiB
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 areturnUrlso deep links survive login.Track: frontend · Depends on: Phases 0–2 · 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:
- The login card carries zero trust evidence.
client/src/components/auth/AuthCard.tsx:12-31is an outlinedPaperwith aBrandMarkand nothing else — no illustration, no mention of nurse verification or escrowed payment, on a product whose entire pitch is trust. - No OTP autofill. A repo-wide grep for
autoComplete,one-time-code, andOTPCredentialoverclient/srcreturns zero hits —OtpInput.tsx:121-128setsinputMode/maxLength/aria-labelbut neverautoComplete, and there is no WebOTP wiring. Every login feels worse than the apps users compare against. - No consent, no terms, no privacy.
PhoneStep.tsx:56-102renders title/field/CTA/role-switch only;PUBLIC_PATHS = [ROUTES.LOGIN](client/src/constants/routes.ts:169) — no/termsor/privacyroute exists anywhere. Entering a phone number creates an account with no disclosed terms. - The 429 state is the least visible one.
PhoneStep.tsx:76rendersrate_limitedas greyhelperTextwhileerror={invalid}stays false — the one state where the user is blocked gets no error styling. - Bidi + digit defects on the OTP screen.
OtpStep.tsx:100interpolates 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 Latin01:23in a Persian sentence; backspace on an empty box (OtpInput.tsx:89-93) moves focus but never clears the previous digit. - Select-role is semantically confusing.
SelectRole.tsx:21-24gives 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. - Onboarding renders inside the full app shell.
(customer)/onboarding/page.tsxsits 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. - Deep links die at login.
middleware.ts:29redirects to/{locale}/logindiscarding 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 owns
theme/, the logotype replacing the Twemoji pencil, the icon registry, andAppButton(including killing itsmargin: 1starter default). Consume the mark viaBrandMark/ICONS.logo; do not design a second one. - Shared primitives — Phase 1 owns
formatNumber(the locale-digit helper),StepperHeader, and the state views. Consume them. - The de-startered public shell — Phase 2 owns
layout/, including retiringPublicLayout'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 — 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.mdGolden 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 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 theOtpInputdigit inputs (slotProps.htmlInput), so iOS/Android offer the SMS code as a keyboard suggestion.- WebOTP: feature-detect
'OTPCredential' in window, callnavigator.credentials.get({ otp: { transport: ['sms'] }, signal })with anAbortController(abort on unmount and on manual completion), distribute the received code through the existingonChange/onCompletepath 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, clearchars[index-1]and move focus — one keypress per digit to erase. - Persian-digit countdown: replace
formatMmSs's rawString/padStart(OtpStep.tsx:24-28) with the Phase-1formatNumberhelper so/farenders ۰۱:۲۳ and/enrenders 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 seterroron 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)inOtpStep.tsx:100in a<bdi>/dir="ltr"inline element (viat.richor splitting the sentence) so0912•••1234never 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 areAppLinks. - Create
/termsand/privacyunder(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. AddROUTES.TERMS/ROUTES.PRIVACYand append both toPUBLIC_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.tsxrenders insideCustomerLayout(5-tab BottomBar + bell). Relocate it to a sibling route group, e.g.(private-routes)/(customer-focused)/onboarding/, whose layout keepsRoleGuard(expected=customer)(non-negotiable) but renders a chrome-free focused shell in the spirit ofAuthCard— logo, progress, content, nothing to tab away to. Route groups don't change the URL, so/onboarding, theROUTES.ONBOARDINGconstant, 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.
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:
LoginFlowreadsnextand passes it toRoleRouter; add a pure, unit-tested helper besideresolveRoleDestinationinservices/auth/routing.ts(e.g.resolvePostLoginDestination(me, intendedRole, next)) that returnsnextonly when it is (a) a same-origin relative path — starts with/, not//, no scheme — and (b) permitted by the resolved roles (a customer'snext=/nurse/...falls through). Anything else falls back toresolveRoleDestination— which stays the single "which app" source of truth. - Touch
middleware.tssurgically: 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/authsession logic,persistAuthTokens/clearAuthTokens, the fetch-layer silent refresh,useLogout, cookie handling, and theRoleGuard/useRoleHydrationresolved-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+ brandedAuthSplashwhile/meis in flight,AuthAccountErroron failure,RoleGuardredirect-with-toast on mismatch. The relocated onboarding layout must keepRoleGuard(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 (resendAvailableInSecondsseedsuseCountdown), resend staying available during lockout as the escape hatch, and the distinct wrong/expired/locked/429 states. nextmust 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.
intendedRoleparameterizes; 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.jsonandmessages/fa.json; tokens/palette keys, never hexes; RTL-safe logical props (verify/fafirst); both color schemes; MUI v9 API only; illustrations are CSS/SVG in tokens — no stock photos; shared components (OtpInput,PhoneNumberField,RelationSelect, anything new incomponents/) 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 checkgreen;npm run test:cigreen for every touched shared component (OtpInput,PhoneNumberField,RelationSelect, the new routing helper's unit tests, and any new shared component);en.json/fa.jsonin 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
/faand 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.
/termsand/privacyload 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.
/onboardingrenders 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/bookingslogged-out → login → after OTP, landing back on/fa/bookings;?next=//evil.comand?next=https://evil.comare ignored (role-home fallback). - REQ-039 appended to the tracker.
7. How to test (what a human can verify after this phase)
- Open
/falogged-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. - Enter a phone (demo seeds
0912000000x), request the code. On the OTP screen the masked number reads0912•••…correctly inside the Persian sentence; the countdown ticks in Persian digits on/fa. - 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).
- Spam request-OTP until a 429 → the message renders in error styling on the field.
- 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.
- Log in with a fresh phone →
/select-roleshows 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. - 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. - Logged-out, open
/fa/bookings→ login carries?next=%2Fbookings→ after OTP you land on/fa/bookings. Repeat with?next=https://evil.commanually — you land on your role home. - Sanity: a seeded nurse still lands on
/nurse, an admin on/admin(RoleRouter untouched);?role=nursestill pre-selects the nurse copy and carries into select-role.
8. Hand off & document (close the phase)
- Update
client/CLAUDE.mdProject Structure: the new(public-routes)terms/privacypages, 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.