# UI Phase 6 — Checkout & money > **Mission:** money UI is where trust is won or lost — and today the checkout total is a `subtitle2` row > buried inside a breakdown card, the payment "confirmation" has no reference code a user could quote in a > dispute, the top-level «کیف‌پول» tab is permanently empty for every card payer, and BNPL plans hide the > numbers people decide with. Restructure checkout around "who am I paying for / how much", turn the > confirmation into a real screenshot-worthy receipt, fill the wallet into the customer money hub, and make > the BNPL comparison honest — without touching one rial of the money pipeline itself. > > **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) · > [Phase 5](ui-phase-5-booking-lifecycle.md) recommended · **Unlocks:** the money moment finally earns the trust the ledger deserves > **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the `frontend-designer` skill — both are mandatory.** ## 1. Context — where this sits Balinyaar's pitch is "pay on-platform because your money sits in escrow until the visit is confirmed." The checkout → confirmation → wallet → invoice chain is where a family decides whether to believe that. The plumbing is excellent; the presentation treats the payment moment like any other form. Diagnosed current state (all verified in code): 1. **The total is visually a footnote.** C6's page h1 is a plain `variant="h6"` (`…/bookings/checkout/page.tsx:159`); the total — the most important number on screen — is a `subtitle2` row inside `PriceBreakdown` (`components/PriceBreakdown/PriceBreakdown.tsx:62-63`); the pay CTA is the last element of a scroll column (`page.tsx:202-225`), not sticky, not paired with the amount. 2. **A live race between two payment paths.** During pay-initiate, `busy` (`page.tsx:148`) disables only the card CTA (`:207`); the «پرداخت اقساطی» button (`:214-224`) stays tappable mid-redirect. 3. **The confirmation is not a receipt.** `checkout/confirmation/page.tsx:51-113` renders only total + variant + nurse name — no کد پیگیری, date-time, method, or booking number — and the amount panel is `{summary ? … : null}` with no loading/error fallback: a failed fetch silently erases the paid amount. The wire can't serve a receipt yet (`PaymentOutcomeDto`, `services/payment/types.ts:104-110`). 4. **«کیف‌پول» is an empty promise.** `wallet/page.tsx` renders only `WalletInstallments` — provider-reported BNPL plans. For card payers (the default; BNPL is mock-gated) the tab shows «طرح اقساط فعالی ندارید» forever, wasting 1 of 5 bottom-nav slots — and it self-constrains to `maxWidth: 560` (`WalletInstallments.tsx:24`) while checkout/confirmation/invoice run at the shell's 800. 5. **BNPL hides the deciding numbers.** `BnplPlanCard.tsx:77-98` renders the down payment as a percent + `LinearProgress` bar — a static fact styled as a loading indicator — with no Toman figure and no total repayment cost; `PlanStep.tsx`'s «مبلغ کل» header silently swaps on plan tap (`plans.find(…) ?? plans[0]`); providers are picked from two-letter text glyphs; `EligibilityStep.tsx:163` runs a multi-second credit check behind a bare disabled button; `bnpl/return/page.tsx:100-104`'s invalid-link CTA says «پرداخت با کارت» but navigates to the bookings list. 6. **Spinner-stacked waits, forked terminal cards.** `checkout/return/page.tsx:139-147` shows title + `CircularProgress` + a `pending` chip — three redundant signals at the flow's most anxious moment — and its `StateCard` / C6's `MessageCard` are copy-pasted four times across the card & BNPL flows. 7. **Currency-unit ambiguity.** Breakdown rows and installment amounts render bare grouped numbers; only the total carries «تومان» (`PriceBreakdown.tsx:53` vs `:63`, `InstallmentScheduleRow.tsx:57`) — the classic Toman/Rial second-guess, on wire amounts that are Rials displayed as Toman. **What already exists (do not rebuild):** - The money pipeline: `utils/money.ts` (BigInt, `formatIrrToToman`, `parseIrr`), IRR digit-strings on the wire, Toman display-only, and `PriceBreakdown`'s dev reconciliation guard (`PriceBreakdown.tsx:35-42`). - The full C6 → return → confirmation flow and the 4-step BNPL wizard: idempotency-key-per-attempt, benign-409 convergence, bounded outcome polling, every state (skeleton/error/empty/expired) covered. - `EscrowNotice` (product-mandated verbatim copy), `CountdownTimer` (server-frozen, LTR clock), `RefundStatusCard` + `RefundEtaBanner`, `StatusChip`, `TrustBadge`, `PaymentStatusBadge`; the invoice's print mechanics — visibility-scoped print area, `insetInlineStart` anchoring, dark→light token flip so dark-mode users print on paper colors (`invoice/page.tsx:98-113`). - Phase 0's theme/`AppIcon` work, phase 1's primitives (``, PageHeader, state views, StatusTimeline), phase 2's shells. Consume them; extend a foundation file minimally (noted in your report) — never fork a copy. ## 2. Required reading (do this first) - [audit/checkout-money.md](audit/checkout-money.md) — the 18-problem inventory with file/line evidence, the 8 opportunities this scope is drawn from, and the keep-list §5 repeats. - [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — the design contract (two-layer tokens, `App*` wrappers, icon registry). Invoke it. - Code (paths abbreviate `client/src/app/[locale]/(private-routes)/(customer)` as `…`): `…/bookings/checkout/page.tsx` (+ `return/`, `confirmation/`), `…/bookings/checkout/bnpl/page.tsx` (+ the four step files, `return/`, `gateway/`), `…/wallet/` (both files), `…/bookings/[id]/invoice/page.tsx`; components `PriceBreakdown`, `BnplPlanCard`, `InstallmentScheduleRow`, `EscrowNotice`, `RefundStatusCard`; services `payment` (types + hooks + both `PaymentApi` impls), `bnpl`, `refunds`; `components/config.ts` (`CONTENT_MAX_WIDTH`) and `src/layout/config.ts` (bottom-bar dimensions, for the sticky bar). - Product truth: [escrow-ledger.md](../../../product/payments/escrow-ledger.md) (what escrow actually promises — the explainer must not overclaim), [bnpl-landscape.md](../../../product/payments/bnpl-landscape.md) (provider-financed; the agreement is customer ↔ provider), [iranian-payment-reality.md](../../../product/payments/iranian-payment-reality.md) (gateway redirects, receipt culture, Toman/Rial). Plus `client/CLAUDE.md` "Golden rules" + Project Structure. ## 3. Scope — build this ### 3.1 C6 checkout: real hierarchy + sticky pay bar - **Prominent total.** Lift the total out of the breakdown into an unmissable figure near the top (an `h4`-weight `` with «تومان»); replace the `h6`-as-h1 with the phase-1 PageHeader treatment. `PriceBreakdown` keeps its total row — the page-level figure is the same served `totalIrr`, never recomputed. - **Sticky pay bar.** A page-level bar pinned above the customer bottom nav: total + pay CTA, always co-located on mobile. Respect `env(safe-area-inset-bottom)` and the bottom-bar height from `src/layout/config.ts` — do **not** edit `layout/` (phase 2 owns it); this is page composition. - **Identity moment.** Give `EngagementSummary` the nurse's avatar + verified `TrustBadge` so "who am I paying for" is answered at the moment of payment. `CheckoutSummaryDto` serves neither field — file the REQ (§4) and serve them from the payment mock meanwhile. - **Kill the race:** `disabled={busy}` on the BNPL branch button too — both CTAs freeze during initiate. - **Back affordance.** The payable state gets an explicit «بازگشت به درخواست» text link — a user under a ticking countdown needs an in-page path back to the request, beyond phase 2's header chrome. - **Trust line at the CTA:** lock icon + «پرداخت امن از طریق درگاه بانکی» caption under the pay bar so the gateway redirect is expected, not alarming. Real gateway/Shaparak logos are (DEFERRED — until licensed assets exist; never fake a bank's mark). - **Un-bake the arrows.** `payment.cta_pay` embeds «←»/«→» in the translated string (fa/en.json:577) — move the arrow to an `endIcon` slot (register a direction-aware chevron in `AppIcon/config.ts` if phase 0/1 didn't). ### 3.2 Confirmation as a receipt - Rebuild `…/checkout/confirmation/page.tsx` as a screenshot-worthy receipt card (Iranian users screenshot receipts — design for that): **کد پیگیری** in a copyable `dir="ltr"` row with a copy button, Shamsi payment date-time, method («کارت بانکی» / «اقساطی — {provider}»), booking reference, the paid total as a `` hero, and the escrow reassurance via the `EscrowNotice` component — never rewritten copy. - **No silent vanish:** the summary fetch gets real loading (skeleton) and error (retry) states. Add a compact "what happens next" 2-step strip (اطلاع‌رسانی به پرستار ← ویزیت و ثبت ورود) via the phase-1 StatusTimeline idiom, extending trust past the payment. - The wire serves none of کد پیگیری/paidAt/method today — file the REQ (§4), render from the mock now, and degrade gracefully on the real path (hide the row rather than show a fake code). Web-Share/save-image actions are (DEFERRED — the copyable code is the contract). ### 3.3 Wallet as the money hub - Restructure `…/wallet/` into segmented sections (MUI Tabs or a segmented control — one pattern, both locales): **پرداخت‌ها** (payment history: every card/BNPL transaction with amount, Shamsi date, `PaymentStatusBadge`, deep-link to the booking — needs the customer payment-transactions list REQ, §4; build mock-tolerant behind `services/payment`), **اقساط** (the existing `WalletInstallments` content, semantics unchanged — provider-reported, never a Balinyaar ledger), **استردادها** (reuse `RefundStatusCard` entries; `services/refunds` has by-booking/status reads but no "all my refunds" list — REQ if confirmed missing, §4), **رسیدها** (invoice deep-links derived client-side from succeeded payments — no endpoint). Each section keeps four-state discipline with money-hub-specific empty copy («هنوز پرداختی نداشته‌اید» — not the installments placeholder). - **Normalize money-surface width:** drop the wallet's local `maxWidth: 560`; all money surfaces (checkout, confirmation, wallet, invoice) read at one width system — the shell's `CONTENT_MAX_WIDTH`. If a narrower measure is deliberately wanted, make it a named constant in `components/config.ts` applied to **all four**. ### 3.4 BNPL honesty + polish - **Plan cards decide with Toman, not percent.** Each `BnplPlanCard` shows: پیش‌پرداخت (امروز) in Toman, قسط ماهانه, and **مجموع بازپرداخت** with the fee delta vs interest-free explicit («+۴۵۰٬۰۰۰ تومان کارمزد» in terracotta / «بدون کارمزد» neutral). Replace the `LinearProgress` down-payment bar with a plain labelled amount row — a static fact must not look like loading. All figures are **served** by the plan DTO; if one isn't, REQ it — never compute percent × total client-side. - **PlanStep's swapping total explained.** The «مبلغ کل» header names the plan it reflects («مبلغ کل با طرح {plan}») and shows the fee delta when it changes — no silently morphing number. - **Provider logos strategy (decide and document):** build a small `BnplProviderLogo` registry component — `providerCode` → bundled SVG when an asset exists, falling back to a *designed* neutral chip (full provider name + tinted monogram, not the current two-letter glyph). Asset-tolerant: real logos drop in without touching call-sites. - **Eligibility gets feedback.** During the credit check the button swaps to «در حال استعلام اعتبار…» with a spinner (mirror C6's `state_initiating` pattern); the prefilled mobile field becomes `readOnly` presentation (normal contrast), not `disabled`. - **Fix the mislabelled CTA** on `bnpl/return`'s invalid-link state: it navigates to the bookings list, so label it «رزروهای من» (reuse C6's `bd_my_bookings` invalid-link pattern) — or route to an actual card checkout if a `request_id` is recoverable. Label and destination must agree. - **Guard the dev harness.** `…/checkout/bnpl/gateway/page.tsx` is a test harness reachable in production — env-gate it with `notFound()` outside development, mirroring the card-gateway harness refinement phase 4 deleted. Also remove the dead `CHECKOUT_GATEWAY` constant in `src/constants/routes.ts:24` (verify unreferenced). ### 3.5 Invoice: fiscal grade - Extend `…/bookings/[id]/invoice/page.tsx` toward a document a family can file: buyer name, service description + visit date(s) (composable client-side from the booking detail read — a UI join, not money math), booking + transaction references, payment method, and a seller fiscal-identity block (legal name, economic code, address). `InvoiceDto` serves none of the fiscal fields — REQ (§4); render what the client can compose now, reserve labelled slots for the rest. - Add an A4 print stylesheet: `@page` margins, a document footer (invoice number + issue date + مودیان reference when present), type sized for paper. **Keep** the existing print mechanics untouched. - Delete the stale comment at `invoice/page.tsx:148-149` (it claims fa `common.brand` reads «بلینیار»; the catalog actually has «بالین یار»). The brand-spelling unification itself is (DEFERRED → [phase 12](ui-phase-12-copy-motion-and-polish.md), which owns the catalog sweep). ### 3.6 Designed wait states + one terminal-state component - Replace `checkout/return`'s pending spinner+chip+title triple with a staged 2-node progress: «بازگشت از درگاه ✓» → «در انتظار تایید بانک» (active, calm animated indicator) + expected-duration copy («معمولاً کمتر از یک دقیقه طول می‌کشد»). Keep the manual «بررسی دوباره» escape hatch and the bounded poll. - Extract the four copy-pasted terminal cards (`MessageCard` in `checkout/page.tsx:255-286` + `bnpl/page.tsx`, `StateCard` in `checkout/return/page.tsx:151+` + `bnpl/return/page.tsx`) into **one** shared `src/components/PaymentStateCard/` (co-located test, barrel export) covering icon/tone/title/body/actions — the card and BNPL flows can no longer drift. ### 3.7 Money-display sweep - Adopt the phase-1 `` primitive on every money row in this area — `PriceBreakdown` rows **and** total, confirmation, `MethodStep`, `PlanStep`, `EligibilityStep`, `WalletInstallments`, `BnplPlanCard`, `InstallmentScheduleRow` — so **every** amount carries «تومان» and identical typography. Bare grouped numbers on a Rial-wire/Toman-display product are the exact ambiguity users second-guess. If `` lacks a needed variant (e.g. strike-through for fee comparison), extend the phase-1 component minimally + its test, and note it in your report. - An expandable «چطور کار می‌کند؟» escrow explainer **around** `EscrowNotice` — a 3-step visual (پرداخت ← امانت نزد بالین‌یار ← آزادسازی پس از تایید پایان ویزیت) plus the cancellation/refund implication, available from checkout and confirmation. The mandated `EscrowNotice` sentence is **untouchable** — the explainer wraps it, never edits it. Ground the steps in [escrow-ledger.md](../../../product/payments/escrow-ledger.md); never promise timing the ledger doesn't guarantee. ## 4. Mocks & seams in this phase **No new mocks or seams.** Everything stays behind the existing `services/payment`, `services/bnpl`, and `services/refunds` seams (`USE_PAYMENT_MOCK` / `USE_BNPL_MOCK` unchanged); the UI is mock-tolerant — real-path gaps degrade gracefully, never faked data. Backend gaps become REQ entries appended to [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md). REQ-001…038 are taken and parallel UI phases are also filing — **check the tracker at execution time and take the next free numbers.** File (verify each gap against the current types first): 1. **Checkout/receipt enrichment** — `nurseAvatarUrl` + `nurseVerified` on `CheckoutSummaryDto` (§3.1); tracking/reference code, `paidAt`, and payment method on the payment outcome or a confirm-read for the §3.2 receipt (`PaymentOutcomeDto` today: `types.ts:104-110`; extends REQ-016/017). 2. **Customer payment-transactions list** — card+BNPL transactions (amount, status, date, bookingId) for the wallet «پرداخت‌ها» section (§3.3); receipts derive from it client-side. 3. **Customer refunds list** — an "all my refunds" read for «استردادها» (§3.3) if `services/refunds` truly has only by-booking/status reads (extends REQ-021). 4. **Invoice fiscal fields** — buyer name, payment method + transaction reference, seller fiscal identity (legal name, economic code, address) on `InvoiceDto` (§3.5; extends REQ-018). ## 5. Critical rules you must not get wrong - **`EscrowNotice` copy is product-mandated — never edit it.** Wrap and explain around it; verbatim, one shared component. - **Money values are served IRR digit-strings, formatted via the BigInt util. The UI never computes a figure** — no percent × total, no client-side VAT, no float anywhere on the money path. `PriceBreakdown`'s dev reconciliation guard stays. - **Idempotency-key-per-attempt and benign-409 convergence stay exactly as implemented** (`page.tsx:54-57`, `:137-143`) — a 409 on initiate routes to the outcome read, never a toast. - **`CountdownTimer` semantics:** server-frozen deadline, LTR-forced tabular clock — restyle, never recompute. - **BNPL honesty architecture stays:** D5 installments are provider-reported (never a Balinyaar ledger), ownership notes at point-of-choice and contract step, consent checkboxes gate credit check and contract, every decline path offers the card fallback. `RefundEtaBanner`'s honest 7–10-day BNPL window is untouched. - **Terracotta is the single money accent** — pay CTA, BNPL selection, outstanding balance, breakdown total. Never as body-text color; teal/neutral carries everything else. **Invoice print mechanics + dark→light print flip are do-not-regress** (`invoice/page.tsx:98-113,128-136`). - Design-contract non-negotiables that bite here: every string in **both** `en.json`/`fa.json`; tokens, never hexes; logical/RTL-safe props only (`dir="ltr"` islands for codes and clocks); both color schemes verified; MUI v9 API only; co-located tests for shared components; `clientFetch`/cookie rules untouched; **frontend lane only** — never edit `server/`. ## 6. Definition of Done On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md): - [ ] `npm run check` green; `npm run test:ci` green for every touched shared component (`PriceBreakdown`, `BnplPlanCard`, `InstallmentScheduleRow`, `PaymentStateCard`, `BnplProviderLogo`); `en.json`/`fa.json` in sync. - [ ] C6 answers "who + how much" without scrolling at 375px: identity block with avatar + TrustBadge, prominent total, safe-area-aware sticky pay bar; **both** CTAs disabled during initiate; back link. - [ ] Confirmation renders a receipt: copyable LTR کد پیگیری, Shamsi date-time, method, booking reference, escrow line — with real loading/error states (mock shows all fields; real path hides unserved rows). - [ ] Wallet has the four sections with four-state coverage each; a card-paying user sees payment history (mock) instead of an installments-only empty tab; money surfaces share one width system. - [ ] BNPL plan cards show down-payment/monthly/total-repayment in Toman with the fee delta explicit; no `LinearProgress` as a static fact; PlanStep's total names its plan; eligibility shows in-progress feedback; the `bnpl/return` invalid-link CTA label matches its destination. - [ ] `bnpl/gateway` returns 404 outside development; dead `CHECKOUT_GATEWAY` constant removed. - [ ] Every amount in the area carries «تومان» via ``; `PriceBreakdown` still reconciles (dev guard fires on a deliberate mismatch). Invoice prints as an A4 document (footer, paper colors from dark mode). - [ ] REQs filed with the next free numbers; visual verification on all four axes (`/fa` + `/en` × light + dark), mobile **and** desktop, for checkout, confirmation, wallet, BNPL wizard, invoice. ## 7. How to test (what a human can verify after this phase) 1. Login as a seeded customer with an accepted request → `/fa/bookings/checkout?request_id=…` at 375px: total figure at top, sticky pay bar above the bottom nav with total + «پرداخت»; nurse avatar + TrustBadge on the summary; tap pay → both CTAs disable, label swaps to «در حال شروع…». 2. Complete the mock gateway round-trip → the return page shows the staged 2-node wait («بازگشت از درگاه ✓ → در انتظار تایید بانک») with duration copy — no bare spinner+chip stack. 3. Confirmation: کد پیگیری renders LTR with a working copy button; Shamsi date-time, method, booking reference, escrow line present; reload with the network blocked → skeleton then error+retry, never a silently missing amount. 4. Open «کیف‌پول» → four sections; پرداخت‌ها lists the payment just made (mock) linking to the booking; استردادها shows `RefundStatusCard` entries after cancelling a paid booking; رسیدها links to the invoice. 5. Back on checkout, tap «پرداخت اقساطی» → provider rows show designed logos/chips (no two-letter glyphs); plan cards show پیش‌پرداخت/قسط ماهانه/مجموع بازپرداخت in Toman with «+… تومان کارمزد» on fee plans, no progress bar; selecting a plan names the header total; the eligibility check shows progress while pending. 6. `/fa/bookings/checkout/bnpl/gateway` in a production build → 404; in dev it still works. Open a paid booking's invoice → buyer/service/reference rows present (mock); print preview shows an A4 document with footer; triggered from dark mode → paper colors. 7. Repeat 1–5 on `/en` (LTR) and in dark mode: no clipped RTL/LTR islands, no unreadable tokens, «تومان» (fa) / unit label (en) on every amount. ## 8. Hand off & document (close the phase) - Update `client/CLAUDE.md` (Project Structure) for new components/route changes (`PaymentStateCard`, `BnplProviderLogo`, wallet sections); note any minimal extension made to a phase-0/1 foundation file. - Write the report at `dev/shared-working-context/reports/ui-phase-6-report.md`: what shipped per §3 item, the provider-logo and money-width decisions, four-axes screenshots, and the exact REQ numbers filed. - Save a memory note per operating-rules §8: checkout/receipt/wallet/BNPL end state, REQ numbers, and gotchas (sticky-bar vs bottom-nav layering, print stylesheet interactions) the next phase should know.