Jeko Payment Widget

Intégration pour développeurs front-end
v2.0
📋

Régularisation de vos factures impayées

Vous avez des factures en attente de paiement ? Entrez votre numéro de contrat ci-dessous pour générer le lien de régularisation. Copiez ce lien et envoyez-le à votre client pour qu'il puisse régulariser ses factures impayées.

🔗 Ouvrir

💡 Comment ça marche ? Entrez le numéro de contrat du client, cliquez sur "Générer le lien", puis copiez ou ouvrez le lien. Le client pourra sélectionner ses factures à payer et choisir son moyen de paiement (Wave, Orange Money, Moov, MTN, Djamo, Visa, Mastercard).

Tester les 3 types de paiement — Cliquez sur un scénario pour ouvrir le widget
1️⃣ Souscription

Premier paiement

Après validation d'un nouveau contrat

2️⃣ Avance

Paiement anticipé

Renseignez l'identifiant du contrat

3️⃣ Régularisation

Récupération d'impayés

Renseignez l'identifiant du contrat et Sélectionnez les factures à régulariser

🧪 Factures pré-sélectionnées pour la régularisation :

💡 Ces factures seront automatiquement pré-sélectionnées dans le widget

Intégration simple dans une page HTML classique avec les 3 types de paiement. Chargez le script du widget et initialisez-le avec vos paramètres.

📖 Documentation d'intégration

🎯 Support multi-providers : Ce widget supporte Wave et Jeko selon le paymentMethod sélectionné.

1. Inclure le widget JS :

<script src="{{ url('/api/paiements/jeko-payment-widget.js') }}"></script>

2. Initialiser le widget :

const widget = new JekoWidget({
    // Endpoint pour initialiser le paiement (unifié Wave + Jeko)
    backendEndpoint: "{{ url('/api/paiements/init') }}",

    // Endpoint pour vérifier le contrat
    contractCheckEndpoint: "{{ url('/api/paiements/contrat/verifier') }}",

    // Endpoint pour vérifier le statut du paiement (polling automatique)
    statusCheckEndpoint: "{{ url('/api/paiements/statut') }}",

    // Devise par défaut (XOF, XAF, USD, EUR)
    currency: "XOF",

    // Timeout de la requête en millisecondes
    timeout: 30000,

    // Vérification automatique du contrat si contractId fourni
    autoVerifyContract: true,

    // Configuration du polling du statut (optionnel)
    statusPollInterval: 3000,          // Intervalle de polling en ms (défaut: 3000)
    statusPollMaxAttempts: 20,         // Nombre max de tentatives (défaut: 20)

    // Headers personnalisés (CSRF, etc.)
    headers: {
        "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]')?.content || "",
        "Accept": "application/json",
    },

    // Callbacks pour gérer les événements
    callbacks: {
        onSuccess: (redirectUrl, data) => {
            console.log("✅ Paiement initialisé", { redirectUrl, data });
            // Le widget ouvre automatiquement la page de paiement
            // et effectue le polling du statut
        },
        onError: (message, data) => {
            console.error("❌ Erreur de paiement", { message, data });
            alert("Erreur: " + message);
        },
        onOpen: (data) => console.log("🔄 Widget ouvert", data),
        onClose: () => console.log("❌ Widget fermé"),
    },

    // Personnalisation du thème (optionnel)
    theme: {
        primary: "#1D603D",
        primaryDark: "#0B482F",
        accent: "#E09518",
        radius: "16px",
        fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
        maxWidth: "550px",
    },

    // Traductions personnalisées (optionnel)
    translations: {
        title: "Paiement sécurisé",
        close: "Fermer",
        back: "Retour",
        stepContract: "Contrat",
        stepMethod: "Paiement",
        stepSummary: "Résumé",
        chooseMethod: "Choisissez un moyen de paiement",
        confirmAndPay: "Confirmer et payer",
        processing: "Initialisation du paiement en cours...",
        success: "Paiement initialisé",
        error: "Échec du paiement",
        retry: "Réessayer",
        networkError: "Erreur réseau",
        requiredField: "Ce champ est requis",
        contractIdLabel: "Identifiant du contrat",
        contractIdPlaceholder: "Ex: 345678",
        verify: "Vérifier le contrat",
        verifying: "Vérification en cours...",
        loadingContract: "Chargement des informations du contrat...",
        // Traductions pour le polling du statut
        polling: "Vérification du statut du paiement en cours...",
        pollingPending: "Paiement en attente...",
        pollingSuccess: "Paiement réussi !",
        pollingError: "Le paiement a échoué",
        pollingTimeout: "Délai d'attente dépassé",
        pollingRestart: "Vérifier à nouveau",
    },
});

3. Moyens de paiement supportés :

💳 Wave : paymentMethod: "wave" → API Wave

📱 Mobile Money (Jeko) :

  • paymentMethod: "orange" → Orange Money
  • paymentMethod: "moov" → Moov Money
  • paymentMethod: "mtn" → MTN Mobile Money
  • paymentMethod: "djamo" → Djamo

💳 Cartes (Jeko) :

  • paymentMethod: "visa" → Visa
  • paymentMethod: "mastercard" → Mastercard

3. Polling automatique du statut :

🔄 Le widget effectue automatiquement le polling du statut du paiement :

  • Après l'initialisation, le widget ouvre la page Jeko dans un nouvel onglet
  • Le widget vérifie automatiquement le statut toutes les 3 secondes (configurable)
  • Statuts possibles : pending, success, error
  • Si pending : le widget continue le polling avec un spinner
  • Si success : le webhook met à jour le statut en base, le widget affiche "Paiement réussi !"
  • Si error : le widget met à jour le statut en base et affiche une erreur
  • Après 20 tentatives (configurable) : bouton "Vérifier à nouveau" pour relancer le polling

4. Vérifier manuellement le statut (optionnel) :

// Interroger le statut d'un paiement via l'API
// Endpoint: GET /api/paiements/statut/{referenceInterne}
async function checkPaymentStatus(referenceInterne) {
    const response = await fetch(`{{ BASE_URL }}/api/paiements/statut/${referenceInterne}`);
    const data = await response.json();

    if (data.success) {
        console.log("Statut du paiement:", data.data.statut);
        console.log("Montant:", data.data.montant);
        console.log("Détails:", data.data.details);
        return data.data;
    } else {
        console.error("Erreur:", data.message);
        return null;
    }
}

// Exemple d'utilisation
checkPaymentStatus("PAI-20261007095526-1791366926864-b1v5kukn-1782");

5. Générer une référence unique :

function generateReference() {
    const now = new Date();
    const timestamp = now.getTime();
    const random = Math.random().toString(36).substring(2, 10);
    const code = String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0");
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, "0");
    const day = String(now.getDate()).padStart(2, "0");
    const hours = String(now.getHours()).padStart(2, "0");
    const minutes = String(now.getMinutes()).padStart(2, "0");
    const seconds = String(now.getSeconds()).padStart(2, "0");
    // Format: PAI-YYYYMMDDHHMMSS-TIMESTAMP-RANDOM-CODE
    return `PAI-${year}${month}${day}${hours}${minutes}${seconds}-${timestamp}-${random}-${code}`;
}

6. Récupérer les factures pré-sélectionnées :

function getPreselectedInvoices() {
    var ids = [];
    document.querySelectorAll('input[name="invoice_ids"]:checked').forEach(function(el) {
        ids.push(el.value);
    });
    return ids;
}
HTML
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <title>Jeko Payment Widget</title>
</head>
<body>
    <div class="container">
        <h1>💳 Paiement sécurisé</h1>
        <p>Démo des 3 types de paiement — Jeko Widget</p>

        <!-- ============================================ -->
        <!-- 1) PREMIER PAIEMENT (firstPayment) -->
        <!-- ============================================ -->
        <div class="scenario">
            <span class="badge">1️⃣ Souscription</span>
            <h3>Premier paiement</h3>
            <p>Après validation d'un nouveau contrat. La vérification est automatique.</p>
            <input type="hidden" id="contractIdFirstPayment" value="1093" />
            <button class="btn-pay" id="btnFirstPayment">Payer la première prime</button>
        </div>

        <!-- ============================================ -->
        <!-- 2) PAIEMENT ANTICIPÉ (earlyPayment) -->
        <!-- ============================================ -->
        <div class="scenario">
            <span class="badge">2️⃣ Avance</span>
            <h3>Paiement anticipé</h3>
            <p>Le client renseigne son identifiant de contrat. La vérification est automatique.</p>
            <div class="field">
                <label for="contractIdEarly">Identifiant de contrat</label>
                <input type="text" id="contractIdEarly" placeholder="Ex: 12452" value="12452" />
            </div>
            <button class="btn-pay" id="btnEarlyPayment">Payer en avance</button>
        </div>

        <!-- ============================================ -->
        <!-- 3) RÉGULARISATION (recoveryPrime) -->
        <!-- ============================================ -->
        <div class="scenario">
            <span class="badge">3️⃣ Régularisation</span>
            <h3>Récupération de primes impayées</h3>
            <p>Le client sélectionne les factures en attente. Pré-sélection possible.</p>
            <div class="field">
                <label for="contractIdRecovery">Identifiant de contrat</label>
                <input type="text" id="contractIdRecovery" placeholder="Ex: 12452" value="12452" />
            </div>
            <div style="margin: 12px 0; padding: 12px; background: #f8faf9; border-radius: 8px;">
                <p style="margin: 0 0 8px; font-size: 13px; font-weight: 600;">🧪 Pré-sélection de factures :</p>
                <label style="display: inline-block; margin-right: 16px;">
                    <input type="checkbox" name="invoice_ids" value="173669" checked /> Facture #173669
                </label>
                <label style="display: inline-block; margin-right: 16px;">
                    <input type="checkbox" name="invoice_ids" value="3073532" /> Facture #3073532
                </label>
                <label style="display: inline-block;">
                    <input type="checkbox" name="invoice_ids" value="3132514" checked /> Facture #3132514
                </label>
            </div>
            <button class="btn-pay" id="btnRecoveryPrime">Régulariser mes impayés</button>
        </div>
    </div>

    <!-- ============================================ -->
    <!-- CHARGEMENT DU WIDGET -->
    <!-- ============================================ -->
    <script src="{{ url('/api/paiements/jeko-payment-widget.js') }}"></script>

    <script>
        (function() {
            "use strict";

            // ============================================================
            // VÉRIFICATION DU CHARGEMENT
            // ============================================================
            if (typeof JekoWidget === "undefined") {
                console.error("❌ JekoWidget non chargé");
                document.querySelectorAll(".btn-pay").forEach(function(btn) {
                    btn.disabled = true;
                    btn.textContent = "⚠️ Service indisponible";
                });
                return;
            }

            console.log("✅ JekoWidget chargé avec succès");

            // ============================================================
            // INITIALISATION DU WIDGET
            // ============================================================
            var widget = new JekoWidget({
                // --- Configuration des endpoints ---
                backendEndpoint: "{{ BASE_URL }}/api/paiements/init",
                contractCheckEndpoint: "{{ BASE_URL }}/api/paiements/contrat/verifier",
                statusCheckEndpoint: "{{ BASE_URL }}/api/paiements/statut",

                // --- Configuration générale ---
                currency: "XOF",
                timeout: 30000,
                autoVerifyContract: true,

                // --- Configuration du polling du statut ---
                statusPollInterval: 3000,          // Intervalle de polling en ms (défaut: 3000)
                statusPollMaxAttempts: 20,         // Nombre max de tentatives (défaut: 20)

                // --- Headers ---
                headers: {
                    "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]')?.content || "",
                    "Accept": "application/json",
                },

                // --- Callbacks ---
                callbacks: {
                    onSuccess: function(redirectUrl, data) {
                        console.log("✅ Paiement initialisé", { redirectUrl, data });
                        // Le widget ouvre automatiquement la page de paiement
                        // et effectue le polling du statut
                    },
                    onError: function(message, data) {
                        console.error("❌ Erreur de paiement", { message, data });
                        alert("Erreur: " + message);
                    },
                    onOpen: function(data) {
                        console.log("🔄 Widget ouvert", data);
                    },
                    onClose: function() {
                        console.log("❌ Widget fermé");
                    },
                },

                // --- Thème personnalisé ---
                theme: {
                    primary: "#1D603D",
                    primaryDark: "#0B482F",
                    accent: "#E09518",
                    radius: "16px",
                    fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
                    maxWidth: "550px",
                },

                // --- Traductions ---
                translations: {
                    title: "Paiement sécurisé",
                    close: "Fermer",
                    back: "Retour",
                    stepContract: "Contrat",
                    stepMethod: "Paiement",
                    stepSummary: "Résumé",
                    chooseMethod: "Choisissez un moyen de paiement",
                    confirmAndPay: "Confirmer et payer",
                    processing: "Initialisation du paiement en cours...",
                    success: "Paiement initialisé",
                    error: "Échec du paiement",
                    retry: "Réessayer",
                    networkError: "Erreur réseau",
                    requiredField: "Ce champ est requis",
                    contractIdLabel: "Identifiant du contrat",
                    contractIdPlaceholder: "Ex: 345678",
                    verify: "Vérifier le contrat",
                    verifying: "Vérification en cours...",
                    loadingContract: "Chargement des informations du contrat...",
                    // Traductions pour le polling du statut
                    polling: "Vérification du statut du paiement en cours...",
                    pollingPending: "Paiement en attente...",
                    pollingSuccess: "Paiement réussi !",
                    pollingError: "Le paiement a échoué",
                    pollingTimeout: "Délai d'attente dépassé",
                    pollingRestart: "Vérifier à nouveau",
                },
            });

            // ============================================================
            // FONCTIONS UTILITAIRES
            // ============================================================

            /**
             * Génère une référence unique pour le paiement
             * Format: PAI-YYYYMMDDHHMMSS-TIMESTAMP-RANDOM-CODE
             * @returns {string} Référence unique
             */
            function generateReference() {
                var now = new Date();
                var timestamp = now.getTime();
                var random = Math.random().toString(36).substring(2, 10);
                var code = String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0");
                var year = now.getFullYear();
                var month = String(now.getMonth() + 1).padStart(2, "0");
                var day = String(now.getDate()).padStart(2, "0");
                var hours = String(now.getHours()).padStart(2, "0");
                var minutes = String(now.getMinutes()).padStart(2, "0");
                var seconds = String(now.getSeconds()).padStart(2, "0");
                return "PAI-" + year + month + day + hours + minutes + seconds + "-" + timestamp + "-" + random + "-" + code;
            }

            /**
             * Récupère les factures pré-sélectionnées par l'utilisateur
             * @returns {string[]} Tableau des IDs de factures
             */
            function getPreselectedInvoices() {
                var ids = [];
                document.querySelectorAll('input[name="invoice_ids"]:checked').forEach(function(el) {
                    ids.push(el.value);
                });
                return ids;
            }

            /**
             * Met à jour le message d'état d'un scénario
             * @param {string} elementId - ID de l'élément de statut
             * @param {string} message - Message à afficher
             * @param {string} type - Type de message (info, success, error)
             */
            function updateStatus(elementId, message, type) {
                var el = document.getElementById(elementId);
                if (!el) return;
                el.textContent = message;
                el.className = "status-msg visible";
                if (type === "error") {
                    el.style.background = "#fef2f2";
                    el.style.color = "#991b1b";
                } else if (type === "success") {
                    el.style.background = "#f0fdf4";
                    el.style.color = "#166534";
                } else {
                    el.style.background = "#f3f4f6";
                    el.style.color = "#6b7280";
                }
            }

            // ============================================================
            // 1) PREMIER PAIEMENT (firstPayment)
            // ============================================================
            /**
             * Scénario: Premier paiement d'un contrat
             * - Utilisé lors de la souscription d'un nouveau contrat
             * - Le contractId est optionnel (peut être récupéré automatiquement)
             * - La prime principale et les frais d'adhésion sont calculés côté serveur
             */
            document.getElementById("btnFirstPayment").addEventListener("click", function() {
                var contractId = document.getElementById("contractIdFirstPayment").value;
                console.log("🔹 Premier paiement initié", { contractId: contractId });

                widget.open({
                    // Référence unique du paiement
                    reference: generateReference(),
                    
                    // Type de paiement: firstPayment, earlyPayment, recoveryPrime
                    paymentType: "firstPayment",
                    
                    // Identifiant du contrat (optionnel pour firstPayment)
                    contractId: contractId || undefined,
                    
                    // Description du paiement (affichée dans le widget)
                    description: "Souscription — première prime",
                    
                    // Email du client (pour les notifications)
                    customerEmail: "client@example.com",
                    
                    // Nom du client
                    customerName: "Jean Dupont",
                    
                    // URL de redirection en cas de succès
                    successUrl: window.location.origin + "/paiement/recu",
                    
                    // URL de redirection en cas d'erreur
                    errorUrl: window.location.origin + "/paiement/error",
                    
                    // Métadonnées additionnelles (pour le suivi)
                    metadata: {
                        source: "web_demo",
                        scenario: "firstPayment",
                        timestamp: new Date().toISOString(),
                    },
                });
            });

            // ============================================================
            // 2) PAIEMENT ANTICIPÉ (earlyPayment)
            // ============================================================
            /**
             * Scénario: Paiement anticipé des primes
             * - L'utilisateur renseigne l'identifiant du contrat
             * - Le widget vérifie automatiquement le contrat
             * - Vérifie qu'il n'y a pas d'impayés avant de permettre le paiement anticipé
             */
            document.getElementById("btnEarlyPayment").addEventListener("click", function() {
                var contractId = document.getElementById("contractIdEarly").value.trim();
                if (!contractId) {
                    alert("⚠️ Veuillez saisir un identifiant de contrat.");
                    return;
                }
                console.log("🔹 Paiement anticipé initié", { contractId: contractId });
                updateStatus("earlyStatus", "⏳ Vérification automatique du contrat en cours...", "info");

                widget.open({
                    reference: generateReference(),
                    paymentType: "earlyPayment",
                    contractId: contractId,
                    description: "Paiement anticipé de primes",
                    customerEmail: "client@example.com",
                    customerName: "Jean Dupont",
                    successUrl: window.location.origin + "/paiement/recu",
                    errorUrl: window.location.origin + "/paiement/error",
                    metadata: {
                        source: "web_demo",
                        scenario: "earlyPayment",
                        contractId: contractId,
                    },
                });
            });

            // ============================================================
            // 3) RÉGULARISATION (recoveryPrime)
            // ============================================================
            /**
             * Scénario: Régularisation de primes impayées
             * - L'utilisateur renseigne l'identifiant du contrat
             * - Le widget liste les factures en attente
             * - L'utilisateur sélectionne les factures à régulariser
             * - Possibilité de pré-sélectionner des factures
             */
            document.getElementById("btnRecoveryPrime").addEventListener("click", function() {
                var contractId = document.getElementById("contractIdRecovery").value.trim();
                var preselectedIds = getPreselectedInvoices();

                if (!contractId) {
                    alert("⚠️ Veuillez saisir un identifiant de contrat.");
                    return;
                }

                console.log("🔹 Régularisation initiée", {
                    contractId: contractId,
                    preselectedCount: preselectedIds.length,
                    preselectedIds: preselectedIds,
                });

                updateStatus(
                    "recoveryStatus",
                    "⏳ Vérification automatique du contrat avec " + preselectedIds.length + " facture(s) pré-sélectionnée(s)...",
                    "info"
                );

                widget.open({
                    reference: generateReference(),
                    paymentType: "recoveryPrime",
                    contractId: contractId,
                    preselectedInvoiceIds: preselectedIds.length > 0 ? preselectedIds : undefined,
                    description: "Régularisation de primes impayées",
                    customerEmail: "client@example.com",
                    customerName: "Jean Dupont",
                    successUrl: window.location.origin + "/paiement/recu",
                    errorUrl: window.location.origin + "/paiement/error",
                    metadata: {
                        source: "web_demo",
                        scenario: "recoveryPrime",
                        contractId: contractId,
                        preselectedCount: preselectedIds.length,
                    },
                });
            });

            console.log("✅ Widget Jeko initialisé avec succès");

        })();
    </script>

</body>
</html>
📌 Récapitulatif des 3 types :
firstPayment : Premier paiement d'un contrat (souscription) — contractId optionnel
earlyPayment : Paiement anticipé des primes — contractId obligatoire
recoveryPrime : Régularisation de primes impayées — contractId obligatoire + preselectedInvoiceIds optionnel
🔧 Paramètres de configuration du widget :
backendEndpoint : URL pour initialiser le paiement (obligatoire)
contractCheckEndpoint : URL pour vérifier le contrat (obligatoire)
currency : Devise (XOF, XAF, USD, EUR) (défaut: XOF)
timeout : Timeout de la requête en ms (défaut: 30000)
autoVerifyContract : Vérification automatique du contrat (défaut: true)
headers : Headers personnalisés (CSRF, etc.) (optionnel)
callbacks : Callbacks (onSuccess, onError, onOpen, onClose) (optionnel)
theme : Personnalisation du thème (optionnel)
translations : Traductions personnalisées (optionnel)

Composant React réutilisable avec les 3 types de paiement. Gestion d'état, chargement dynamique et callbacks personnalisés.

⚛️ Documentation d'intégration React

🎯 Support multi-providers : Le widget route automatiquement vers Wave ou Jeko selon le paymentMethod.

1. Installation du composant :

// Copier le composant JekoPaymentButton dans votre projet
// Fichier: components/JekoPaymentButton.tsx

2. Variables d'environnement (.env.local) :

REACT_APP_PAYMENT_API_URL={{ BASE_URL }}/api/paiements/init
REACT_APP_CONTRACT_CHECK_URL={{ BASE_URL }}/api/paiements/contrat/verifier
REACT_APP_STATUS_CHECK_URL={{ BASE_URL }}/api/paiements/statut

3. Utilisation du composant :

// 1) Premier paiement
<JekoPaymentButton contractId="1093" paymentType="firstPayment" />

// 2) Paiement anticipé
<JekoPaymentButton contractId="12452" paymentType="earlyPayment" />

// 3) Régularisation avec pré-sélection
<JekoPaymentButton 
    contractId="12452" 
    paymentType="recoveryPrime" 
    preselectedInvoiceIds={['173669', '3132514']} 
/>

// 4) Paiement Wave (spécifier paymentMethod)
<JekoPaymentButton 
    contractId="12452" 
    paymentType="earlyPayment" 
    paymentMethod="wave"
/>
React
import React, { useState, useEffect, useRef } from "react";

// ============================================================
// TYPES
// ============================================================
type PaymentType = "firstPayment" | "earlyPayment" | "recoveryPrime";

interface JekoPaymentButtonProps {
    // Identifiant du contrat (obligatoire pour earlyPayment et recoveryPrime)
    contractId: string;
    // Type de paiement: firstPayment | earlyPayment | recoveryPrime
    paymentType?: PaymentType;
    // Email du client
    customerEmail?: string;
    // Nom du client
    customerName?: string;
    // Factures pré-sélectionnées (uniquement pour recoveryPrime)
    preselectedInvoiceIds?: string[];
    // Callback en cas de succès
    onSuccess?: (redirectUrl: string, data: any) => void;
    // Callback en cas d'erreur
    onError?: (message: string, data?: any) => void;
    // Classe CSS personnalisée
    className?: string;
    // Contenu du bouton
    children?: React.ReactNode;
}

// ============================================================
// CHARGEMENT DYNAMIQUE DU WIDGET
// ============================================================
const loadJekoWidget = (): Promise<any> => {
    return new Promise((resolve) => {
        if (window.JekoWidget) {
            resolve(window.JekoWidget);
            return;
        }
        const script = document.createElement("script");
        script.src = "{{ BASE_URL }}/api/paiements-payment-widget.js";
        script.onload = () => resolve(window.JekoWidget);
        script.onerror = () => resolve(null);
        document.head.appendChild(script);
    });
};

// ============================================================
// COMPOSANT PRINCIPAL
// ============================================================
const JekoPaymentButton: React.FC<JekoPaymentButtonProps> = ({
    contractId,
    paymentType = "firstPayment",
    customerEmail = "",
    customerName = "",
    preselectedInvoiceIds = [],
    onSuccess,
    onError,
    className = "",
    children,
}) => {
    const [isLoading, setIsLoading] = useState(false);
    const [isReady, setIsReady] = useState(false);
    const widgetRef = useRef<any>(null);

    useEffect(() => {
        const initWidget = async () => {
            try {
                const JekoWidget = await loadJekoWidget();
                if (!JekoWidget) {
                    console.error("❌ JekoWidget non chargé");
                    return;
                }

                widgetRef.current = new JekoWidget({
                    // Endpoint pour initialiser le paiement
                    backendEndpoint: process.env.REACT_APP_JEKO_API_URL || "{{ BASE_URL }}/api/paiements/init",
                    // Endpoint pour vérifier le contrat
                    contractCheckEndpoint: process.env.REACT_APP_JEKO_CONTRACT_CHECK_URL || "{{ BASE_URL }}/api/paiements/contrat/verifier",
                    // Devise par défaut
                    currency: "XOF",
                    // Timeout de la requête (ms)
                    timeout: 30000,
                    // Vérification automatique du contrat si contractId fourni
                    autoVerifyContract: true,
                    // Headers personnalisés
                    headers: {
                        "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]')?.content || "",
                    },
                    // Callbacks
                    callbacks: {
                        onSuccess: (redirectUrl: string, data: any) => {
                            setIsLoading(false);
                            onSuccess?.(redirectUrl, data);
                            window.open(redirectUrl, "_blank");
                        },
                        onError: (message: string, data: any) => {
                            setIsLoading(false);
                            onError?.(message, data);
                        },
                        onOpen: () => setIsLoading(true),
                        onClose: () => setIsLoading(false),
                    },
                    // Personnalisation du thème (optionnel)
                    theme: {
                        primary: "#1D603D",
                        primaryDark: "#0B482F",
                        accent: "#E09518",
                        radius: "16px",
                    },
                    // Traductions personnalisées (optionnel)
                    translations: {
                        title: "Paiement sécurisé",
                        close: "Fermer",
                        back: "Retour",
                        stepContract: "Contrat",
                        stepMethod: "Paiement",
                        stepSummary: "Résumé",
                        chooseMethod: "Choisissez un moyen de paiement",
                        confirmAndPay: "Confirmer et payer",
                        processing: "Initialisation du paiement en cours...",
                    },
                });
                setIsReady(true);
            } catch (error) {
                console.error("❌ Erreur chargement widget Jeko:", error);
                onError?.(error instanceof Error ? error.message : "Erreur inconnue");
            }
        };
        initWidget();
    }, [onSuccess, onError]);

    /**
     * Génère une référence unique pour le paiement
     * Format: PAI-YYYYMMDDHHMMSS-TIMESTAMP-RANDOM-CODE
     */
    const generateReference = (): string => {
        const now = new Date();
        const timestamp = now.getTime();
        const random = Math.random().toString(36).substring(2, 10);
        const code = String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0");
        const date = now.getFullYear() +
            String(now.getMonth() + 1).padStart(2, "0") +
            String(now.getDate()).padStart(2, "0") +
            String(now.getHours()).padStart(2, "0") +
            String(now.getMinutes()).padStart(2, "0") +
            String(now.getSeconds()).padStart(2, "0");
        return `PAI-${date}-${timestamp}-${random}-${code}`;
    };

    const handlePayment = () => {
        if (!widgetRef.current || !isReady) return;

        const reference = generateReference();

        // Construction des données selon le type de paiement
        const paymentData: any = {
            reference,
            paymentType,
            contractId,
            description: `Paiement via Jeko - ${paymentType}`,
            customerEmail,
            customerName,
            successUrl: window.location.origin + "/paiements/jeko/success",
            errorUrl: window.location.origin + "/paiements/jeko/error",
            metadata: {
                source: "react_app",
                timestamp: new Date().toISOString(),
            },
        };

        // Ajouter les factures pré-sélectionnées pour recoveryPrime
        if (paymentType === "recoveryPrime" && preselectedInvoiceIds.length > 0) {
            paymentData.preselectedInvoiceIds = preselectedInvoiceIds;
        }

        widgetRef.current.open(paymentData);
    };

    return (
        <button
            onClick={handlePayment}
            disabled={!isReady || isLoading}
            className={`btn-pay ${className}`}
            style={{
                background: "#1D603D",
                color: "#fff",
                border: "none",
                padding: "13px 24px",
                borderRadius: "10px",
                fontWeight: 600,
                fontSize: "15px",
                cursor: "pointer",
                width: "100%",
                transition: "all 0.2s",
                opacity: (!isReady || isLoading) ? 0.5 : 1,
            }}
        >
            {isLoading ? "Chargement..." : (children || "Payer avec Jeko")}
        </button>
    );
};

export default JekoPaymentButton;

// ============================================================
// EXEMPLES D'UTILISATION DES 3 TYPES
// ============================================================

/*
// 1) PREMIER PAIEMENT (firstPayment)
<JekoPaymentButton
    contractId="1093"
    paymentType="firstPayment"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    onSuccess={(url) => console.log("Succès", url)}
    onError={(msg) => console.error("Erreur", msg)}
>
    Payer la première prime
</JekoPaymentButton>

// 2) PAIEMENT ANTICIPÉ (earlyPayment)
<JekoPaymentButton
    contractId="12452"
    paymentType="earlyPayment"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    onSuccess={(url) => console.log("Succès", url)}
    onError={(msg) => console.error("Erreur", msg)}
>
    Payer en avance
</JekoPaymentButton>

// 3) RÉGULARISATION (recoveryPrime) avec pré-sélection
<JekoPaymentButton
    contractId="12452"
    paymentType="recoveryPrime"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    preselectedInvoiceIds={['173669', '3132514']}
    onSuccess={(url) => console.log("Succès", url)}
    onError={(msg) => console.error("Erreur", msg)}
>
    Régulariser mes impayés
</JekoPaymentButton>
*/
⚛️ Exemples d'utilisation des 3 types :
firstPayment : <JekoPaymentButton contractId="1093" paymentType="firstPayment" />
earlyPayment : <JekoPaymentButton contractId="12452" paymentType="earlyPayment" />
recoveryPrime : <JekoPaymentButton contractId="12452" paymentType="recoveryPrime" preselectedInvoiceIds={['173669', '3132514']} />
🔧 Props du composant React :
contractId : Identifiant du contrat (obligatoire)
paymentType : Type de paiement (firstPayment, earlyPayment, recoveryPrime) (défaut: firstPayment)
customerEmail : Email du client (optionnel)
customerName : Nom du client (optionnel)
preselectedInvoiceIds : Factures pré-sélectionnées (optionnel, uniquement pour recoveryPrime)
onSuccess : Callback en cas de succès (optionnel)
onError : Callback en cas d'erreur (optionnel)
className : Classe CSS personnalisée (optionnel)
children : Contenu du bouton (optionnel)

Composant Vue.js avec les 3 types de paiement. Gestion d'état réactive, événements personnalisés et props typées.

🟢 Documentation d'intégration Vue.js

🎯 Support multi-providers : Le widget route automatiquement vers Wave ou Jeko selon le paymentMethod.

1. Créer le composant :

// Fichier: components/JekoPaymentButton.vue
// Copier le code du composant ci-dessous

2. Variables d'environnement (.env) :

VUE_APP_PAYMENT_API_URL={{ BASE_URL }}/api/paiements/init
VUE_APP_CONTRACT_CHECK_URL={{ BASE_URL }}/api/paiements/contrat/verifier
VUE_APP_STATUS_CHECK_URL={{ BASE_URL }}/api/paiements/statut

3. Utilisation du composant :

// 1) Premier paiement
<JekoPaymentButton contract-id="1093" payment-type="firstPayment" />

// 2) Paiement anticipé
<JekoPaymentButton contract-id="12452" payment-type="earlyPayment" />

// 3) Régularisation avec pré-sélection
<JekoPaymentButton 
    contract-id="12452" 
    payment-type="recoveryPrime" 
    :preselected-invoice-ids="['173669', '3132514']" 
/>

// 4) Paiement Wave (spécifier paymentMethod)
<JekoPaymentButton 
    contract-id="12452" 
    payment-type="earlyPayment" 
    payment-method="wave"
/>
// 3) Régularisation avec pré-sélection <JekoPaymentButton contract-id="12452" payment-type="recoveryPrime" :preselected-invoice-ids="['173669', '3132514']" />
Vue.js
<template>
    <div>
        <!-- 1) PREMIER PAIEMENT -->
        <JekoPaymentButton
            contract-id="1093"
            payment-type="firstPayment"
            customer-email="client@example.com"
            customer-name="Jean Dupont"
            label="Payer la première prime"
            @success="handleSuccess"
            @error="handleError"
        />

        <!-- 2) PAIEMENT ANTICIPÉ -->
        <JekoPaymentButton
            :contract-id="earlyContractId"
            payment-type="earlyPayment"
            customer-email="client@example.com"
            customer-name="Jean Dupont"
            label="Payer en avance"
            @success="handleSuccess"
            @error="handleError"
        />

        <!-- 3) RÉGULARISATION avec pré-sélection -->
        <JekoPaymentButton
            :contract-id="recoveryContractId"
            payment-type="recoveryPrime"
            :preselected-invoice-ids="['173669', '3132514']"
            customer-email="client@example.com"
            customer-name="Jean Dupont"
            label="Régulariser mes impayés"
            @success="handleSuccess"
            @error="handleError"
        />
    </div>
</template>

<script>
import JekoPaymentButton from "./components/JekoPaymentButton.vue";

export default {
    name: "PaymentPage",
    components: { JekoPaymentButton },
    data() {
        return {
            earlyContractId: "12452",
            recoveryContractId: "12452",
        };
    },
    methods: {
        handleSuccess(redirectUrl, data) {
            console.log("✅ Paiement réussi", { redirectUrl, data });
        },
        handleError(message, data) {
            console.error("❌ Erreur de paiement", { message, data });
        },
    },
};
</script>

<!-- ============================================================ -->
<!-- COMPOSANT JekoPaymentButton.vue -->
<!-- ============================================================ -->

<template>
    <button
        @click="handlePayment"
        :disabled="!isReady || isLoading"
        class="btn-pay"
    >
        {{ isLoading ? "Chargement..." : label }}
    </button>
</template>

<script>
export default {
    name: "JekoPaymentButton",
    props: {
        // Identifiant du contrat (obligatoire)
        contractId: {
            type: String,
            required: true,
        },
        // Type de paiement: firstPayment | earlyPayment | recoveryPrime
        paymentType: {
            type: String,
            default: "firstPayment",
            validator: function(value) {
                return ["firstPayment", "earlyPayment", "recoveryPrime"].includes(value);
            },
        },
        // Email du client
        customerEmail: {
            type: String,
            default: "",
        },
        // Nom du client
        customerName: {
            type: String,
            default: "",
        },
        // Factures pré-sélectionnées (uniquement pour recoveryPrime)
        preselectedInvoiceIds: {
            type: Array,
            default: () => [],
        },
        // Libellé du bouton
        label: {
            type: String,
            default: "Payer avec Jeko",
        },
    },
    data() {
        return {
            widget: null,
            isReady: false,
            isLoading: false,
        };
    },
    mounted() {
        this.loadWidget();
    },
    methods: {
        /**
         * Charge le widget Jeko dynamiquement
         */
        loadWidget() {
            if (window.JekoWidget) {
                this.initWidget();
                return;
            }
            const script = document.createElement("script");
            script.src = process.env.VUE_APP_JEKO_WIDGET_URL || "{{ BASE_URL }}/api/paiements-payment-widget.js";
            script.onload = this.initWidget;
            script.onerror = () => {
                console.error("❌ Erreur chargement du widget Jeko");
                this.$emit("error", "Impossible de charger le widget");
            };
            document.head.appendChild(script);
        },

        /**
         * Initialise le widget avec la configuration
         */
        initWidget() {
            this.widget = new JekoWidget({
                // Endpoint pour initialiser le paiement
                backendEndpoint: process.env.VUE_APP_JEKO_API_URL || "{{ BASE_URL }}/api/paiements/init",
                // Endpoint pour vérifier le contrat
                contractCheckEndpoint: process.env.VUE_APP_JEKO_CONTRACT_CHECK_URL || "{{ BASE_URL }}/api/paiements/contrat/verifier",
                // Devise par défaut
                currency: "XOF",
                // Timeout de la requête (ms)
                timeout: 30000,
                // Vérification automatique du contrat si contractId fourni
                autoVerifyContract: true,
                // Headers personnalisés
                headers: {
                    "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]')?.content || "",
                },
                // Callbacks
                callbacks: {
                    onSuccess: (redirectUrl, data) => {
                        this.isLoading = false;
                        this.$emit("success", redirectUrl, data);
                        window.open(redirectUrl, "_blank");
                    },
                    onError: (message, data) => {
                        this.isLoading = false;
                        this.$emit("error", message, data);
                    },
                    onOpen: () => {
                        this.isLoading = true;
                    },
                    onClose: () => {
                        this.isLoading = false;
                    },
                },
                // Personnalisation du thème (optionnel)
                theme: {
                    primary: "#1D603D",
                    primaryDark: "#0B482F",
                    accent: "#E09518",
                    radius: "16px",
                    fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
                    maxWidth: "550px",
                },
                // Traductions personnalisées (optionnel)
                translations: {
                    title: "Paiement sécurisé",
                    close: "Fermer",
                    back: "Retour",
                    stepContract: "Contrat",
                    stepMethod: "Paiement",
                    stepSummary: "Résumé",
                    chooseMethod: "Choisissez un moyen de paiement",
                    confirmAndPay: "Confirmer et payer",
                    processing: "Initialisation du paiement en cours...",
                    success: "Paiement initialisé",
                    error: "Échec du paiement",
                    retry: "Réessayer",
                    networkError: "Erreur réseau",
                    requiredField: "Ce champ est requis",
                    contractIdLabel: "Identifiant du contrat",
                    contractIdPlaceholder: "Ex: 345678",
                    verify: "Vérifier le contrat",
                    verifying: "Vérification en cours...",
                    loadingContract: "Chargement des informations du contrat...",
                },
            });
            this.isReady = true;
        },

        /**
         * Génère une référence unique pour le paiement
         * Format: PAI-YYYYMMDDHHMMSS-TIMESTAMP-RANDOM-CODE
         */
        generateReference() {
            const now = new Date();
            const timestamp = now.getTime();
            const random = Math.random().toString(36).substring(2, 10);
            const code = String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0");
            const year = now.getFullYear();
            const month = String(now.getMonth() + 1).padStart(2, "0");
            const day = String(now.getDate()).padStart(2, "0");
            const hours = String(now.getHours()).padStart(2, "0");
            const minutes = String(now.getMinutes()).padStart(2, "0");
            const seconds = String(now.getSeconds()).padStart(2, "0");
            return `PAI-${year}${month}${day}${hours}${minutes}${seconds}-${timestamp}-${random}-${code}`;
        },

        /**
         * Gère le clic sur le bouton de paiement
         */
        handlePayment() {
            if (!this.widget || !this.isReady) return;

            const reference = this.generateReference();

            // Construction des données selon le type de paiement
            const paymentData = {
                reference,
                paymentType: this.paymentType,
                contractId: this.contractId,
                description: `Paiement via Jeko - ${this.paymentType}`,
                customerEmail: this.customerEmail,
                customerName: this.customerName,
                successUrl: window.location.origin + "/paiements/jeko/success",
                errorUrl: window.location.origin + "/paiements/jeko/error",
                metadata: {
                    source: "vue_app",
                    timestamp: new Date().toISOString(),
                },
            };

            // Ajouter les factures pré-sélectionnées pour recoveryPrime
            if (this.paymentType === "recoveryPrime" && this.preselectedInvoiceIds.length > 0) {
                paymentData.preselectedInvoiceIds = this.preselectedInvoiceIds;
            }

            this.widget.open(paymentData);
        },
    },
};
</script>

<style scoped>
.btn-pay {
    background: #1D603D;
    color: #fff;
    border: none;
    padding: 13px 24px;
    border-radius: 10px;
    font-weight: 600;
    font-size: 15px;
    cursor: pointer;
    width: 100%;
    transition: all 0.2s;
}
.btn-pay:hover:not(:disabled) {
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(29, 96, 61, 0.3);
}
.btn-pay:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
</style>
🟢 Exemples d'utilisation des 3 types :
<JekoPaymentButton contract-id="1093" payment-type="firstPayment" />
<JekoPaymentButton contract-id="12452" payment-type="earlyPayment" />
<JekoPaymentButton contract-id="12452" payment-type="recoveryPrime" :preselected-invoice-ids="['173669', '3132514']" />
🔧 Props du composant Vue.js :
contract-id : Identifiant du contrat (obligatoire)
payment-type : Type de paiement (firstPayment, earlyPayment, recoveryPrime) (défaut: firstPayment)
customer-email : Email du client (optionnel)
customer-name : Nom du client (optionnel)
preselected-invoice-ids : Factures pré-sélectionnées (optionnel, uniquement pour recoveryPrime)
label : Libellé du bouton (défaut: "Payer avec Jeko")
@success : Événement en cas de succès (optionnel)
@error : Événement en cas d'erreur (optionnel)

Service et composant Angular pour une intégration propre avec les 3 types de paiement.

🅰️ Documentation d'intégration Angular

🎯 Support multi-providers : Le widget route automatiquement vers Wave ou Jeko selon le paymentMethod.

1. Créer le service et le composant :

// Fichiers:
// - jeko-widget.service.ts
// - jeko-payment.component.ts
// Copier les codes ci-dessous

2. Importer le module :

// app.module.ts
import { JekoPaymentComponent } from './components/jeko-payment.component';

@NgModule({
    declarations: [JekoPaymentComponent],
    // ...
})

3. Utilisation du composant :

// 1) Premier paiement
<jeko-payment contractId="1093" paymentType="firstPayment"></jeko-payment>

// 2) Paiement anticipé
<jeko-payment contractId="12452" paymentType="earlyPayment"></jeko-payment>

// 3) Régularisation avec pré-sélection
<jeko-payment 
    contractId="12452" 
    paymentType="recoveryPrime" 
    [preselectedInvoiceIds]="['173669', '3132514']" 
></jeko-payment>

// 4) Paiement Wave (spécifier paymentMethod)
<jeko-payment 
    contractId="12452" 
    paymentType="earlyPayment" 
    paymentMethod="wave"
></jeko-payment>
// 1) Premier paiement
<app-jeko-payment
    contractId="1093"
    paymentType="firstPayment"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    label="Payer la première prime"
    (success)="onPaymentSuccess($event)"
    (error)="onPaymentError($event)"
>
</app-jeko-payment>
Angular
// ============================================================
// jeko-widget.service.ts
// ============================================================
import { Injectable, Inject, DOCUMENT } from "@angular/core";
import { Subject } from "rxjs";

export type PaymentType = "firstPayment" | "earlyPayment" | "recoveryPrime";

export interface JekoWidgetConfig {
    // Endpoint pour initialiser le paiement
    backendEndpoint: string;
    // Endpoint pour vérifier le contrat
    contractCheckEndpoint: string;
    // Devise par défaut
    currency: string;
    // Headers personnalisés
    headers?: Record<string, string>;
    // Timeout de la requête (ms)
    timeout?: number;
    // Vérification automatique du contrat
    autoVerifyContract?: boolean;
    // Personnalisation du thème
    theme?: {
        primary?: string;
        primaryDark?: string;
        accent?: string;
        radius?: string;
        fontFamily?: string;
        maxWidth?: string;
    };
    // Traductions personnalisées
    translations?: Record<string, string>;
}

export interface JekoPaymentData {
    // Référence unique du paiement
    reference: string;
    // Type de paiement: firstPayment | earlyPayment | recoveryPrime
    paymentType: PaymentType;
    // Identifiant du contrat
    contractId: string;
    // Description du paiement
    description?: string;
    // Email du client
    customerEmail?: string;
    // Nom du client
    customerName?: string;
    // Factures pré-sélectionnées (uniquement pour recoveryPrime)
    preselectedInvoiceIds?: string[];
    // URL de redirection en cas de succès
    successUrl?: string;
    // URL de redirection en cas d'erreur
    errorUrl?: string;
    // Métadonnées additionnelles
    metadata?: any;
}

@Injectable({ providedIn: "root" })
export class JekoWidgetService {
    private widget: any = null;
    private isReady = false;
    private successSubject = new Subject<{ redirectUrl: string; data: any }>();
    private errorSubject = new Subject<{ message: string; data?: any }>();

    constructor(@Inject(DOCUMENT) private document: Document) {}

    /**
     * Charge le widget Jeko dynamiquement
     */
    loadWidget(config: JekoWidgetConfig): Promise<void> {
        return new Promise((resolve) => {
            if (window["JekoWidget"]) {
                this.initWidget(config);
                resolve();
                return;
            }
            const script = this.document.createElement("script");
            script.src = "{{ BASE_URL }}/api/paiements-payment-widget.js";
            script.onload = () => {
                this.initWidget(config);
                resolve();
            };
            script.onerror = () => {
                console.error("❌ Erreur chargement du widget Jeko");
                resolve();
            };
            this.document.head.appendChild(script);
        });
    }

    /**
     * Initialise le widget avec la configuration
     */
    private initWidget(config: JekoWidgetConfig): void {
        if (!window["JekoWidget"]) return;

        this.widget = new window["JekoWidget"]({
            backendEndpoint: config.backendEndpoint,
            contractCheckEndpoint: config.contractCheckEndpoint,
            currency: config.currency || "XOF",
            timeout: config.timeout || 30000,
            autoVerifyContract: config.autoVerifyContract !== undefined ? config.autoVerifyContract : true,
            headers: config.headers || {},
            theme: config.theme || {
                primary: "#1D603D",
                primaryDark: "#0B482F",
                accent: "#E09518",
                radius: "16px",
            },
            translations: config.translations || {},
            callbacks: {
                onSuccess: (url: string, data: any) => {
                    this.successSubject.next({ redirectUrl: url, data });
                    window.open(url, "_blank");
                },
                onError: (msg: string, data: any) => {
                    this.errorSubject.next({ message: msg, data });
                },
            },
        });
        this.isReady = true;
    }

    /**
     * Ouvre le widget avec les données de paiement
     */
    openPayment(data: JekoPaymentData): void {
        if (!this.widget || !this.isReady) {
            console.error("❌ Widget Jeko non prêt");
            return;
        }
        this.widget.open(data);
    }

    /**
     * Observable des événements de succès
     */
    get onSuccess() {
        return this.successSubject.asObservable();
    }

    /**
     * Observable des événements d'erreur
     */
    get onError() {
        return this.errorSubject.asObservable();
    }

    /**
     * Ferme le widget
     */
    close(): void {
        if (this.widget) {
            this.widget.close();
        }
    }

    /**
     * Génère une référence unique pour le paiement
     * Format: PAI-YYYYMMDDHHMMSS-TIMESTAMP-RANDOM-CODE
     */
    generateReference(): string {
        const now = new Date();
        const timestamp = now.getTime();
        const random = Math.random().toString(36).substring(2, 10);
        const code = String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0");
        const year = now.getFullYear();
        const month = String(now.getMonth() + 1).padStart(2, "0");
        const day = String(now.getDate()).padStart(2, "0");
        const hours = String(now.getHours()).padStart(2, "0");
        const minutes = String(now.getMinutes()).padStart(2, "0");
        const seconds = String(now.getSeconds()).padStart(2, "0");
        return `PAI-${year}${month}${day}${hours}${minutes}${seconds}-${timestamp}-${random}-${code}`;
    }
}

// ============================================================
// jeko-payment.component.ts
// ============================================================
import { Component, Input, OnInit, Output, EventEmitter } from "@angular/core";
import { JekoWidgetService, PaymentType } from "./jeko-widget.service";

@Component({
    selector: "app-jeko-payment",
    template: `
        <button
            (click)="handlePayment()"
            [disabled]="!isReady || isLoading"
            class="btn-pay"
        >
            {{ isLoading ? "Chargement..." : label }}
        </button>
    `,
    styles: [`
        .btn-pay {
            background: #1D603D;
            color: #fff;
            border: none;
            padding: 13px 24px;
            border-radius: 10px;
            font-weight: 600;
            font-size: 15px;
            cursor: pointer;
            width: 100%;
            transition: all 0.2s;
        }
        .btn-pay:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 8px 24px rgba(29, 96, 61, 0.3);
        }
        .btn-pay:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
    `]
})
export class JekoPaymentComponent implements OnInit {
    // Identifiant du contrat (obligatoire)
    @Input() contractId!: string;
    // Type de paiement: firstPayment | earlyPayment | recoveryPrime
    @Input() paymentType: PaymentType = "firstPayment";
    // Email du client
    @Input() customerEmail: string = "";
    // Nom du client
    @Input() customerName: string = "";
    // Factures pré-sélectionnées (uniquement pour recoveryPrime)
    @Input() preselectedInvoiceIds: string[] = [];
    // Libellé du bouton
    @Input() label: string = "Payer avec Jeko";

    @Output() success = new EventEmitter<{ redirectUrl: string; data: any }>();
    @Output() error = new EventEmitter<{ message: string; data?: any }>();

    isReady = false;
    isLoading = false;

    constructor(private widgetService: JekoWidgetService) {}

    ngOnInit(): void {
        // Configuration du widget
        const config: JekoWidgetConfig = {
            backendEndpoint: "{{ BASE_URL }}/api/paiements/init",
            contractCheckEndpoint: "{{ BASE_URL }}/api/paiements/contrat/verifier",
            currency: "XOF",
            timeout: 30000,
            autoVerifyContract: true,
            theme: {
                primary: "#1D603D",
                primaryDark: "#0B482F",
                accent: "#E09518",
                radius: "16px",
            },
            translations: {
                title: "Paiement sécurisé",
                close: "Fermer",
                back: "Retour",
                stepContract: "Contrat",
                stepMethod: "Paiement",
                stepSummary: "Résumé",
                chooseMethod: "Choisissez un moyen de paiement",
                confirmAndPay: "Confirmer et payer",
                processing: "Initialisation du paiement en cours...",
            },
        };

        this.widgetService.loadWidget(config).then(() => {
            this.isReady = true;
        });

        this.widgetService.onSuccess.subscribe((result) => {
            this.isLoading = false;
            this.success.emit(result);
        });

        this.widgetService.onError.subscribe((result) => {
            this.isLoading = false;
            this.error.emit(result);
        });
    }

    /**
     * Gère le clic sur le bouton de paiement
     */
    handlePayment(): void {
        if (!this.isReady) return;

        this.isLoading = true;
        const reference = this.widgetService.generateReference();

        // Construction des données selon le type de paiement
        const paymentData: any = {
            reference,
            paymentType: this.paymentType,
            contractId: this.contractId,
            description: `Paiement via Jeko - ${this.paymentType}`,
            customerEmail: this.customerEmail,
            customerName: this.customerName,
            successUrl: window.location.origin + "/paiements/jeko/success",
            errorUrl: window.location.origin + "/paiements/jeko/error",
            metadata: {
                source: "angular_app",
                timestamp: new Date().toISOString(),
            },
        };

        // Ajouter les factures pré-sélectionnées pour recoveryPrime
        if (this.paymentType === "recoveryPrime" && this.preselectedInvoiceIds.length > 0) {
            paymentData.preselectedInvoiceIds = this.preselectedInvoiceIds;
        }

        this.widgetService.openPayment(paymentData);
    }
}

// ============================================================
// EXEMPLES D'UTILISATION DES 3 TYPES
// ============================================================

/*
// 1) PREMIER PAIEMENT (firstPayment)
<app-jeko-payment
    contractId="1093"
    paymentType="firstPayment"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    label="Payer la première prime"
    (success)="onPaymentSuccess($event)"
    (error)="onPaymentError($event)"
>
</app-jeko-payment>

// 2) PAIEMENT ANTICIPÉ (earlyPayment)
<app-jeko-payment
    contractId="12452"
    paymentType="earlyPayment"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    label="Payer en avance"
    (success)="onPaymentSuccess($event)"
    (error)="onPaymentError($event)"
>
</app-jeko-payment>

// 3) RÉGULARISATION (recoveryPrime) avec pré-sélection
<app-jeko-payment
    contractId="12452"
    paymentType="recoveryPrime"
    [preselectedInvoiceIds]="['173669', '3132514']"
    customerEmail="client@example.com"
    customerName="Jean Dupont"
    label="Régulariser mes impayés"
    (success)="onPaymentSuccess($event)"
    (error)="onPaymentError($event)"
>
</app-jeko-payment>
*/
🔧 Inputs du composant Angular :
contractId : Identifiant du contrat (obligatoire)
paymentType : Type de paiement (firstPayment, earlyPayment, recoveryPrime) (défaut: firstPayment)
customerEmail : Email du client (optionnel)
customerName : Nom du client (optionnel)
preselectedInvoiceIds : Factures pré-sélectionnées (optionnel, uniquement pour recoveryPrime)
label : Libellé du bouton (défaut: "Payer avec Jeko")
success : Événement en cas de succès (optionnel)
error : Événement en cas d'erreur (optionnel)

Composant Next.js avec App Router et "use client" pour les 3 types de paiement.

▲ Documentation d'intégration Next.js

🎯 Support multi-providers : Le widget route automatiquement vers Wave ou Jeko selon le paymentMethod.

1. Créer le composant :

// Fichier: components/JekoPaymentButton.tsx
// Copier le code du composant ci-dessous

2. Variables d'environnement (.env.local) :

NEXT_PUBLIC_PAYMENT_API_URL={{ BASE_URL }}/api/paiements/init
NEXT_PUBLIC_CONTRACT_CHECK_URL={{ BASE_URL }}/api/paiements/contrat/verifier
NEXT_PUBLIC_STATUS_CHECK_URL={{ BASE_URL }}/api/paiements/statut

3. Utilisation du composant :

// 1) Premier paiement
<JekoPaymentButton contractId="1093" paymentType="firstPayment" />

// 2) Paiement anticipé
<JekoPaymentButton contractId="12452" paymentType="earlyPayment" />

// 3) Régularisation avec pré-sélection
<JekoPaymentButton 
    contractId="12452" 
    paymentType="recoveryPrime" 
    preselectedInvoiceIds={['173669', '3132514']} 
/>

// 4) Paiement Wave (spécifier paymentMethod)
<JekoPaymentButton 
    contractId="12452" 
    paymentType="earlyPayment" 
    paymentMethod="wave"
/>
// 3) Régularisation avec pré-sélection <JekoPaymentButton contractId="12452" paymentType="recoveryPrime" preselectedInvoiceIds={['173669', '3132514']} />
Next.js
'use client';

import React, { useState, useEffect, useRef } from "react";

// ============================================================
// TYPES
// ============================================================
type PaymentType = "firstPayment" | "earlyPayment" | "recoveryPrime";

interface JekoPaymentButtonProps {
    // Identifiant du contrat (obligatoire)
    contractId: string;
    // Type de paiement: firstPayment | earlyPayment | recoveryPrime
    paymentType?: PaymentType;
    // Email du client
    customerEmail?: string;
    // Nom du client
    customerName?: string;
    // Factures pré-sélectionnées (uniquement pour recoveryPrime)
    preselectedInvoiceIds?: string[];
    // Callback en cas de succès
    onSuccess?: (redirectUrl: string, data: any) => void;
    // Callback en cas d'erreur
    onError?: (message: string, data?: any) => void;
    // Classe CSS personnalisée
    className?: string;
    // Contenu du bouton
    children?: React.ReactNode;
}

declare global {
    interface Window {
        JekoWidget: any;
    }
}

// ============================================================
// CHARGEMENT DYNAMIQUE DU WIDGET
// ============================================================
const loadJekoWidget = (): Promise<any> => {
    return new Promise((resolve) => {
        if (typeof window === "undefined") {
            resolve(null);
            return;
        }
        if (window.JekoWidget) {
            resolve(window.JekoWidget);
            return;
        }
        const script = document.createElement("script");
        script.src = process.env.NEXT_PUBLIC_JEKO_WIDGET_URL || "{{ BASE_URL }}/api/paiements-payment-widget.js";
        script.onload = () => resolve(window.JekoWidget);
        script.onerror = () => resolve(null);
        document.head.appendChild(script);
    });
};

// ============================================================
// COMPOSANT PRINCIPAL
// ============================================================
export const JekoPaymentButton: React.FC<JekoPaymentButtonProps> = ({
    contractId,
    paymentType = "firstPayment",
    customerEmail = "",
    customerName = "",
    preselectedInvoiceIds = [],
    onSuccess,
    onError,
    className = "",
    children,
}) => {
    const [isLoading, setIsLoading] = useState(false);
    const [isReady, setIsReady] = useState(false);
    const widgetRef = useRef<any>(null);

    useEffect(() => {
        const initWidget = async () => {
            try {
                const JekoWidget = await loadJekoWidget();
                if (!JekoWidget) {
                    console.error("❌ JekoWidget non chargé");
                    return;
                }

                widgetRef.current = new JekoWidget({
                    // Endpoint pour initialiser le paiement
                    backendEndpoint: process.env.NEXT_PUBLIC_JEKO_API_URL || "{{ BASE_URL }}/api/paiements/init",
                    // Endpoint pour vérifier le contrat
                    contractCheckEndpoint: process.env.NEXT_PUBLIC_JEKO_CONTRACT_CHECK_URL || "{{ BASE_URL }}/api/paiements/contrat/verifier",
                    // Devise par défaut
                    currency: "XOF",
                    // Timeout de la requête (ms)
                    timeout: 30000,
                    // Vérification automatique du contrat si contractId fourni
                    autoVerifyContract: true,
                    // Headers personnalisés
                    headers: {
                        "X-CSRF-TOKEN": document.querySelector('meta[name="csrf-token"]')?.getAttribute("content") || "",
                    },
                    // Callbacks
                    callbacks: {
                        onSuccess: (redirectUrl: string, data: any) => {
                            setIsLoading(false);
                            onSuccess?.(redirectUrl, data);
                            window.open(redirectUrl, "_blank");
                        },
                        onError: (message: string, data: any) => {
                            setIsLoading(false);
                            onError?.(message, data);
                        },
                        onOpen: () => setIsLoading(true),
                        onClose: () => setIsLoading(false),
                    },
                    // Personnalisation du thème (optionnel)
                    theme: {
                        primary: "#1D603D",
                        primaryDark: "#0B482F",
                        accent: "#E09518",
                        radius: "16px",
                        fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
                        maxWidth: "550px",
                    },
                    // Traductions personnalisées (optionnel)
                    translations: {
                        title: "Paiement sécurisé",
                        close: "Fermer",
                        back: "Retour",
                        stepContract: "Contrat",
                        stepMethod: "Paiement",
                        stepSummary: "Résumé",
                        chooseMethod: "Choisissez un moyen de paiement",
                        confirmAndPay: "Confirmer et payer",
                        processing: "Initialisation du paiement en cours...",
                        success: "Paiement initialisé",
                        error: "Échec du paiement",
                        retry: "Réessayer",
                        networkError: "Erreur réseau",
                        requiredField: "Ce champ est requis",
                        contractIdLabel: "Identifiant du contrat",
                        contractIdPlaceholder: "Ex: 345678",
                        verify: "Vérifier le contrat",
                        verifying: "Vérification en cours...",
                        loadingContract: "Chargement des informations du contrat...",
                    },
                });
                setIsReady(true);
            } catch (error) {
                console.error("❌ Erreur chargement widget Jeko:", error);
                onError?.(error instanceof Error ? error.message : "Erreur inconnue");
            }
        };
        initWidget();
    }, [onSuccess, onError]);

    /**
     * Génère une référence unique pour le paiement
     * Format: PAI-YYYYMMDDHHMMSS-TIMESTAMP-RANDOM-CODE
     */
    const generateReference = (): string => {
        const now = new Date();
        const timestamp = now.getTime();
        const random = Math.random().toString(36).substring(2, 10);
        const code = String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0");
        const date = now.getFullYear() +
            String(now.getMonth() + 1).padStart(2, "0") +
            String(now.getDate()).padStart(2, "0") +
            String(now.getHours()).padStart(2, "0") +
            String(now.getMinutes()).padStart(2, "0") +
            String(now.getSeconds()).padStart(2, "0");
        return `PAI-${date}-${timestamp}-${random}-${code}`;
    };

    const handlePayment = () => {
        if (!widgetRef.current || !isReady) return;

        const reference = generateReference();

        // Construction des données selon le type de paiement
        const paymentData: any = {
            reference,
            paymentType,
            contractId,
            description: `Paiement via Jeko - ${paymentType}`,
            customerEmail,
            customerName,
            successUrl: window.location.origin + "/paiements/jeko/success",
            errorUrl: window.location.origin + "/paiements/jeko/error",
            metadata: {
                source: "nextjs_app",
                timestamp: new Date().toISOString(),
            },
        };

        // Ajouter les factures pré-sélectionnées pour recoveryPrime
        if (paymentType === "recoveryPrime" && preselectedInvoiceIds.length > 0) {
            paymentData.preselectedInvoiceIds = preselectedInvoiceIds;
        }

        widgetRef.current.open(paymentData);
    };

    return (
        <button
            onClick={handlePayment}
            disabled={!isReady || isLoading}
            className={`btn-pay ${className}`}
            style={{
                background: "#1D603D",
                color: "#fff",
                border: "none",
                padding: "13px 24px",
                borderRadius: "10px",
                fontWeight: 600,
                fontSize: "15px",
                cursor: "pointer",
                width: "100%",
                transition: "all 0.2s",
                opacity: (!isReady || isLoading) ? 0.5 : 1,
            }}
        >
            {isLoading ? "Chargement..." : (children || "Payer avec Jeko")}
        </button>
    );
};

export default JekoPaymentButton;

// ============================================================
// PAGE D'EXEMPLE (app/page.tsx)
// ============================================================

import { JekoPaymentButton } from "@/components/JekoPaymentButton";

export default function PaymentPage() {
    const handleSuccess = (redirectUrl: string, data: any) => {
        console.log("✅ Paiement réussi", { redirectUrl, data });
    };

    const handleError = (message: string, data?: any) => {
        console.error("❌ Erreur de paiement", { message, data });
    };

    return (
        <div style={{ maxWidth: 500, margin: "40px auto", padding: "0 20px" }}>
            <h1>💳 Paiement sécurisé</h1>

            <h3>1️⃣ Premier paiement (firstPayment)</h3>
            <p style={{ color: "#6b7280", fontSize: "14px" }}>
                Après validation d'un nouveau contrat. La vérification est automatique.
            </p>
            <JekoPaymentButton
                contractId="1093"
                paymentType="firstPayment"
                customerEmail="client@example.com"
                customerName="Jean Dupont"
                onSuccess={handleSuccess}
                onError={handleError}
            >
                Payer la première prime
            </JekoPaymentButton>

            <h3 style={{ marginTop: "24px" }}>2️⃣ Paiement anticipé (earlyPayment)</h3>
            <p style={{ color: "#6b7280", fontSize: "14px" }}>
                Le client renseigne son identifiant de contrat. La vérification est automatique.
            </p>
            <JekoPaymentButton
                contractId="12452"
                paymentType="earlyPayment"
                customerEmail="client@example.com"
                customerName="Jean Dupont"
                onSuccess={handleSuccess}
                onError={handleError}
            >
                Payer en avance
            </JekoPaymentButton>

            <h3 style={{ marginTop: "24px" }}>3️⃣ Régularisation (recoveryPrime)</h3>
            <p style={{ color: "#6b7280", fontSize: "14px" }}>
                Le client sélectionne les factures en attente. Pré-sélection possible.
            </p>
            <JekoPaymentButton
                contractId="12452"
                paymentType="recoveryPrime"
                preselectedInvoiceIds={['173669', '3132514']}
                customerEmail="client@example.com"
                customerName="Jean Dupont"
                onSuccess={handleSuccess}
                onError={handleError}
            >
                Régulariser mes impayés
            </JekoPaymentButton>
        </div>
    );
}
▲ Configuration Next.js :
Ajoutez ces variables dans votre .env.local :
NEXT_PUBLIC_JEKO_API_URL={{ BASE_URL }}/api/paiements/init
NEXT_PUBLIC_JEKO_CONTRACT_CHECK_URL={{ BASE_URL }}/api/paiements/contrat/verifier
🔧 Props du composant Next.js :
contractId : Identifiant du contrat (obligatoire)
paymentType : Type de paiement (firstPayment, earlyPayment, recoveryPrime) (défaut: firstPayment)
customerEmail : Email du client (optionnel)
customerName : Nom du client (optionnel)
preselectedInvoiceIds : Factures pré-sélectionnées (optionnel, uniquement pour recoveryPrime)
onSuccess : Callback en cas de succès (optionnel)
onError : Callback en cas d'erreur (optionnel)
className : Classe CSS personnalisée (optionnel)
children : Contenu du bouton (optionnel)
📌 Récapitulatif des 3 types de paiement :
firstPayment : <JekoPaymentButton contractId="1093" paymentType="firstPayment" /> — Premier paiement d'un contrat (souscription)
earlyPayment : <JekoPaymentButton contractId="12452" paymentType="earlyPayment" /> — Paiement anticipé des primes
recoveryPrime : <JekoPaymentButton contractId="12452" paymentType="recoveryPrime" preselectedInvoiceIds={['173669', '3132514']} /> — Régularisation de primes impayées avec pré-sélection
🔒 Sécurité :
• Le widget n'appelle JAMAIS l'API Jeko directement
• Toutes les clés secrètes restent côté serveur
• Le montant est recalculé et vérifié côté serveur pour éviter toute fraude
• API accessible via {{ BASE_URL }}/api/paiements/...