# 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`](./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 in [`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md) > **Before you start, read [`../_shared/agent-operating-rules.md`](../_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`](./frontend-phase-0.md)): the three actor app shells + route groups, the **nurse shell** ("نمای پرستار"), the `services/{domain}` + TanStack Query caching pattern (`keys.ts` factory, `apis/clientApi.ts`, one-hook-per-file), the contracts→`types.ts` pattern, 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.** The `verification` i18n namespace was reserved in f0 — fill it. - **f1-b2 auth** ([`frontend-phase-1-b2.md`](./frontend-phase-1-b2.md)): phone-OTP login, the role router, roles in `AuthContext`. The nurse arrives here already authenticated with the `nurse` role. **Step 1 of the checklist (شماره موبایل — mobile verified) is already satisfied at login** — render it as `passed`, don't ask for it again. - **f2-b3 onboarding** ([`frontend-phase-2-b3.md`](./frontend-phase-2-b3.md)): the nurse profile bootstrap (`CreateNurseProfileCommand` → an unverified `nurse_profiles` row, `is_verified=false`) and the nurse **bank-account settings** screen (IBAN entry → ownership inquiry). The verification pipeline's `bank_account_verification` step 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`](./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`](./frontend-phase-4-b5.md)): the nurse "add a service" builder (B7) and `services/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.md`](../_shared/agent-operating-rules.md) and [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) — how you work, the gate, the contract/handoff lanes, the mock-then-swap rule (§6). - [`../../../client/CLAUDE.md`](../../../client/CLAUDE.md) — the engineering contract (RSC/client boundary, layouts, the `services/{domain}` shape, i18n, theme, cookies, the fetch services). Non-negotiable. - **Invoke the `frontend-designer` skill** before any visual work — it is the design/brand contract (palette, tokens, typography, the `App*` 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`](../../../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`](../../../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`](../../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** — derive `types.ts` from this doc + the published `swagger.json`. If a shape you need is missing, append a request to [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md) and mock behind the `services/verification` seam meanwhile (operating-rules §6). - [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) and [`money-and-types.md`](../../contracts/conventions/money-and-types.md) — the envelope (`OperationResult` → `ApiResult`, already unwrapped by `clientFetch`), `snake_case` routes/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/catalog` and `services/onboarding` domains from f4/f2 — the exact `types.ts`/`keys.ts`/ `apis/clientApi.ts`/`hooks/use*.ts`/`index.ts` layout your `services/verification` copies. - 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`); the `clientFetch` multipart path. ## 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`](../../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 **step `code`s** (`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 + ordered `steps[]` each with `code`, `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)`. Deliberate `staleTime` (status is moderately fresh — e.g. 30s — because submitting a step changes it; the badge is longer-lived). - **`apis/clientApi.ts`** wrapping `clientFetch` — one function per contract endpoint (see §3.2). The document upload uses the multipart path against the `IObjectStorage`-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 invalidates `verificationKeys.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 is `not_started`). - `POST .../nurse/verification/identity` → `RunIdentityKycCommand` — national-ID + card image + liveness selfie; drives B4. (Server also runs `shahkar_match` off the bound national-ID — surface both steps.) - `POST .../nurse/verification/moh-license` (+ `.../ino`, `.../criminal-record` if split) → the credential-submit commands behind B5. - `POST .../nurse/verification/document` (or a presigned-URL flow) → the `IObjectStorage`-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) as `passed`** from 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 calls `useStartVerification` first. - States: loading skeleton, error, and the terminal **`approved` state** (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 the `identity_kyc` (and server-run `shahkar_match`) steps move to `pending`/`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 to `in_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 `` shared component so **search results (C2) and the public nurse profile (C3) in f6 reuse it**. Source it from `GetVerifiedBadgeQuery`. `expired` (a required credential lapsed) shows distinctly from `unverified`. - **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 **``** 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`](./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 `expired` badge state → the nurse renewal prompt UI is **(DEFERRED)**; the server's `CredentialExpiryScannerJob` (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](../../shared-working-context/reports/mocks-registry.md), and **append the gap to [`for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)**. The swap to the real endpoint must be implementation-only (same hook signatures, same `queryKey`s) — 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 guarded `is_verified` flip — 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 `rejected` step renders its `rejection_reason` and 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 each `code`/`status` to 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 (``, ``) live at the shared level with tests. - **Caching is a feature.** B3 and B6 read the *same* `VerificationStatus` query — 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, `select` for slices). - **RSC/client boundary + RTL + both locales + tokens.** No `next/headers`/`next-intl/server` in client components; design RTL-first (`fa` default); every string a key in both locale files; colours from `tokens.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](../_shared/definition-of-done.md), plus: - [ ] `services/verification` exists with the f0/f4 shape; types derived from [`verification.md`](../../contracts/domains/verification.md) (not guessed); mutations invalidate the status/badge query; one hook per file; no raw `fetch()`. - [ ] 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. - [ ] `` and `` 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 check` green; `npm run test:ci` green (shared components added/touched); `en.json`/`fa.json` in sync. - [ ] `client/CLAUDE.md` *Project 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 rest `not_started`. The continue CTA routes to the next step (calling `start` first when `not_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_kyc` and `shahkar_match` steps move to **`pending`/`in_review`** on 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 `clientApi` to return `approved`) → re-open B3: aggregate is **`approved`**, all required steps `passed`, 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 check` and `npm run test:ci` pass; toggle locale → RTL/strings flip correctly; dark mode renders; React Query Devtools shows one `verification.status` query 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.md`](../../../client/CLAUDE.md) for the nurse verification route subtree, `services/verification`, and the new shared components (``, ``); 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`](../../../product/business/02-nurse-verification.md) (don't invent — record decisions). - **Contracts:** **consume** [`../../contracts/domains/verification.md`](../../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`](../../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`](../../shared-working-context/frontend/STATUS.md); write [`../../shared-working-context/reports/frontend-phase-5-report.md`](../../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 (the `services/verification` mock `clientApi`, if used) and how it swaps to the real b6 endpoint, the contract consumed + gaps filed, and the follow-up that f6 reuses the ``. Update the [mock registry](../../shared-working-context/reports/mocks-registry.md) for any client-side mock. - **Memory:** save a `project` memory note for the non-obvious decisions — the data-driven step rendering, the single-status-query-two-views (B3/B6) caching choice, the reusable ``/`` seams, and the publish-gate wiring — with a one-line pointer in `MEMORY.md`.