Customer Portal — Design Specification¶
Module: customer-portal | Prefix: CP | Side: [CUSTOMER] | Date: 2026-05-28
Worktree:
docs/customer-portal-design· Branch:docs/customer-portal-designLegal status: Items marked
[LEGAL-REVIEW]depend on open questions CP-OQ-01, CP-OQ-02, CP-OQ-05. See §9 Compliance UI for detail.
0. UX Design Decisions¶
All decisions below were made by the DESIGNER, informed by the PRD, the Design Principles, and the user context (low-end Android, slow 3G, French speakers encountering insurance flows for the first time). These are the non-obvious choices — document them here so future designers and developers understand the why.
| # | Decision | Choice Made | Rationale |
|---|---|---|---|
| D1 | Progress framing | 3-step lightweight StepIndicator (Choisir → Vérifier → Payer) | Don Norman's system-status principle: users on a slow connection need to know "how much is left" before committing. Reduces abandon anxiety. Hidden on terminal screens (reçu, échec, lien remplacé). Bundle cost: ~1 KB. |
| D2 | Landing above-the-fold | Compact broker block → greeting Bonjour {Prénom}, → one-line context sentence → first contract row |
Answers the unspoken "pourquoi est-ce que je reçois ça ?" before presenting money amounts. Trust first, task second. Critical for first-time internet users who are rightfully suspicious of "click a link and pay" messages. |
| D3 | Contract details | Tap-to-expand accordion, zero network calls on expand | All data is already in the list payload (CP-02 AC). Accordion minimises cognitive load (Hick's Law: show less upfront). Zero additional RTT on slow 3G. |
| D4 | Selection persistence indicator | Amber/green sync dot per row + enregistré / en cours… microtext |
Visual confirmation of server sync without modal interruption. Never silently fails — forgiving interface principle. Color is NOT the only indicator: dot colour + text label together. |
| D5 | Consent block placement | Full-width inline block on the summary screen, un-pre-checked checkbox visually connected to the Pay CTA | Inline proximity (Don Norman) = the consent is about what you are about to do. BottomSheet alternative rejected: regulated consent text must be readable, not behind a tap. Two actions (tick + tap Payer) satisfy Loi 2013-546 art. 20 double-clic. |
| D6 | Pending screen animation | Pulsing ring (CSS-only, prefers-reduced-motion respected) |
Spinner alone = anti-pattern (implies "wait forever"). A pulsing ring communicates "alive, actively checking." Pure CSS = zero JS animation library = smaller bundle. On prefers-reduced-motion: static ring + text counter. |
| D7 | Network drop banner | Amber InlineBanner immediately below BrokerHeader, role="status" aria-live="polite" |
PRD R-CP-030 mandates appearance within 5 s. Amber (not red) because the situation is not catastrophic — selection IS saved. Must pair copy "Vos choix sont enregistrés" with the warning — never a bare "offline" message. |
| D8 | Mentions légales surface | Full dedicated route (/mentions-legales), NOT a BottomSheet |
The page contains five distinct regulated sections (broker identity, réclamation, données personnelles, transferts UEMOA, CGU). A BottomSheet at 360 px is too cramped for this volume and would trivialise legally required content. Full route = correct gravity. |
| D9 | Receipt presentation | Per-contract ReceiptCard with individual download button + single consolidated contestation InlineBanner | One card per contract makes the proof concrete and verifiable. Contestation banner is legally required (CP-07 AC, CIMA art.) and must be prominent but not alarming. |
| D10 | Error/failure screens | Neutral amber icon (not red X) + specific mapped French reason + exactly two CTAs | Failure is an invitation to retry, not a punishment. Specific mapped reasons (not generic "erreur") respect the user's intelligence and reduce support calls. Two CTAs = "Réessayer" + "Appeler le cabinet". Exception: RETOUR_INCOHÉRENT shows only the contact CTA (security signal — retry must not be suggested). |
1. User Flow Diagram¶
flowchart TD
START([Clic lien signé]) --> AUTH{AUTH vérifie le lien}
AUTH -->|AUTHENTICATED| SESSION{État session\ncôté serveur}
AUTH -->|EXPIRED| SCREEN_EXPIRED[Lien expiré\nAUTH-CP-05 copy]
AUTH -->|REVOKED reason=REISSUED| SCREEN_REPLACED[CP-S7\nLien remplacé]
AUTH -->|REVOKED reason=ALL_PAID| SCREEN_PAID[CP-S8\nDéjà réglé]
AUTH -->|REVOKED reason=MAX_LOCKOUT| SCREEN_LOCKED[Lien désactivé\nAUTH copy]
AUTH -->|TENANT_SUSPENDED| SCREEN_SUSPENDED[Tenant suspendu\nHTTP 503 AUTH copy]
SESSION -->|inflight tx PENDING| SCREEN_PENDING[CP-S5\nVérification en cours]
SESSION -->|inflight tx CONFIRMED| SCREEN_RECU[CP-S4\nReçu]
SESSION -->|inflight tx FAILED| SCREEN_FAIL[CP-S6\nÉchec paiement]
SESSION -->|no inflight tx| SCREEN_LIST[CP-S1\nListe des contrats]
SCREEN_LIST -->|tap contrat| EXPAND[Accordéon détails\nCP-02 — zero RTT]
EXPAND --> SCREEN_LIST
SCREEN_LIST -->|sélection ≥ 1| STICKY[StickyPayBar activée]
STICKY -->|tap Payer| SCREEN_SUMMARY[CP-S2\nRécapitulatif + Consentement]
SCREEN_SUMMARY -->|tap Modifier| SCREEN_LIST
SCREEN_SUMMARY -->|sélection changée côté serveur| NOTICE_CHANGED[Notice\nVotre sélection a changé]
NOTICE_CHANGED --> SCREEN_SUMMARY
SCREEN_SUMMARY -->|tick consentement\naudit customer_consent_captured| CONSENT_OK[Bouton Payer activé]
CONSENT_OK -->|tap Payer| SCREEN_INITIATING[CP-S3\nInitiation paiement]
SCREEN_INITIATING -->|PAY OK — redirect_url| PSP[Redirect PSP\n303 See Other]
SCREEN_INITIATING -->|PAY timeout / 5xx| SCREEN_FAIL
PSP -->|retour customer| RETURN{Vérif statut PAY}
RETURN -->|CONFIRMED| SCREEN_RECU
RETURN -->|PENDING| SCREEN_PENDING
RETURN -->|FAILED / CANCELLED| SCREEN_FAIL
SCREEN_PENDING -->|poll → CONFIRMED| SCREEN_RECU
SCREEN_PENDING -->|poll → FAILED ou EXPIRED_PENDING| SCREEN_FAIL
SCREEN_PENDING -->|ferme page\nreouvre lien dans TTL 72h| SESSION
SCREEN_RECU -->|télécharger reçu| DOWNLOAD[Download reçu signé\naudit CP_RECU_DOWNLOADED]
SCREEN_RECU -->|AUTH auto-revoke ALL_PAID| SCREEN_PAID
SCREEN_FAIL -->|tap Réessayer| SCREEN_SUMMARY
SCREEN_LIST -->|AUTH revoke mid-flow REISSUED| SCREEN_REPLACED
SCREEN_SUMMARY -->|AUTH revoke mid-flow REISSUED| SCREEN_REPLACED
SCREEN_LIST -->|lien pied de page| SCREEN_LEGAL[CP-S9\nMentions légales]
SCREEN_SUMMARY -->|lien pied de page| SCREEN_LEGAL
SCREEN_LEGAL -->|Retour| PREV_SCREEN[Écran précédent]
SCREEN_LIST -.->|réseau coupé > 5s| BANNER_NET[Bandeau NetworkBanner\nConnexion instable. Vos choix sont enregistrés.]
BANNER_NET -.->|XHR réussi| SCREEN_LIST
2. Screen Inventory¶
| Screen ID | Story | V0 Sub-version | UI-Bearing | Description |
|---|---|---|---|---|
| CP-S1 | CP-01, CP-02, CP-03 | V0.0.0 | ✅ | Contract list — landing screen |
| CP-S2 | CP-04 | V0.0.1 | ✅ | Summary + consent |
| CP-S3 | CP-05 | V0.0.1 | Minimal | Payment initiation redirect (loading state only) |
| CP-S4 | CP-07 | V0.0.1 | ✅ | Reçu (payment confirmed) |
| CP-S5 | CP-08 | V0.0.1 | ✅ | Vérification en cours (pending) |
| CP-S6 | CP-09 | V0.0.1 | ✅ | Échec du paiement |
| CP-S7 | CP-10 | V0.0.2 | ✅ | Lien remplacé |
| CP-S8 | CP-01.3, CP-13 | V0.0.2 | ✅ | Déjà réglé (all paid / post-revoke) |
| CP-S9 | CP-12 | V0.0.0 | Content route | Mentions légales et données personnelles |
| CP-S10 | AUTH-CP-05 | V0.1.0 | AUTH-owned | Lien expiré (AUTH controls copy, CP provides broker skin) |
| CP-S11 | AUTH §B 503 | V1 | AUTH-owned | Tenant suspendu (AUTH 503, CP provides broker skin) |
Cross-screen transient states:
| State | Trigger | Screens affected |
|---|---|---|
| NetworkBanner | XHR failure >5 s OR navigator.onLine = false |
CP-S1 through CP-S9 |
| Selection sync dot | Any selection mutation POST | CP-S1 contract rows |
3. Global Elements¶
3.1 BrokerHeader — every screen¶
Design decision: D2. The broker identity must appear above the fold on every screen, including error and terminal screens (CP-NFR-13). This is a CIMA regulatory requirement, not a design preference.
360px
┌──────────────────────────────────────────────┐
│ [LOGO 48×48] Cabinet Konan Assurances │ ← commercial_name —font-size-h4 —font-weight-semibold
│ Agrément CIMA 00-XX-2024 │ ← —font-size-caption —color-text-secondary
│ [📞 Appeler] [💬 WhatsApp] │ ← Contact chips, 44px min-height
└──────────────────────────────────────────────┘
768px+
┌──────────────────────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances [Agrément] [📞] [💬] │
└──────────────────────────────────────────────────────────────┘
- Position: sticky top,
z-index: var(--z-topbar),background: var(--color-surface),box-shadow: var(--shadow-sm) - Logo: 48×48px,
object-fit: contain, loaded from TENANTlogo_object_keysigned URL; fallback = initials avatar using--color-primary - WhatsApp chip: hidden if
tenant.whatsapp_business_phoneis null (CP-TV-07) - Loading skeleton: gray rect 48×48 + two shimmer text lines
3.2 StepIndicator — CP-S1, CP-S2, CP-S3 only¶
Design decision: D1.
- Active node:
--color-primaryfilled circle 12px - Inactive node:
--color-bordercircle 12px - Passed connector:
--color-primaryline; pending connector:--color-borderline - Labels:
--font-size-caption--color-text-secondarybelow each node - Hidden entirely on CP-S4 through CP-S11 (terminal / error / edge screens)
- New component:
StepIndicatorinfrontend/src/components/ui/StepIndicator.tsx
3.3 NetworkBanner — all active screens¶
Design decision: D7. PRD R-CP-030.
┌──────────────────────────────────────────────┐
│ ⚠ Connexion instable. Vos choix sont │
│ enregistrés. │
└──────────────────────────────────────────────┘
- Positioned immediately below BrokerHeader (not overlapping content)
role="status"aria-live="polite"aria-atomic="true"- Background:
--color-warningat 15% opacity; left border 3px--color-warning - Appears within 5 s of first XHR failure; auto-clears on next successful XHR (no manual dismiss)
- Animation: 150 ms slide-down.
prefers-reduced-motion: instant appear/disappear.
4. Wireframes¶
CP-S1: Contract List¶
Design decisions shaping this screen: D1, D2, D3, D4.
UX note: This is the highest-trust-gap screen in the product. A customer who received a WhatsApp saying "cliquez ici pour renouveler" is rightfully wary. The three unspoken questions before they will engage — "Qui m'envoie ça ?", "Pourquoi ?", "Qu'est-ce que je dois faire ?" — must all be answered above the fold on a 360px screen before presenting money amounts.
Layout — 360px (primary)¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │ ← BrokerHeader (sticky)
│ Agrément CIMA 00-XX-2024 │
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ [ ●──────────○──────────○ ] │ ← StepIndicator step 1
│ Choisir Vérifier Payer │
├──────────────────────────────────────────────┤
│ Bonjour Awa, │ ← h1 —font-size-h2 —font-weight-semibold
│ Votre cabinet vous invite à renouveler │ ← —font-size-body —color-text-secondary
│ vos contrats qui arrivent à échéance. │
├──────────────────────────────────────────────┤
│ Vos contrats à renouveler [2 contrats] │ ← Section label + Badge
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ [☑] Auto — Police 4471 [▾] │ │ ← ContractRow (selected, collapsed)
│ │ Échéance 30/06/2026 │ │
│ │ 75 000 XOF ● enregistré │ │ ← Sync dot green
│ └──────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────┐ │
│ │ [☑] Habitation — Police 8823 [▾] │ │ ← ContractRow (selected, collapsed)
│ │ Échéance 15/07/2026 │ │
│ │ 42 500 XOF ● enregistré │ │
│ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ Mentions légales et données personnelles │ ← Footer link —font-size-small
└──────────────────────────────────────────────┘
↓ StickyPayBar (fixed bottom) ↓
┌──────────────────────────────────────────────┐
│ 2 contrats sélectionnés │
│ [ Payer 2 contrats — 117 500 XOF ] │ ← Button primary fullWidth
└──────────────────────────────────────────────┘
Expanded contract row (D3 — zero network calls on expand)¶
┌──────────────────────────────────────────────┐
│ [☑] Auto — Police 4471 [▴] │
│ Période : 01/07/2026 – 30/06/2027 │
│ Échéance : 30/06/2026 │
│ Prime : 75 000 XOF │
│ ────────────────────────────────────── │
│ Garanties principales │
│ Responsabilité civile, Vol, Incendie │
│ ────────────────────────────────────── │
│ Exclusions / Limites │
│ Usure normale, Guerre │
│ ────────────────────────────────────── │
│ [Plus de détails — Mentions légales →] │
└──────────────────────────────────────────────┘
Missing field variant (R-CP-005):
"Liste mise à jour" notice (R-CP-002)¶
┌──────────────────────────────────────────────┐
│ ℹ Certains contrats ne sont plus │
│ éligibles. Votre liste a été mise à jour.│
└──────────────────────────────────────────────┘
variant="info", above contract rows, auto-dismissable after 8 s.
5 States¶
| State | Behaviour |
|---|---|
| Loading | BrokerHeader skeleton (logo gray rect 48×48 + 2 shimmer lines), StepIndicator placeholder, 2 ContractRow skeletons same height as rows. Shimmer uses --color-bg-tertiary. No spinner alone. |
| Empty | If ALL_PAID → route to CP-S8 (R-CP-004). If operator excluded all → InlineBanner info: "Aucun contrat à renouveler pour le moment. Contactez votre cabinet." + click-to-call chip. |
| Error | XHR load failure → InlineBanner error: "Impossible de charger vos contrats. Vérifiez votre connexion et réessayez." + Button ghost "Réessayer". BrokerHeader still visible (from session cache). |
| Low-network / drop | NetworkBanner appears. Selection mutations queue. Amber sync dot on mutated rows. Retry every 10 s with exponential backoff (cap 30 s). On reconnect: dot turns green + "enregistré". |
| Success | Contracts listed with checkboxes. StickyPayBar live-reflects total. Server selection authoritative on reload. |
CP-S2: Récapitulatif + Consentement¶
Design decisions: D5 (consent inline), D1 (step 2).
UX note: Double duty — legal compliance surface AND confirmation UX. The rétractation notice (CIMA art. 24) MUST appear BEFORE the Pay button. The consent checkbox MUST be un-pre-checked and visually tethered to the Pay action. Two distinct actions — tick + tap — satisfy Loi 2013-546 art. 20.
Layout — 360px¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │ ← BrokerHeader
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ [ ●──────────●──────────○ ] │ ← StepIndicator step 2
│ Choisir Vérifier Payer │
├──────────────────────────────────────────────┤
│ Récapitulatif │ ← h1
│ Vérifiez vos contrats avant de payer. │ ← —color-text-secondary
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ Auto — Police 4471 │ │ ← ContractSummaryRow (read-only Card)
│ │ Période : 01/07/2026 – 30/06/2027 │ │
│ │ Prime : 75 000 XOF │ │
│ └──────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────┐ │
│ │ Habitation — Police 8823 │ │
│ │ Période : 01/08/2026 – 31/07/2027 │ │
│ │ Prime : 42 500 XOF │ │
│ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ Total : 117 500 XOF │ ← —font-size-h3 —font-weight-bold
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ [☐] Je confirme avoir pris connaissance │ │ ← Checkbox (NEVER pre-ticked, NEVER auto-ticked)
│ │ des conditions des contrats │ │
│ │ sélectionnés et des │ │
│ │ [mentions légales] du cabinet. │ │ ← "mentions légales" = link to CP-S9
│ └──────────────────────────────────────────┘ │
│ │
│ ─ Droit de rétractation ─────────────────── │ ← CIMA art. 24 — must appear BEFORE Pay button
│ Vous disposez d'un délai de 14 jours │
│ pour exercer votre droit de rétractation. │
│ Contactez votre cabinet au │
│ +225 XX XX XX XX. │
│ ─────────────────────────────────────────── │
│ │
│ [ Payer 117 500 XOF par mobile money ] │ ← Button primary fullWidth (DISABLED until tick)
│ [ ← Modifier ma sélection ] │ ← Button ghost fullWidth
│ │
├──────────────────────────────────────────────┤
│ Mentions légales et données personnelles │
└──────────────────────────────────────────────┘
"Sélection a changé" variant (R-CP-009)¶
┌──────────────────────────────────────────────┐
│ ⚠ Votre sélection a changé : voici le │
│ détail à jour. │
│ [Confirmer] [← Modifier] │
└──────────────────────────────────────────────┘
variant="warning" above contracts. Consent checkbox hidden + Pay button disabled until user taps Confirmer.
5 States¶
| State | Behaviour |
|---|---|
| Loading | ContractSummaryRow skeletons + Total skeleton. Consent block hidden until data loaded. |
| Empty | All selected contracts ineligible → Toast error + redirect to contract list: "Aucun contrat sélectionné n'est plus éligible." |
| Error | XHR eligibility re-check fails → InlineBanner error: "Impossible de charger le récapitulatif. Réessayez." + Button ghost "Réessayer". |
| Low-network | NetworkBanner. Consent tick is optimistic-local; customer_consent_captured POST retries on reconnect. Pay button enabled on local tick. |
| Success | Contracts read-only. Checkbox un-ticked. Pay button disabled. On tick: POST audit event, Pay button enabled. |
CP-S3: Initiation paiement (Redirect loading)¶
UX note: The gap between "tap Payer" and the PSP redirect can feel long on Slow 3G. Immediate button loading state prevents double-tap anxiety. After 1.5 s, full overlay with transaction info.
Button loading state (0–1.5 s)¶
[ ◌ Connexion au service de paiement… ] ← Button primary loading, DISABLED
[ ← Modifier ma sélection (disabled) ]
Full overlay (>1.5 s)¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │
├──────────────────────────────────────────────┤
│ [ ●──────────●──────────● ] │ ← StepIndicator step 3
├──────────────────────────────────────────────┤
│ │
│ ◌ (pulsing ring, CSS-only) │
│ │
│ Connexion au service de paiement… │ ← h1
│ Veuillez patienter. │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Référence provisoire PAY-2026-XXXXX │ │
│ │ Montant 117 500 XOF │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
5 States¶
| State | Behaviour |
|---|---|
| Loading | Button spinner (0–1.5 s) → pulsing ring overlay (>1.5 s) |
| Empty | N/A |
| Error | PAY error / timeout → transition to CP-S6 with provider_error_code=PAY_INITIATION_FAILED |
| Low-network | NetworkBanner. POST to PAY continues (server-side). After 15 s server timeout → CP-S6. |
| Success | Redirect via 303 See Other to PSP URL (browser handles). |
CP-S4: Reçu (Paiement confirmé)¶
Design decisions: D9 (per-contract cards + contestation), D10 (success framing).
UX note: Money has changed hands. Design communicates: ✓ it worked, ✓ how much, ✓ here is your proof, ✓ what to do if there is a problem. The reference number on-screen IS proof (readable to an agent over the phone). PDF is secondary proof.
Layout — 360px¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │ ← BrokerHeader (sticky)
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ ✓ Paiement reçu │ │ ← SuccessBanner (—color-success bg)
│ │ Référence : PAY-2026-XXXXX │ │
│ │ 117 500 XOF — 28/05/2026 │ │
│ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ Vos contrats renouvelés │ ← Section label
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ ✓ Auto — Police 4471 │ │ ← ReceiptCard
│ │ Période 01/07/2026 – 30/06/2027 │ │
│ │ 75 000 XOF │ │
│ │ [ ↓ Télécharger le reçu ] │ │ ← Button secondary
│ └──────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────┐ │
│ │ ✓ Habitation — Police 8823 │ │ ← ReceiptCard
│ │ Période 01/08/2026 – 31/07/2027 │ │
│ │ 42 500 XOF │ │
│ │ [ ↓ Télécharger le reçu ] │ │
│ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ ℹ Pour toute contestation, contactez │ │ ← InlineBanner info (CIMA obligation)
│ │ le cabinet sous 30 jours au │ │
│ │ +225 XX XX XX XX. │ │
│ └──────────────────────────────────────────┘ │
│ Vous recevrez un message de confirmation │ ← NOTIF cross-module info
│ par SMS/WhatsApp. │
├──────────────────────────────────────────────┤
│ Mentions légales et données personnelles │
└──────────────────────────────────────────────┘
Download interaction¶
- Tap "Télécharger le reçu" → button loading state (spinner)
- CP fetches fresh signed URL from PAY (
GET /pay/transactions/{ref}/recus) — 5-min TTL, single-use - Browser navigates to signed URL → download
- Button returns to default state
- On failure: button shows "Reçu indisponible. Réessayez." — retry re-fetches a fresh URL (R-CP-023)
5 States¶
| State | Behaviour |
|---|---|
| Loading | SuccessBanner skeleton (green tint) + ReceiptCard skeletons while fetching signed URLs. |
| Empty | N/A (always ≥ 1 confirmed contract) |
| Error | Reçu URL fetch fails for one contract → that ReceiptCard shows error. Other cards unaffected. |
| Low-network | NetworkBanner. Download button shows "Connexion requise pour télécharger" when offline. |
| Success | SuccessBanner + ReceiptCards + Contestation InlineBanner. |
CP-S5: Vérification en cours (PENDING)¶
Design decisions: D6 (pulsing ring), D10 (clear copy).
UX note: "Did I pay or not?" Three imperatives: (1) reassure "nothing wrong yet," (2) prove a transaction exists (reference + amount = verifiable artefact), (3) give explicit permission to close the page at 60 s. Spinner alone = anti-pattern.
Layout — 0–60 s¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ │
│ ◌ (pulsing ring — CSS-only) │ ← 60px, —color-primary, 1.5s ease-in-out loop
│ │
│ Paiement en cours de vérification… │ ← h1
│ │
│ Cela peut prendre jusqu'à quelques minutes.│
│ Ne fermez pas cette page si possible. │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Référence PAY-2026-XXXXX │ │ ← Transaction Card (read-only)
│ │ Montant 117 500 XOF │ │
│ │ Opérateur Orange Money │ │
│ └──────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
Layout — >60 s¶
│ ◌ (pulsing ring, slower rhythm) │
│ │
│ Paiement en cours de vérification… │
│ │
│ Cela prend plus de temps que prévu. │
│ Vous recevrez un message dès que │
│ le paiement est confirmé. │
│ │
│ Vous pouvez fermer cette page. │ ← Explicit exit — —font-weight-semibold
│ │
│ [Transaction Card] │
│ [ 📞 Contacter le cabinet ] │
prefers-reduced-motion variant¶
Pulsing ring → static ring + text counter: "Vérification… (N tentatives)". No animation.
5 States¶
| State | Behaviour |
|---|---|
| Loading (mount) | BrokerHeader skeleton briefly, then immediately pending state. |
| Empty | N/A |
| Error | Poll XHR fails 3 consecutive times → InlineBanner warning: "Vérification difficile. Votre paiement est en cours de traitement." Polling continues. |
| Low-network | NetworkBanner. Polling pauses. On reconnect: Toast "Connexion rétablie. Vérification en cours…" + polling resumes. |
| Terminal transitions | CONFIRMED → 200 ms cross-fade to CP-S4. FAILED/EXPIRED_PENDING → 200 ms cross-fade to CP-S6. (prefers-reduced-motion: instant cut.) |
CP-S6: Échec du paiement¶
Design decisions: D10 (neutral icon, specific copy, two CTAs).
UX note: Failure is an invitation to retry. Amber icon (not red). Specific mapped French reason. Selection preserved (R-CP-026). RETOUR_INCOHÉRENT = only contact CTA (security signal).
Layout — 360px¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ │
│ (!) (circle-exclamation, amber) │ ← SVG icon <2 KB, —color-warning
│ │
│ Échec du paiement │ ← h1
│ │
│ Solde insuffisant sur votre compte │ ← Mapped FR reason
│ mobile money. │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Référence PAY-2026-XXXXX │ │
│ │ Montant 117 500 XOF │ │
│ └──────────────────────────────────────────┘ │
│ │
│ [ Réessayer le paiement ] │ ← Button primary
│ [ 📞 Contacter le cabinet ] │ ← Button secondary
│ │
└──────────────────────────────────────────────┘
Error code → FR copy mapping¶
provider_error_code |
Copy displayed |
|---|---|
SOLDE_INSUFFISANT |
Solde insuffisant sur votre compte mobile money. |
CODE_REFUSÉ |
Le code saisi a été refusé par votre opérateur. |
ANNULATION_CLIENT |
Vous avez annulé le paiement. |
INDISPONIBLE |
Le service de paiement est temporairement indisponible. Réessayez. |
EXPIRATION |
Le délai de paiement est dépassé. Réessayez. |
RETOUR_INCOHÉRENT |
Une incohérence a été détectée. Contactez le cabinet. (Réessayer hidden) |
AUTRE |
Le paiement n'a pas abouti. Réessayez ou contactez le cabinet. |
PAY_INITIATION_FAILED |
Impossible de démarrer le paiement. Réessayez ou contactez le cabinet. |
EXPIRED_PENDING |
Le paiement n'a pas pu être confirmé. Aucun montant n'a été prélevé. Vous pouvez réessayer. |
5 States¶
| State | Behaviour |
|---|---|
| Loading | Brief skeleton while reading session state. |
| Empty | N/A |
| Error | PAY status poll fails → "Impossible de vérifier l'état du paiement. Contactez le cabinet." + contact chip only. |
| Low-network | NetworkBanner. "Réessayer" navigates to CP-S2 (local navigation, no network required). |
| Success (rendered) | Specific reason + two CTAs (or one for RETOUR_INCOHÉRENT). |
CP-S7: Lien remplacé¶
UX note: User clicked a link they received and it "stopped working." No jargon ("révoqué", "invalidé", "session expirée"). Reassure, explain, give the path forward.
Layout — 360px¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ │
│ 📩 (envelope SVG, <3 KB) │ ← Reassuring, not alarming
│ │
│ Un nouveau lien est disponible │ ← h1
│ │
│ Votre cabinet a envoyé un nouveau lien │
│ de renouvellement. │
│ Consultez votre WhatsApp ou SMS │
│ le plus récent. │
│ │
│ [ 💬 Ouvrir WhatsApp ] │ ← wa.me deep link (hidden if no WA number)
│ [ 📞 Contacter le cabinet ] │ ← tel: deep link
│ │
└──────────────────────────────────────────────┘
5 States¶
| State | Behaviour |
|---|---|
| Loading | BrokerHeader skeleton (1 s max), then full screen render. |
| Displayed | As above. Terminal state on this link. |
| No WA number | WhatsApp button hidden. Tel button only. |
| Low-network | NetworkBanner. Buttons use tel: / wa.me: deep links (no network needed). |
| Mid-flow revoke | Toast: "Ce lien a été remplacé par votre cabinet." → same screen. |
CP-S8: Déjà réglé (All Paid)¶
UX note: Positive state — but the user may be returning specifically for receipts. Surface download capability immediately after the brief confirmation.
Layout — 360px¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ │
│ ✓ (checkmark SVG, —color-success) │
│ │
│ Vous avez déjà réglé toutes vos │ ← h1
│ échéances pour cette période. │
│ │
│ Retrouvez vos reçus ci-dessous. │ ← Immediate utility
│ │
├──────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────┐ │
│ │ ✓ Auto — Police 4471 │ │ ← ReceiptCard
│ │ 75 000 XOF — 28/05/2026 │ │
│ │ [ ↓ Télécharger le reçu ] │ │
│ └──────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────┐ │
│ │ ✓ Habitation — Police 8823 │ │ ← ReceiptCard
│ │ 42 500 XOF — 28/05/2026 │ │
│ │ [ ↓ Télécharger le reçu ] │ │
│ └──────────────────────────────────────────┘ │
├──────────────────────────────────────────────┤
│ Mentions légales et données personnelles │
└──────────────────────────────────────────────┘
5 States¶
| State | Behaviour |
|---|---|
| Loading | Checkmark + headline static. ReceiptCard skeletons while fetching signed URLs. |
| Empty | N/A (always ≥ 1 paid contract) |
| Error | Receipt URL fetch fails per card → "Reçu temporairement indisponible. Réessayez." Other cards unaffected. |
| Low-network | NetworkBanner. Download buttons show "Connexion requise" when offline. |
| TTL elapsed | CP-side read-only session expired → "Ce lien a expiré. Contactez votre cabinet pour obtenir vos reçus." + click-to-call chip. |
CP-S9: Mentions légales et données personnelles¶
Design decision: D8 (full route, not BottomSheet).
Layout — 360px¶
┌──────────────────────────────────────────────┐
│ [LOGO] Cabinet Konan Assurances │
│ [📞 Appeler] [💬 WhatsApp] │
├──────────────────────────────────────────────┤
│ [← Retour] │ ← Button ghost
│ Mentions légales et données personnelles │ ← h1
├──────────────────────────────────────────────┤
│ Identité du courtier │ ← h2
│ {legal_name}, {commercial_name} │
│ Agrément CIMA {agrement_number} │
│ RCCM {rccm}, Adresse : {address} │
│ ─────────────────────────────────────────── │
│ Procédure de réclamation │ ← h2
│ Pour toute réclamation ou contestation, │
│ contactez le cabinet dans un délai de │
│ 30 jours suivant la date du paiement. │
│ {custom_content from TENANT} │
│ ─────────────────────────────────────────── │
│ Protection des données personnelles │ ← h2
│ {custom_content from TENANT} │
│ ─────────────────────────────────────────── │
│ Transferts hors UEMOA │ ← h2 (NOTIF-US-08 canonical block)
│ {canonical_content from NOTIF} │
│ ─────────────────────────────────────────── │
│ Conditions générales d'utilisation │ ← h2
│ et conditions du paiement │
│ {custom_content from TENANT} │
│ ─────────────────────────────────────────── │
│ Version : {content_version} │ ← —font-size-caption —color-text-tertiary
│ Dernière mise à jour : {JJ/MM/AAAA} │
└──────────────────────────────────────────────┘
Default content rule: every section has platform-default French text with {commercial_name}, {agrement_number}, {agency.phone} substituted. Never empty, never lorem ipsum.
5 States¶
| State | Behaviour |
|---|---|
| Loading | Section headings render immediately. Body content sections skeleton-shimmer while fetching. |
| Empty | N/A (platform defaults always fill content) |
| Error | Fetch fails → platform defaults render + InlineBanner info: "Contenu mis à jour non disponible. Version de secours affichée." |
| Low-network | NetworkBanner. Content likely cached within session. |
| Success | Full tenant-customised content with version footer. |
5. Interaction Specifications¶
ContractRow (new component)¶
File: frontend/src/customer/components/ContractRow.tsx
States:
| State | Visual |
|---|---|
| unselected-collapsed | Unchecked checkbox, chevron right, no sync dot |
| selected-collapsed | Checked checkbox (—color-primary), chevron right, green dot + "enregistré" |
| selected-pending-sync | Checked checkbox, amber dot + "en cours…" |
| selected-sync-error | Checked checkbox, amber dot + "non enregistré" + subtle retry indicator |
| expanded | Any selection state above + expanded details block (200 ms slide-down ease-out) |
Touch: - Entire row tap = toggle expand/collapse (except checkbox area) - Checkbox: 48×48 px minimum hit area, tap = toggle selection only - Chevron is decorative (tap goes to row handler)
Keyboard: Tab → focus checkbox; Space → toggle selection; Enter on row → toggle expand
Accessibility:
- role="article" on row container; aria-expanded="true|false" on row
- Checkbox aria-label="Sélectionner le contrat {branche} — Police {n°}"
- Sync dot: aria-label per state + text label (color never sole indicator)
Props:
interface ContractRowProps {
contract: ContractSummary; // branche, n_police, echeance, periode, prime, garanties?, exclusions?
selected: boolean;
syncState: 'idle' | 'pending' | 'saved' | 'error';
expanded: boolean;
onToggleSelect: (contractId: string) => void;
onToggleExpand: (contractId: string) => void;
}
StickyPayBar (new component)¶
File: frontend/src/customer/components/StickyPayBar.tsx
Position: position: fixed; bottom: 0; left: 0; right: 0; padding-bottom: env(safe-area-inset-bottom, 0px); z-index: calc(var(--z-topbar) + 10)
States:
Disabled (0 selected):
"Sélectionnez au moins un contrat à régler."
[ Payer (disabled, grayed) ]
Enabled (N > 0):
"N contrat(s) sélectionné(s)"
[ Payer N contrats — TOTAL XOF ]
Loading (after tap):
"N contrat(s) sélectionnés"
[ ◌ Connexion au service de paiement… ]
Animation: slides up when first contract selected (200 ms ease-out); slides down on last deselect. prefers-reduced-motion: instant.
Accessibility: role="region" aria-label="Récapitulatif de sélection"; count line aria-live="polite" aria-atomic="true".
NetworkBanner (new component)¶
File: frontend/src/customer/components/NetworkBanner.tsx
Trigger hook (useNetworkBanner):
1. navigator.onLine = false → show immediately
2. XHR timeout / 5xx → show after 5 s debounce
3. Clears on next successful XHR or navigator.onLine = true
Accessibility: role="status" aria-live="polite" aria-atomic="true"
BrokerHeader (new component)¶
File: frontend/src/customer/components/BrokerHeader.tsx
interface BrokerHeaderProps {
tenant: {
commercial_name: string;
agrement_number: string;
logo_url?: string; // signed URL; null → initials fallback using —color-primary
};
agency: {
phone: string; // E.164 → display +225 XX XX XX XX
whatsapp_phone?: string; // null → WA chip hidden
commercial_name: string;
};
onCallClick?: () => void; // emits customer_support_contact_clicked audit event
onWhatsAppClick?: () => void;
}
ReceiptCard (new component)¶
File: frontend/src/customer/components/ReceiptCard.tsx
States: default, downloading (button spinner), error (download failed, retry available)
Download error recovery: retry calls GET /pay/transactions/{ref}/recus for a fresh URL (not retry of expired signed URL).
Accessibility: Download button aria-label="Télécharger le reçu pour {branche} — Police {n°}"
Consent Checkbox (CP-S2, uses existing Checkbox component)¶
aria-required="true",aria-describedby="consent-text"→ the full consent paragraph element- Initial state:
checked={false}— NEVER auto-set to true on any render, including reload (R-CP-010) - On tick: POST
customer_consent_captured→ on ACK: enable Pay button - On POST failure: keep Pay button disabled + NetworkBanner + silent retry on reconnect
- On un-tick: disable Pay button immediately (local state)
6. Responsive Behaviour¶
Phone 360px — primary design target¶
- BrokerHeader: two-row compact (logo + name/agrément row, chips row)
- StepIndicator: full width, labels below nodes
- ContractRows: full width, stacked
- StickyPayBar: full viewport width, fixed bottom
- Summary: single column
- Consent block: full-width Card,
padding: var(--space-4) - Mentions légales: single column full-width prose
Phone 414px¶
Same layout. Internal padding: --space-4 → --space-5. Rows slightly taller.
Tablet 768px¶
- BrokerHeader: single row — logo + name + agrément chip inline + chips right
- StepIndicator: centered, wider node spacing
- ContractRows: max-width 600px, centered
- StickyPayBar: max-width 600px, centered (not full-viewport)
- Summary + consent: centered 600px
- Mentions légales: centered 640px, broker identity in 2-column table
Desktop 1024–1440px¶
Intentional decision: content column max-width 500px, centered, wide warm margins. No sidebar, no top nav, no density expansion. The portal always feels like a mobile experience. If a customer lands on desktop (shared computer), the narrow column still works perfectly and keeps the broker-first aesthetic.
1024px+
┌───────────────────────────────────────────────────────────┐
│ (—color-bg-primary warm fill — wide margins) │
│ ┌─────────────────────────────────────┐ │
│ │ BrokerHeader │ 500px │
│ │ StepIndicator │ column │
│ │ ...content... │ │
│ │ StickyPayBar (stays in column) │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
StickyPayBar at desktop: max-width: 500px; left: 50%; transform: translateX(-50%).
7. Connectivity Behaviour¶
Slow initial load (Slow 3G: RTT 2 s, 400 Kbps)¶
| Screen | Behaviour |
|---|---|
| CP-S1 | HTML + critical CSS stream first. BrokerHeader skeleton renders immediately. Contract JSON (~5–15 KB) arrives 2–3 s. First contentful paint < 2 s. Critical path bundle < 200 KB gzipped (CP-NFR-01). |
| CP-S2 | Client-side nav from CP-S1: data in memory, near-instant. Hard URL load: same skeleton pattern. |
| CP-S3 | Pulsing ring overlay — no skeleton. UI is explicitly "loading / connecting." |
| CP-S4, S5, S6 | Transaction status from session on load. 1–2 s skeleton then state-appropriate screen. |
| CP-S9 | Section headings render immediately. Body skeletons while TENANT content fetches. |
Network drop mid-flow¶
| Scenario | Behaviour |
|---|---|
| Drop on CP-S1, selection in progress | NetworkBanner. Selection queue with amber dot. Retry 10 s / exponential backoff (cap 30 s). On reconnect: dot turns green. |
| Drop on CP-S2 | NetworkBanner. Consent tick optimistic-local; POST retries on reconnect. |
| Drop at PSP redirect | Browser handles. Customer re-opens link → session shows inflight tx → routes to CP-S5 (PENDING). |
| Drop during CP-S5 polling | NetworkBanner. Polling pauses. On reconnect: Toast "Connexion rétablie. Vérification en cours…" + polling resumes. |
| Drop during CP-S4 receipt download | NetworkBanner. ReceiptCard: "Reçu indisponible. Réessayez." On reconnect: normal retry. |
Reload at any point (within 72h TTL)¶
Server session is the single source of truth (CP-NFR-04). On reload: 1. AUTH verifies link → returns session state 2. CP reads B.4 routing table → renders correct screen 3. Zero data loss: selection, transaction reference, consent version (all server-side)
Link reuse¶
Semantically identical to reload. The signed link IS the session key.
Payment-status-unknown (PENDING on reload)¶
CP-S5 renders immediately. Polling resumes. Same reference + amount. Copy: "vérification en cours" — not "nous avons perdu trace."
8. French Micro-copy¶
BrokerHeader¶
| Element | Copy |
|---|---|
| Agrément | Agrément CIMA {agrement_number} |
| Call chip | 📞 Appeler |
| WA chip | 💬 WhatsApp |
aria-label call |
Appeler le cabinet {commercial_name} |
aria-label WA |
Contacter le cabinet {commercial_name} sur WhatsApp |
CP-S1 — Contract List¶
| Element | Copy |
|---|---|
| Greeting (individual) | Bonjour {prénom}, |
| Greeting (legal entity / name unavailable) | Bonjour, |
| Context sentence | Votre cabinet vous invite à renouveler vos contrats qui arrivent à échéance. |
| Section label | Vos contrats à renouveler |
| Count (1) | 1 contrat |
| Count (N) | {N} contrats |
Expand aria-label |
Voir les détails du contrat {branche} — Police {n°} |
Collapse aria-label |
Masquer les détails du contrat {branche} — Police {n°} |
Checkbox aria-label |
Sélectionner le contrat {branche} — Police {n°} |
| Sync dot saved | enregistré |
| Sync dot pending | en cours… |
| Sync dot error | non enregistré |
| Details link | Plus de détails — Mentions légales → |
| Missing field | Détails non fournis. Demandez à votre conseiller. |
| Footer link | Mentions légales et données personnelles |
| Notice — list updated | Certains contrats ne sont plus éligibles. Votre liste a été mise à jour. |
| Empty — no eligible | Aucun contrat à renouveler pour le moment. Contactez votre cabinet. |
| Error — load failed | Impossible de charger vos contrats. Vérifiez votre connexion et réessayez. |
| Error CTA | Réessayer |
StickyPayBar¶
| State | Row 1 | Button |
|---|---|---|
| 0 selected | Sélectionnez au moins un contrat à régler. |
Payer (disabled) |
| 1 selected | 1 contrat sélectionné |
Payer 1 contrat — {total} XOF |
| N selected | {N} contrats sélectionnés |
Payer {N} contrats — {total} XOF |
CP-S2 — Summary + Consent¶
| Element | Copy |
|---|---|
| h1 | Récapitulatif |
| Sub-copy | Vérifiez vos contrats avant de payer. |
| Total | Total : |
| Consent | Je confirme avoir pris connaissance des conditions des contrats sélectionnés et des mentions légales du cabinet. |
| Rétractation | Vous disposez d'un délai de 14 jours pour exercer votre droit de rétractation. Contactez votre cabinet au {agency.phone}. |
| Pay button (enabled) | Payer {total} XOF par mobile money |
| Back button | ← Modifier ma sélection |
| Selection changed | Votre sélection a changé : voici le détail à jour. |
| Confirm CTA | Confirmer |
| No eligible (Toast) | Aucun contrat sélectionné n'est plus éligible. |
| Error | Impossible de charger le récapitulatif. Réessayez. |
CP-S3 — Initiation¶
| Element | Copy |
|---|---|
| Button loading | Connexion au service de paiement… |
| Overlay heading | Connexion au service de paiement… |
| Overlay body | Veuillez patienter. |
| Initiation failed h1 | Impossible de démarrer le paiement |
| Initiation failed body | Réessayez ou contactez le cabinet. |
CP-S4 — Reçu¶
| Element | Copy |
|---|---|
| Success banner | Paiement reçu |
| Reference | Référence : {transaction_reference} |
| Amount + date | {total} XOF — {JJ/MM/AAAA} |
| Section title | Vos contrats renouvelés |
| Download button | ↓ Télécharger le reçu |
| Download loading | Téléchargement… |
| Download error | Reçu indisponible. Réessayez. |
| Contestation | Pour toute contestation, contactez le cabinet sous 30 jours au {agency.phone}. |
| NOTIF info | Vous recevrez un message de confirmation par SMS/WhatsApp. |
CP-S5 — Vérification en cours¶
| Period | Element | Copy |
|---|---|---|
| 0–60 s | h1 | Paiement en cours de vérification… |
| 0–60 s | Body | Cela peut prendre jusqu'à quelques minutes. Ne fermez pas cette page si possible. |
| >60 s | Body | Cela prend plus de temps que prévu. Vous recevrez un message dès que le paiement est confirmé. |
| >60 s | Exit | Vous pouvez fermer cette page. |
| Poll error | Banner | Vérification difficile. Votre paiement est en cours de traitement. |
| Reconnect | Toast | Connexion rétablie. Vérification en cours… |
CP-S6 — Échec¶
| Element | Copy |
|---|---|
| h1 | Échec du paiement |
| Retry CTA | Réessayer le paiement |
| Contact CTA | 📞 Contacter le cabinet |
| Status unknown | Impossible de vérifier l'état du paiement. Contactez le cabinet. |
(Error code → FR copy mapping: see §4 CP-S6 table)
CP-S7 — Lien remplacé¶
| Element | Copy |
|---|---|
| h1 | Un nouveau lien est disponible |
| Body | Votre cabinet a envoyé un nouveau lien de renouvellement. Consultez votre WhatsApp ou SMS le plus récent. |
| WA button | 💬 Ouvrir WhatsApp |
| Tel button | 📞 Contacter le cabinet |
| Mid-flow Toast | Ce lien a été remplacé par votre cabinet. |
CP-S8 — Déjà réglé¶
| Element | Copy |
|---|---|
| h1 | Vous avez déjà réglé toutes vos échéances pour cette période. |
| Sub-copy | Retrouvez vos reçus ci-dessous. |
| TTL elapsed | Ce lien a expiré. Contactez votre cabinet pour obtenir vos reçus. |
CP-S9 — Mentions légales¶
| Element | Copy |
|---|---|
| h1 | Mentions légales et données personnelles |
| Back | ← Retour |
| Section 1 | Identité du courtier |
| Section 2 | Procédure de réclamation |
| Section 3 | Protection des données personnelles |
| Section 4 | Transferts hors UEMOA |
| Section 5 | Conditions générales d'utilisation et conditions du paiement |
| Version footer | Version du document : {content_version} |
| Updated footer | Dernière mise à jour : {JJ/MM/AAAA} |
| Default fallback | InlineBanner info: Contenu mis à jour non disponible. Version de secours affichée. |
NetworkBanner¶
| Element | Copy |
|---|---|
| Text | ⚠ Connexion instable. Vos choix sont enregistrés. |
9. Component Specifications¶
New components required¶
| Component | File | First needed |
|---|---|---|
BrokerHeader |
frontend/src/customer/components/BrokerHeader.tsx |
V0.0.0 |
ContractRow |
frontend/src/customer/components/ContractRow.tsx |
V0.0.0 |
StickyPayBar |
frontend/src/customer/components/StickyPayBar.tsx |
V0.0.0 |
NetworkBanner |
frontend/src/customer/components/NetworkBanner.tsx |
V0.0.0 |
StepIndicator |
frontend/src/components/ui/StepIndicator.tsx |
V0.0.0 (shared) |
ReceiptCard |
frontend/src/customer/components/ReceiptCard.tsx |
V0.0.1 |
StatusScreen |
frontend/src/customer/components/StatusScreen.tsx |
V0.0.1 (shared wrapper for CP-S5, S6, S7, S8) |
Existing components used¶
| Component | Usage |
|---|---|
Button |
All CTAs: variant="primary"|"secondary"|"ghost", loading, fullWidth, leftIcon |
Checkbox |
ContractRow selection + CP-S2 consent |
Card |
ContractSummaryRow (CP-S2), transaction info cards (CP-S5, S6) |
Toast |
useToast().info() / .error() for transient messages |
Design system components to elevate to HIGH priority before CP implementation¶
| Component | New Priority | Usage |
|---|---|---|
InlineBanner |
HIGH (was MEDIUM) | 8+ screens — notices, warnings, errors, contestation banner |
Skeleton |
HIGH (was MEDIUM) | Every loading state |
Badge |
HIGH | Agrément chip (BrokerHeader), contract count |
10. Compliance UI¶
CIMA Reg. 01-24 + Loi CI 2013-546 mandatory surfaces¶
| Obligation | Article | Screen | Placement | Status |
|---|---|---|---|---|
| Identité du courtier | CIMA arts. 22-23 | All screens | BrokerHeader (sticky) | VÉRIFIÉ |
| Mentions contractuelles avant paiement | CIMA arts. 22-23 | CP-S1 (accordion), CP-S2 (summary) | Per-contract details + summary rows | VÉRIFIÉ (working set, CP-OQ-01 [LEGAL-REVIEW]) |
| Double-clic avant paiement | Loi 2013-546 art. 20 | CP-S2 | Checkbox tick (action 1) + Payer tap (action 2) | VÉRIFIÉ |
| Droit de rétractation 14j AVANT paiement | CIMA art. 24 | CP-S2 | Rétractation block between consent checkbox and Pay button | VÉRIFIÉ |
| Preuve d'acceptation numérique | CIMA + Loi 2013-546 | CP-S2 | Checkbox + AUDIT customer_consent_captured (content_version + ISO timestamp) |
VÉRIFIÉ (CP-OQ-02 [LEGAL-REVIEW]) |
| Contestation / réclamation | CIMA art. 26 | CP-S4, CP-S9 | Contestation InlineBanner on reçu; Section 2 mentions légales | VÉRIFIÉ |
| Identité du courtier + agrément | CIMA arts. 22-23 | CP-S9 | Section 1 mentions légales | VÉRIFIÉ |
| Données personnelles + sous-traitants | Loi CI 2013-546 + ARTCI | CP-S9 | Sections 3 + 4 | VÉRIFIÉ |
| Mentions transferts hors UEMOA | ARTCI + UEMOA | CP-S9 | Section 4 (NOTIF-US-08 canonical block) | VÉRIFIÉ |
Placement rules (NON-NEGOTIABLE)¶
- BrokerHeader on EVERY screen — including CP-S5, CP-S6, CP-S7, CP-S8. Per CP-NFR-13.
- Contestation banner (CP-S4) MUST show the agency phone (
agency.phone) — not the platform number. - Rétractation notice (CP-S2) MUST appear between the consent checkbox and the Pay button — never after.
- "Mentions légales" footer link on CP-S1, CP-S2, CP-S4, CP-S8, CP-S9. May be omitted on CP-S3, CP-S5, CP-S6, CP-S7.
[LEGAL-REVIEW] flags¶
- CP-OQ-01
[LEGAL-REVIEW]— Article-anchored mandatory information set pending qualified lawyer. Working set (D2) deployed per CP-TV-01. Does NOT block V0.0.0–V0.0.2 but must resolve before V1 production. - CP-OQ-02
[LEGAL-REVIEW]— Probative value of checkbox consent. If OTP step-up required: CP-S2 consent block extended with AUTH Level 2 challenge (V2). Does NOT block V0.0.1. - CP-OQ-05
[LEGAL-REVIEW]— Anonymous mandataire (RCCM auth). ReceiptCard usesraison_sociale(not a human name) whencustomer_type = LEGAL_ENTITY. Legal acceptability as CIMA proof-of-payment TBD.
11. V0 Sub-version Implementation Priority¶
| Sub-version | Story | Screens | New components |
|---|---|---|---|
| V0.0.0 | CP-001 | CP-S1 (no payment CTA active), CP-S9, CP-S7 | BrokerHeader, ContractRow, StickyPayBar (disabled), NetworkBanner, StepIndicator, InlineBanner ↑, Skeleton ↑, Badge |
| V0.0.1 | CP-002 | + CP-S2, CP-S3, CP-S4, CP-S5, CP-S6 | ReceiptCard, StatusScreen |
| V0.0.2 | CP-003 | + per-contrat toggle (ContractRow already supports), phone last-4 capture on CP-S3, CP-S8 | Minor ContractRow update |
| V0.0.3 | CP-004 | CP-S2/S3/S4/S5/S6 lazy chunk | Build tooling (Vite code-split), no new components |
| V0.1.0 | CP-005 | Link expiry screen skin (AUTH-owned) | CP-S10 skin only |
12. Wireframe Prototype Generation (Phase 3)¶
UI-bearing screens requiring runnable prototypes (all 5 states + dev state toggle):
| Priority | Screen | Story |
|---|---|---|
| 1st | CP-S1 — Contract List (V0.0.0, no payment) | CP-001 |
| 2nd | CP-S2 — Summary + Consent | CP-002 |
| 2nd | CP-S4 — Reçu | CP-002 |
| 2nd | CP-S5 — Vérification en cours | CP-002 |
| 2nd | CP-S6 — Échec paiement | CP-002 |
| 3rd | CP-S7 — Lien remplacé | CP-003 |
| 3rd | CP-S8 — Déjà réglé | CP-003 |
Prototypes saved to: docs/wireframes/customer-portal/{STORY-ID}/prototype.tsx
End of Customer Portal Design Specification DESIGNER session — 2026-05-28