28 KiB
Frontend Phase 5 — Nurse verification flow (mocked vendors)
Mission: build the trust engine's front end — the staged, platform-owned verification flow a nurse walks through before any of their services can go live. A nurse lands on a status checklist (B3), submits identity (B4: national-ID + card image + liveness selfie), submits professional credentials (B5: نظام پرستاری number + license + education + specialties), and waits on the under-review screen (B6) until an admin decides. Documents upload to the object-storage-backed endpoint with type/size validation and progress; each step shows its own status (and its rejection reason); and a trust badge (verified / unverified / expired) renders on the nurse profile. Verified trust is the entire brand — a nurse is not bookable and cannot publish until verified, and the UI must say so honestly and never advertise a check the platform doesn't actually perform.
Track: frontend · Depends on:
frontend-phase-4-b5.md(catalog browse + nurse service builder) and the b6 verification contract · Unlocks: a bookable verified nurse + the search trust badge consumed infrontend-phase-6-b7.mdBefore you start, read../_shared/agent-operating-rules.md. It is not optional.
1. Context — where this sits
This is the fifth feature slice of the customer/nurse front end and the nurse side's gating step.
By f4 a nurse can build service variants, but those variants are inert — they cannot surface in
search or be booked until nurse_profiles.is_verified is true. This phase builds the screens that flip
that switch: the data-driven verification pipeline (six step types, all vendor calls mocked server-side
behind DI seams in b6) rendered as a nurse-facing checklist, the two submission forms, the waiting state,
the document uploader, and the public trust badge. When this lands, a nurse can go from "registered" to
"verified and publishable", which is the prerequisite for everything downstream — search (f6), booking
(f7), and ultimately payout (f12, gated on the bank-account verification step).
What already exists (do not rebuild) — built by prior phases:
- f0 foundations (
frontend-phase-0.md): the three actor app shells + route groups, the nurse shell ("نمای پرستار"), theservices/{domain}+ TanStack Query caching pattern (keys.tsfactory,apis/clientApi.ts, one-hook-per-file), the contracts→types.tspattern, the money/format utils, and the shared composite components — most importantly the stepper/ progress header and the status chip (verified/pending/…). Reuse both here; do not re-implement a stepper or a status chip. Theverificationi18n namespace was reserved in f0 — fill it. - f1-b2 auth (
frontend-phase-1-b2.md): phone-OTP login, the role router, roles inAuthContext. The nurse arrives here already authenticated with thenurserole. Step 1 of the checklist (شماره موبایل — mobile verified) is already satisfied at login — render it aspassed, don't ask for it again. - f2-b3 onboarding (
frontend-phase-2-b3.md): the nurse profile bootstrap (CreateNurseProfileCommand→ an unverifiednurse_profilesrow,is_verified=false) and the nurse bank-account settings screen (IBAN entry → ownership inquiry). The verification pipeline'sbank_account_verificationstep couples to that bank account — link to the existing bank-account screen for that step; do not rebuild the IBAN form here. - f3-b4 addresses/geo (
frontend-phase-3-b4.md): the nurse coverage-area editor and the cascading geo dropdowns (not used here, but the nurse shell nav points to them). - f4-b5 catalog (
frontend-phase-4-b5.md): the nurse "add a service" builder (B7) andservices/catalog. The "انتشار پروفایل / go live" action that f4 stubs must be gated on verification by this phase — wire the blocked-until-verified state into the publish CTA.
Honesty constraint (load-bearing, from the product doc and GTM notes): vetting is platform-owned and performed at the authoritative source. Never word the UI to advertise a check that isn't performed. The MoH/INO license and criminal-record steps are manual admin review of an uploaded document at launch — copy must say "در حال بررسی" (under review), not "تاییدشده توسط نظام پرستاری" until an admin actually passes it. The civil-registry/Shahkar/liveness checks are real (vendor-mocked) — those may say "استعلام خودکار".
2. Required reading (do this first)
../_shared/agent-operating-rules.mdand../_shared/frontend-conventions-checklist.md— how you work, the gate, the contract/handoff lanes, the mock-then-swap rule (§6).../../../client/CLAUDE.md— the engineering contract (RSC/client boundary, layouts, theservices/{domain}shape, i18n, theme, cookies, the fetch services). Non-negotiable.- Invoke the
frontend-designerskill before any visual work — it is the design/brand contract (palette, tokens, typography, theApp*library, layout shells, the hard UI rules). Every screen, chip, uploader, and badge in this phase goes through it. The wireframe's status legend is green = automatic/verified, amber = pending, grey = manual/next, terracotta = financial — encode those as token-driven chip variants. - Product — the source of truth for the rules:
../../../product/business/02-nurse-verification.md— the six steps, what each verifies, why it's manual vs automated, the structured-credential-registry rationale, continuous re-verification, and the "never advertise a check you don't perform" rule.../../../product/wireframes/index.html— Section B, screens B3–B6 are the visual baseline you implement: B3 status meter "۲ از ۵" + stepped checklist with status badges; B4 identity (کد ملی field, upload national-ID card, liveness selfie, "استعلام خودکار از ثبت احوال" note); B5 professional credentials (شماره نظام پرستاری, license upload, education cert shown uploaded ✓, specialty chips سالمندان/ICU/+افزودن); B6 "در حال بررسی" (24–48h, mini-checklist). Also note B7's "انتشار پروفایل" — the publish gate this phase enforces, and C2/C3's "✓ تاییدشده" badge this phase's trust-badge component feeds.
- The contract you consume (the authoritative server shapes):
../../contracts/domains/verification.md— written by backend-phase-6. The exact request/response shapes, routes, status enums, and the per-step status codes. Do not guess shapes — derivetypes.tsfrom this doc + the publishedswagger.json. If a shape you need is missing, append a request to../../shared-working-context/frontend/requests/for-backend.mdand mock behind theservices/verificationseam meanwhile (operating-rules §6).../../contracts/conventions/api-conventions.mdandmoney-and-types.md— the envelope (OperationResult→ApiResult, already unwrapped byclientFetch),snake_caseroutes/properties, pagination, enums as stable string codes (mirror them as string-literal unions; labels are i18n keys, never derived from the code), UTC timestamps with Shamsi display client-side (credential issue/expiry dates).
- Code to mirror:
- The
services/catalogandservices/onboardingdomains from f4/f2 — the exacttypes.ts/keys.ts/apis/clientApi.ts/hooks/use*.ts/index.tslayout yourservices/verificationcopies. - The shared stepper/progress header and status chip from f0 (
src/components/…) — extend, don't fork. The nurse-profile bank-account screen from f2 — you deep-link into it for the bank step. - The existing document/image handling, if any, in
App*(AppImage); theclientFetchmultipart path.
- The
3. Scope — build this
A new services/verification domain, the nurse verification route subtree under the nurse shell,
a reusable document-upload component, a trust-badge component, and the publish-gate wiring. Build
RTL-first, both locales, query-cached, minimal re-renders.
3.1 services/verification domain (the data layer)
Copy the f0/f4 service shape exactly. Types come from
../../contracts/domains/verification.md — do not invent.
types.ts— string-literal unions mirroring the contract enums: the aggregate status (not_started|pending|in_review|approved|rejected|suspended), the per-step status (not_started|pending|in_review|passed|rejected), the stepcodes (identity_kyc·shahkar_match·moh_competency_license·ino_membership·criminal_record·bank_account_verification),verification_method(manual|portal|api), and the badge state (verified|unverified|expired). Plus the DTOs the contract returns:VerificationStatus(aggregate + orderedsteps[]each withcode,status,is_automated,rejection_reason?,expires_at?),VerificationStep,VerificationDocument(storage key, file name, uploaded-at — metadata only, never bytes),NurseCredential(type, masked number, holder name, issuing authority,issued_at/expires_at).keys.ts— a query-key factory:verificationKeys.status(),.documents(stepCode),.badge(nurseId). DeliberatestaleTime(status is moderately fresh — e.g. 30s — because submitting a step changes it; the badge is longer-lived).apis/clientApi.tswrappingclientFetch— one function per contract endpoint (see §3.2). The document upload uses the multipart path against theIObjectStorage-backed endpoint.hooks/— one hook per file:useVerificationStatus(query),useStartVerification,useSubmitIdentity,useSubmitCredentials(or per-credential submit if the contract splits MoH/INO/criminal),useUploadVerificationDocument(mutation with progress),useNurseTrustBadge(query). Every mutation invalidatesverificationKeys.status()(and the badge where relevant) so the checklist re-renders from cache without a manual refetch. Don't toast 401/403/5xx (the fetch layer does) — only domain 4xx (e.g. "ownership mismatch", "shared-SIM", "national-ID format").
3.2 Endpoints consumed (per the b6 contract — confirm exact routes/shapes there)
Wire each via clientApi; the names below are the expected commands/queries from the digest — bind to
whatever the contract publishes:
GET .../nurse/verification→GetVerificationStatusQuery— the aggregate + per-step list driving B3/B6.POST .../nurse/verification/start→StartNurseVerificationCommand— seeds the steps (call from the B3 "start / continue" CTA when status isnot_started).POST .../nurse/verification/identity→RunIdentityKycCommand— national-ID + card image + liveness selfie; drives B4. (Server also runsshahkar_matchoff the bound national-ID — surface both steps.)POST .../nurse/verification/moh-license(+.../ino,.../criminal-recordif split) → the credential-submit commands behind B5.POST .../nurse/verification/document(or a presigned-URL flow) → theIObjectStorage-backed upload used by every document step.- The bank step (
bank_account_verification) is submitted on the existing f2 bank-account screen — this phase only renders its status in the checklist and deep-links to that screen. GET .../nurse/{id}/badge(public) →GetVerifiedBadgeQuery— feeds the trust badge.
3.3 Screens (under the nurse shell (private-routes) nurse subtree)
Invoke frontend-designer for each. RTL-first; the screens live under the nurse route group from f0.
-
B3 · وضعیت احراز هویت — status checklist (the hub).
- Overall progress meter "X از Y" (X = passed required steps, Y = total required) — reuse the f0 stepper/progress header.
- An ordered, stepped checklist, one row per step from
steps[], each with a status chip (reuse the f0 status chip) in the five per-step states:not_started/locked-next (grey "بعدی"),pending(amber "در انتظار"),in_review(amber "در حال بررسی"),passed(green "تاییدشده"),rejected(red "رد شد" + the reason). Render step 1 (mobile) aspassedfrom auth state. - A "what's blocking go-live" summary + a single continue CTA ("ادامه مرحله N") that routes to
the next actionable step. When
not_started, the CTA callsuseStartVerificationfirst. - States: loading skeleton, error, and the terminal
approvedstate (all passed → "احراز هویت تکمیل شد، میتوانید پروفایل را منتشر کنید" with a link to publish).
-
B4 · تایید هویت — identity submit.
- کد ملی (national-ID) field with format validation (10-digit, checksum); upload national-ID card image (camera/gallery via the document uploader §3.4); liveness selfie capture (camera; handle permission-denied / retry / vendor-timeout states honestly).
- The honest auto note "استعلام خودکار از ثبت احوال" (auto civil-registry query) — this check is performed (vendor-mocked), so the copy is allowed.
- Submit →
useSubmitIdentity; on success theidentity_kyc(and server-runshahkar_match) steps move topending/in_review, the cache invalidates, and the user returns to B3. Surface the shared-SIM Shahkar failure as a clear, non-accusatory message, and the national-ID-mismatch failure on its step.
-
B5 · مدارک حرفهای — professional credentials.
- شماره نظام پرستاری (INO number) field; upload پروانه/کارت نظام پرستاری (the MoH competency license — the single most important credential) via the uploader; education certificate upload (shown with the uploaded-✓ state when present); specialty chips (multi-select: سالمندان, ICU, + افزودن — an add-your-own chip input).
- Structured fields the registry needs where the contract asks for them (license number, issuing authority, holder name as printed, issue/expiry date — Shamsi date picker, stored UTC).
- Submit →
useSubmitCredentials; the credential steps move toin_review(manual admin review). Copy must reflect manual review, not an automated authority confirmation.
-
B6 · در حال بررسی — under review.
- The waiting state: "مدارک شما در حال بررسی است" + the 24–48h expectation + a mini-checklist summarizing which steps are passed vs in-review (identity verified / professional docs in review / bank account pending). Reuses the same per-step chips as B3, condensed. CTA "مشاهده وضعیت" → B3.
- This is the post-submission resting screen; B3 is the canonical source — B6 is a focused view of the
same
VerificationStatus, not a second fetch with different state.
-
Trust badge on the nurse profile. Render the verified / unverified / expired badge (the "✓ تاییدشده" mark) on the nurse's own profile (f2) and export a small
<TrustBadge status=…/>shared component so search results (C2) and the public nurse profile (C3) in f6 reuse it. Source it fromGetVerifiedBadgeQuery.expired(a required credential lapsed) shows distinctly fromunverified. -
Publish gate. Wire the f4 "انتشار پروفایل / go-live" action: when the aggregate status is not
approved, the publish CTA is disabled with a blocked-until-verified explanation ("برای انتشار، احراز هویت را کامل کنید") that links to B3. This is the front-end enforcement of "not bookable until verified" — the server enforces it too; the UI must not let a nurse believe they're live when they're not.
3.4 Document uploader (shared composite component)
Build a reusable <DocumentUpload> at the shared level (src/components/…, co-located *.test.tsx)
composed from MUI/App* primitives — used by every document step (national-ID card, license, education,
criminal record):
- Client-side validation before upload: file type (jpg/png/pdf per the contract) and size cap; reject with a clear field error.
- Upload states: idle → selecting → uploading (progress %) → processing/hashing → success (thumbnail/✓ + file name) → error (retry). Wire progress from the upload mutation.
- Re-upload on reject: when a step is
rejected, the uploader shows the prior file's status and a re-upload affordance. - Returns the server's stored document metadata (storage key, name) — never holds or displays raw bytes beyond a local preview.
3.5 i18n namespace verification (both locales)
Fill the f0-reserved verification namespace in both messages/en.json and messages/fa.json, in
sync: step labels and per-step status labels (one key per code and per status — never derive a label
from the enum string), the B3/B4/B5/B6 screen copy, the uploader states, the honesty-sensitive copy
(manual-review vs auto-query), the blocked-until-verified message, the trust-badge labels, and the
shared-SIM / mismatch / format error messages. fa is default and RTL.
Deferred (do not build here)
- The admin verification review queue (pass/reject a manual step, the doc viewer, credential-entry
form) → (DEFERRED to
frontend-phase-15-b15.md), the admin backoffice. This phase only triggers the mock approval to observe the state change (see §7); it does not build the admin UI. - Credential renewal/expiry prompts beyond rendering the
expiredbadge state → the nurse renewal prompt UI is (DEFERRED); the server'sCredentialExpiryScannerJob(b6) raises the alert. - Video interview (B6 mentions it) — not a built step at MVP; reflect it only as a grey "next" row if the contract returns it; otherwise omit. Do not invent an interview-scheduling flow (DEFERRED).
- Customer national-ID KYC — out of scope (the product gates only nurses) (DEFERRED).
4. Mocks & seams in this phase
This phase introduces no new front-end seam of its own beyond the standard services/{domain} data
seam — the verification vendors (identity KYC, Shahkar, MoH/INO license, criminal record, IBAN ownership,
object storage) are mocked server-side behind DI seams owned by backend-phase-6 (IIdentityKycProvider,
IShahkarVerifier, ICredentialVerifier, IBankAccountOwnershipVerifier, IObjectStorage) — the front
end consumes them only through the b6 contract, exactly as if they were real. Reuse the services/{domain}
mock-clientApi pattern from f0: if the b6 contract or a specific shape isn't published when you start,
build a mock clientApi behind the services/verification seam that returns realistic, contract-shaped
VerificationStatus (e.g. identity → in_review, then flippable to passed), record it in your phase
report + the mock registry, and append the gap
to for-backend.md. The swap to the real
endpoint must be implementation-only (same hook signatures, same queryKeys) — no call-site changes.
5. Critical rules you must not get wrong
- A nurse is NOT bookable and cannot publish until verified. When the aggregate status is not
approved, the publish/go-live CTA is disabled with the blocked-until-complete explanation, and every service variant stays inert. This front-end gate mirrors the server's guardedis_verifiedflip — never let the UI imply a nurse is live before verification completes. - Honest copy — never advertise a check that isn't performed. Manual steps (MoH/INO license, criminal
record) say "در حال بررسی / آپلود شد"; only the genuinely-performed automated checks (identity liveness,
civil-registry, Shahkar) may say "استعلام خودکار". A step is "تاییدشده" only when its status is
passed. This is a product constraint, not a style preference. - Per-step rejected-with-reason. A
rejectedstep renders itsrejection_reasonand a clear re-submit/re-upload path — never a dead end. The shared-SIM Shahkar failure is an explicit, handled, non-accusatory state, not a generic error. - The data is data-driven — render
steps[]from the contract, don't hardcode the six steps. The pipeline is rows server-side; the UI iterates the returned ordered list and maps eachcode/statusto a label + chip. A new step type appearing in the response must render without a code change. - Reuse the shared stepper and status chip from f0 — do not re-implement them. Concrete MUI primitives
stay MUI; the new shared pieces (
<DocumentUpload>,<TrustBadge>) live at the shared level with tests. - Caching is a feature. B3 and B6 read the same
VerificationStatusquery — one cached source, two views; every submit/upload mutation invalidates the status (and badge) so the checklist updates from cache, never an extra manual refetch. Minimise re-renders (stable refs,selectfor slices). - RSC/client boundary + RTL + both locales + tokens. No
next/headers/next-intl/serverin client components; design RTL-first (fadefault); every string a key in both locale files; colours fromtokens.css(the chip variants map to the wireframe's green/amber/grey/red legend); MUI v9 API only. - Document bytes are never shown from the API — uploads return metadata only; a local preview before upload is fine, but the rendered "uploaded" state is driven by server metadata, not retained bytes. The national-ID and credential numbers are sensitive — show masked values where the contract masks them.
- Dates: credential issue/expiry are UTC on the wire, Shamsi for display (use the f0 date util).
6. Definition of Done
The shared definition-of-done.md, plus:
services/verificationexists with the f0/f4 shape; types derived fromverification.md(not guessed); mutations invalidate the status/badge query; one hook per file; no rawfetch().- B3 (status checklist), B4 (identity), B5 (credentials), B6 (under review) render under the nurse shell, RTL, both locales in sync, reusing the f0 stepper + status chip.
<DocumentUpload>and<TrustBadge>are shared components with co-located*.test.tsx; uploader enforces type/size, shows progress, and supports re-upload on reject.- The publish/go-live CTA from f4 is gated: disabled + blocked-until-verified message until
approved. - Honesty copy verified: manual steps never claim an automated authority check; a step shows
"تاییدشده" only when
passed. - Rejected steps show their reason and a re-submit path; shared-SIM is a clear non-accusatory state.
npm run checkgreen;npm run test:cigreen (shared components added/touched);en.json/fa.jsonin sync.client/CLAUDE.mdProject Structure updated for the new route subtree +services/verification+ the new shared components.
7. How to test (what a human can verify after this phase)
Run npm run dev (and the b6 server, or the mock clientApi if b6 isn't merged). Sign in as a nurse.
- Checklist hub: open the nurse verification screen → B3 shows the "X از Y" meter, step 1 (mobile)
already
passed/green, the restnot_started. The continue CTA routes to the next step (callingstartfirst whennot_started). - Submit identity: B4 → enter a valid کد ملی, upload a national-ID card image (watch the
uploader's progress → success), capture a liveness selfie, submit → the
identity_kycandshahkar_matchsteps move topending/in_reviewon B3 without a manual refresh (cache invalidation). Try an invalid national-ID → field error; trigger the mock shared-SIM number → a clear non-accusatory Shahkar message. - Submit credentials: B5 → enter the نظام پرستاری number, upload the license + education cert, add
specialty chips, submit → the credential steps move to
in_review; the UI lands on B6 showing "در حال بررسی", the 24–48h note, and the mini-checklist. - Approval flips verified: trigger the b6 mock admin approval (the mock provider/test endpoint, or set
the mock
clientApito returnapproved) → re-open B3: aggregate isapproved, all required stepspassed, the trust badge shows verified on the nurse profile, and the publish CTA is now enabled (was blocked-until-verified before). - Rejected step: make a step return
rejected(mock) → its row shows the rejection reason and a working re-upload/re-submit path; the publish CTA stays blocked. - Quality:
npm run checkandnpm run test:cipass; toggle locale → RTL/strings flip correctly; dark mode renders; React Query Devtools shows oneverification.statusquery feeding both B3 and B6 and invalidating on each mutation.
8. Hand off & document (close the phase)
- Docs: update the Project Structure tree in
../../../client/CLAUDE.mdfor the nurse verification route subtree,services/verification, and the new shared components (<DocumentUpload>,<TrustBadge>); add a one-line note that the trust badge is the reusable component f6 consumes. If you discover a verification rule the product docs don't capture, record it in../../../product/business/02-nurse-verification.md(don't invent — record decisions). - Contracts: consume
../../contracts/domains/verification.md(frontend produces none). Any missing/ambiguous shape (e.g. whether credential submit is one endpoint or split, the exact document-upload flow, the badge payload) → append to../../shared-working-context/frontend/requests/for-backend.md; do not edit backend files. - Handoff & report: append your phase summary to
../../shared-working-context/frontend/STATUS.md; write../../shared-working-context/reports/frontend-phase-5-report.md— what was built, what is now testable and exactly how (the §7 steps), what is mocked client-side (theservices/verificationmockclientApi, if used) and how it swaps to the real b6 endpoint, the contract consumed + gaps filed, and the follow-up that f6 reuses the<TrustBadge>. Update the mock registry for any client-side mock. - Memory: save a
projectmemory note for the non-obvious decisions — the data-driven step rendering, the single-status-query-two-views (B3/B6) caching choice, the reusable<TrustBadge>/<DocumentUpload>seams, and the publish-gate wiring — with a one-line pointer inMEMORY.md.