Aller au contenu

Customer Portal — Design Specification

Module: customer-portal | Prefix: CP | Side: [CUSTOMER] | Date: 2026-05-28

Worktree: docs/customer-portal-design · Branch: docs/customer-portal-design

Legal 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 TENANT logo_object_key signed URL; fallback = initials avatar using --color-primary
  • WhatsApp chip: hidden if tenant.whatsapp_business_phone is 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.

[ ●──────────────○──────────────○ ]
  Choisir         Vérifier       Payer
  • Active node: --color-primary filled circle 12px
  • Inactive node: --color-border circle 12px
  • Passed connector: --color-primary line; pending connector: --color-border line
  • Labels: --font-size-caption --color-text-secondary below each node
  • Hidden entirely on CP-S4 through CP-S11 (terminal / error / edge screens)
  • New component: StepIndicator in frontend/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-warning at 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):

│      Garanties principales                   │
│      Détails non fournis.                    │
│      Demandez à votre conseiller.            │

"Liste mise à jour" notice (R-CP-002)

┌──────────────────────────────────────────────┐
│ ℹ  Certains contrats ne sont plus           │
│    éligibles. Votre liste a été mise à jour.│
└──────────────────────────────────────────────┘
InlineBanner 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]    │
└──────────────────────────────────────────────┘
InlineBanner 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

  1. Tap "Télécharger le reçu" → button loading state (spinner)
  2. CP fetches fresh signed URL from PAY (GET /pay/transactions/{ref}/recus) — 5-min TTL, single-use
  3. Browser navigates to signed URL → download
  4. Button returns to default state
  5. 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°}"


  • 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)

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
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)

  1. BrokerHeader on EVERY screen — including CP-S5, CP-S6, CP-S7, CP-S8. Per CP-NFR-13.
  2. Contestation banner (CP-S4) MUST show the agency phone (agency.phone) — not the platform number.
  3. Rétractation notice (CP-S2) MUST appear between the consent checkbox and the Pay button — never after.
  4. "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.
  • 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 uses raison_sociale (not a human name) when customer_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