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.
💡 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).
Après validation d'un nouveau contrat
Renseignez l'identifiant du contrat
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.
🎯 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 MoneypaymentMethod: "moov" → Moov MoneypaymentMethod: "mtn" → MTN Mobile MoneypaymentMethod: "djamo" → Djamo💳 Cartes (Jeko) :
paymentMethod: "visa" → VisapaymentMethod: "mastercard" → Mastercard3. Polling automatique du statut :
🔄 Le widget effectue automatiquement le polling du statut du paiement :
pending, success, errorpending : le widget continue le polling avec un spinnersuccess : le webhook met à jour le statut en base, le widget affiche "Paiement réussi !"error : le widget met à jour le statut en base et affiche une erreur4. 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;
}
<!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>
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
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.
🎯 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"
/>
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>
*/
firstPayment : <JekoPaymentButton contractId="1093" paymentType="firstPayment" />
earlyPayment : <JekoPaymentButton contractId="12452" paymentType="earlyPayment" />
recoveryPrime : <JekoPaymentButton contractId="12452" paymentType="recoveryPrime" preselectedInvoiceIds={['173669', '3132514']} />
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.
🎯 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']"
/>
<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>
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.
🎯 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>
// ============================================================
// 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>
*/
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.
🎯 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']}
/>
'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>
);
}
.env.local :
NEXT_PUBLIC_JEKO_API_URL={{ BASE_URL }}/api/paiements/init
NEXT_PUBLIC_JEKO_CONTRACT_CHECK_URL={{ BASE_URL }}/api/paiements/contrat/verifier
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)
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
{{ BASE_URL }}/api/paiements/...