23 KiB
UI Phase 6 — Checkout & money
Mission: money UI is where trust is won or lost — and today the checkout total is a
subtitle2row 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 · Phase 5 recommended · Unlocks: the money moment finally earns the trust the ledger deserves Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the
frontend-designerskill — 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):
- 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 asubtitle2row insidePriceBreakdown(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. - 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. - The confirmation is not a receipt.
checkout/confirmation/page.tsx:51-113renders 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). - «کیفپول» is an empty promise.
wallet/page.tsxrenders onlyWalletInstallments— 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 tomaxWidth: 560(WalletInstallments.tsx:24) while checkout/confirmation/invoice run at the shell's 800. - BNPL hides the deciding numbers.
BnplPlanCard.tsx:77-98renders the down payment as a percent +LinearProgressbar — 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:163runs 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. - Spinner-stacked waits, forked terminal cards.
checkout/return/page.tsx:139-147shows title +CircularProgress+ apendingchip — three redundant signals at the flow's most anxious moment — and itsStateCard/ C6'sMessageCardare copy-pasted four times across the card & BNPL flows. - Currency-unit ambiguity. Breakdown rows and installment amounts render bare grouped numbers; only the
total carries «تومان» (
PriceBreakdown.tsx:53vs: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, andPriceBreakdown'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,insetInlineStartanchoring, dark→light token flip so dark-mode users print on paper colors (invoice/page.tsx:98-113).- Phase 0's theme/
AppIconwork, phase 1's primitives (<Money>, 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 — 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 —
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; componentsPriceBreakdown,BnplPlanCard,InstallmentScheduleRow,EscrowNotice,RefundStatusCard; servicespayment(types + hooks + bothPaymentApiimpls),bnpl,refunds;components/config.ts(CONTENT_MAX_WIDTH) andsrc/layout/config.ts(bottom-bar dimensions, for the sticky bar). - Product truth: escrow-ledger.md (what escrow actually
promises — the explainer must not overclaim),
bnpl-landscape.md (provider-financed; the agreement is
customer ↔ provider), 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<Money>with «تومان»); replace theh6-as-h1 with the phase-1 PageHeader treatment.PriceBreakdownkeeps its total row — the page-level figure is the same servedtotalIrr, 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 fromsrc/layout/config.ts— do not editlayout/(phase 2 owns it); this is page composition. - Identity moment. Give
EngagementSummarythe nurse's avatar + verifiedTrustBadgeso "who am I paying for" is answered at the moment of payment.CheckoutSummaryDtoserves 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_payembeds «←»/«→» in the translated string (fa/en.json:577) — move the arrow to anendIconslot (register a direction-aware chevron inAppIcon/config.tsif phase 0/1 didn't).
3.2 Confirmation as a receipt
- Rebuild
…/checkout/confirmation/page.tsxas a screenshot-worthy receipt card (Iranian users screenshot receipts — design for that): کد پیگیری in a copyabledir="ltr"row with a copy button, Shamsi payment date-time, method («کارت بانکی» / «اقساطی — {provider}»), booking reference, the paid total as a<Money>hero, and the escrow reassurance via theEscrowNoticecomponent — 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 behindservices/payment), اقساط (the existingWalletInstallmentscontent, semantics unchanged — provider-reported, never a Balinyaar ledger), استردادها (reuseRefundStatusCardentries;services/refundshas 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'sCONTENT_MAX_WIDTH. If a narrower measure is deliberately wanted, make it a named constant incomponents/config.tsapplied to all four.
3.4 BNPL honesty + polish
- Plan cards decide with Toman, not percent. Each
BnplPlanCardshows: پیشپرداخت (امروز) in Toman, قسط ماهانه, and مجموع بازپرداخت with the fee delta vs interest-free explicit («+۴۵۰٬۰۰۰ تومان کارمزد» in terracotta / «بدون کارمزد» neutral). Replace theLinearProgressdown-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
BnplProviderLogoregistry 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_initiatingpattern); the prefilled mobile field becomesreadOnlypresentation (normal contrast), notdisabled. - Fix the mislabelled CTA on
bnpl/return's invalid-link state: it navigates to the bookings list, so label it «رزروهای من» (reuse C6'sbd_my_bookingsinvalid-link pattern) — or route to an actual card checkout if arequest_idis recoverable. Label and destination must agree. - Guard the dev harness.
…/checkout/bnpl/gateway/page.tsxis a test harness reachable in production — env-gate it withnotFound()outside development, mirroring the card-gateway harness refinement phase 4 deleted. Also remove the deadCHECKOUT_GATEWAYconstant insrc/constants/routes.ts:24(verify unreferenced).
3.5 Invoice: fiscal grade
- Extend
…/bookings/[id]/invoice/page.tsxtoward 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).InvoiceDtoserves 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:
@pagemargins, 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 facommon.brandreads «بلینیار»; the catalog actually has «بالین یار»). The brand-spelling unification itself is (DEFERRED → phase 12, 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 (
MessageCardincheckout/page.tsx:255-286+bnpl/page.tsx,StateCardincheckout/return/page.tsx:151++bnpl/return/page.tsx) into one sharedsrc/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
<Money>primitive on every money row in this area —PriceBreakdownrows 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<Money>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 mandatedEscrowNoticesentence is untouchable — the explainer wraps it, never edits it. Ground the steps in 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. 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):
- Checkout/receipt enrichment —
nurseAvatarUrl+nurseVerifiedonCheckoutSummaryDto(§3.1); tracking/reference code,paidAt, and payment method on the payment outcome or a confirm-read for the §3.2 receipt (PaymentOutcomeDtotoday:types.ts:104-110; extends REQ-016/017). - Customer payment-transactions list — card+BNPL transactions (amount, status, date, bookingId) for the wallet «پرداختها» section (§3.3); receipts derive from it client-side.
- Customer refunds list — an "all my refunds" read for «استردادها» (§3.3) if
services/refundstruly has only by-booking/status reads (extends REQ-021). - 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
EscrowNoticecopy 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. CountdownTimersemantics: 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 editserver/.
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 (PriceBreakdown,BnplPlanCard,InstallmentScheduleRow,PaymentStateCard,BnplProviderLogo);en.json/fa.jsonin 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
LinearProgressas a static fact; PlanStep's total names its plan; eligibility shows in-progress feedback; thebnpl/returninvalid-link CTA label matches its destination. bnpl/gatewayreturns 404 outside development; deadCHECKOUT_GATEWAYconstant removed.- Every amount in the area carries «تومان» via
<Money>;PriceBreakdownstill 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)
- 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 «در حال شروع…». - Complete the mock gateway round-trip → the return page shows the staged 2-node wait («بازگشت از درگاه ✓ → در انتظار تایید بانک») with duration copy — no bare spinner+chip stack.
- 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.
- Open «کیفپول» → four sections; پرداختها lists the payment just made (mock) linking to the booking;
استردادها shows
RefundStatusCardentries after cancelling a paid booking; رسیدها links to the invoice. - 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.
/fa/bookings/checkout/bnpl/gatewayin 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.- 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.