Intégrer MonCash avec React
Un SPA React qui appelle un mini-backend Node pour créer un paiement MonCash, redirige l'utilisateur vers MonCashConnect, puis confirme la commande via webhook signé.
Commencez en Sandbox
Avant d'implémenter le code ci-dessous avec votre clé live (sk_proj_…), testez d'abord en mode Sandbox avec sk_test_proj_…. Le code est identique — vous ne changez que la valeur de la variable d'environnement. Lisez le Guide Sandbox avant de continuer.
Architecture en deux temps
React tourne dans le navigateur — il ne peut jamais détenir la clé secrète sk_proj_…. L'intégration recommandée est donc :
- Le composant React envoie
{ orderId, amount }à votre backend. - Le backend appelle
POST /pay-createavec la clé secrète et renvoiepaymentUrl. - React redirige le navigateur vers
paymentUrl. - Après paiement, MonCash renvoie l'utilisateur vers votre
returnUrl. - En parallèle, votre webhook reçoit
payment.completed— c'est la source de vérité.
sk_proj_… dans VITE_* ni REACT_APP_* : tout ce qui commence par ce préfixe est inlined dans le bundle public.Prérequis
- Node.js 18+ pour le backend
- React 18+ (Vite ou Create React App)
- Un projet MonCashConnect avec clé secrète — créer un projet
Backend Node + @moncashconnect/sdk
Dans un dossier /server séparé :
npm install express cors dotenv @moncashconnect/sdkFichier .env :
MCC_SECRET_KEY=sk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MCC_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
APP_URL=https://votresite.com
FRONTEND_ORIGIN=https://votresite.com
PORT=3001Fichier server.js :
import "dotenv/config";
import express from "express";
import cors from "cors";
import { MonCashClient, constructEvent, MonCashError } from "@moncashconnect/sdk";
const app = express();
const client = new MonCashClient(process.env.MCC_SECRET_KEY);
// CORS limité à votre origine frontend
app.use(cors({ origin: process.env.FRONTEND_ORIGIN, credentials: false }));
// JSON sauf pour le webhook (qui a besoin du body brut)
app.use((req, res, next) => {
if (req.path === "/webhooks/moncash") return next();
express.json()(req, res, next);
});
// ── POST /api/pay ─────────────────────────────────────────────────────
// Appelé par React, renvoie { paymentUrl, reference }
app.post("/api/pay", async (req, res) => {
const { orderId, amount, customerName, customerEmail } = req.body ?? {};
if (!orderId || !amount) {
return res.status(400).json({ error: "orderId et amount requis." });
}
try {
const payment = await client.createPayment(Number(amount), String(orderId), {
returnUrl: `${process.env.APP_URL}/pay/return`,
customerName,
customerEmail,
});
res.json({
paymentUrl: payment.paymentUrl,
reference: payment.reference,
});
} catch (err) {
if (err instanceof MonCashError) {
return res.status(err.statusCode).json({ error: err.message });
}
res.status(500).json({ error: "Erreur serveur." });
}
});
// ── GET /api/pay-status?reference= ────────────────────────────────────
// Appelé par la page de retour pour afficher un statut
app.get("/api/pay-status", async (req, res) => {
const ref = String(req.query.reference ?? "");
if (!ref) return res.status(400).json({ error: "Référence manquante." });
try {
const tx = await client.getPaymentStatus(ref);
res.json(tx);
} catch (err) {
if (err instanceof MonCashError) {
return res.status(err.statusCode).json({ error: err.message });
}
res.status(500).json({ error: "Erreur serveur." });
}
});
// ── Webhook (voir section dédiée) ─────────────────────────────────────
app.post(
"/webhooks/moncash",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = constructEvent(
req.body,
req.headers["x-mcc-signature"] ?? "",
req.headers["x-mcc-timestamp"] ?? "",
process.env.MCC_WEBHOOK_SECRET,
);
if (event.event === "payment.completed") {
// markOrderPaid(event.reference);
} else if (event.event === "payment.failed") {
// markOrderFailed(event.reference);
}
res.sendStatus(200);
} catch (err) {
if (err instanceof MonCashError) {
return res.status(err.statusCode).send(err.message);
}
res.sendStatus(500);
}
},
);
app.listen(process.env.PORT ?? 3001);Composant React (PayButton)
Dans votre projet React, ajoutez l'URL du backend dans .env :
VITE_API_URL=https://api.votresite.comComposant src/components/PayButton.tsx :
import { useState } from "react";
type Props = {
orderId: string;
amount: number; // en HTG
customerName?: string;
customerEmail?: string;
};
export function PayButton({ orderId, amount, customerName, customerEmail }: Props) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function startPayment() {
setLoading(true);
setError(null);
try {
const res = await fetch(`${import.meta.env.VITE_API_URL}/api/pay`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderId, amount, customerName, customerEmail }),
});
if (!res.ok) {
const j = await res.json().catch(() => ({}));
throw new Error(j.error ?? `HTTP ${res.status}`);
}
const { paymentUrl } = await res.json();
// Redirection vers MonCash — l'utilisateur quitte la page
window.location.assign(paymentUrl);
} catch (e) {
setError(e instanceof Error ? e.message : "Erreur inconnue");
setLoading(false);
}
}
return (
<div>
<button
type="button"
onClick={startPayment}
disabled={loading}
className="rounded-md bg-red-600 px-4 py-2 font-medium text-white disabled:opacity-60"
>
{loading ? "Redirection…" : `Payer ${amount} HTG avec MonCash`}
</button>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
</div>
);
}window.location.assign(paymentUrl) et pas window.open(...) : les bloqueurs de pop-ups empêchent souvent le second mode sur mobile.Page de retour /pay/return
MonCash redirige le client vers {returnUrl}?ref={reference}. Affichez un statut côté client en interrogeant votre backend :
import { useEffect, useState } from "react";
type Tx = { status: "pending" | "completed" | "failed"; amount: number; reference: string };
export function PaymentReturn() {
const [tx, setTx] = useState<Tx | null>(null);
const [err, setErr] = useState<string | null>(null);
useEffect(() => {
const ref = new URLSearchParams(window.location.search).get("ref");
if (!ref) { setErr("Référence manquante"); return; }
fetch(`${import.meta.env.VITE_API_URL}/api/pay-status?reference=${ref}`)
.then((r) => r.json())
.then(setTx)
.catch((e) => setErr(String(e)));
}, []);
if (err) return <p>Erreur : {err}</p>;
if (!tx) return <p>Vérification en cours…</p>;
if (tx.status === "completed") return <p>Merci ! Paiement de {tx.amount} HTG reçu.</p>;
if (tx.status === "failed") return <p>Le paiement a échoué.</p>;
return <p>Paiement en attente de confirmation MonCash…</p>;
}completed ici — attendez l'événement webhook payment.completed.Webhook — source de vérité
Le webhook est implémenté côté backend (cf. server.js plus haut). Configurez l'URL publique de votre backend dans le tableau de bord MonCashConnect :
https://api.votresite.com/webhooks/moncashPour tester en local, exposez votre backend avec un tunnel :
ngrok http 3001Voir le guide de test webhook pour la procédure complète avec rejeu d'événements.
Liste de contrôle avant production
MCC_SECRET_KEY n'est JAMAIS exposé côté React (pas dans VITE_* ni REACT_APP_*)
Le backend valide orderId / amount avant d'appeler /pay-create
CORS du backend est restreint à votre origine de production
La page de retour affiche un état 'pending' tant que le webhook n'a pas confirmé
Le handler webhook est idempotent (ne crédite pas deux fois la même commande)
L'URL https://api.votresite.com/webhooks/moncash est enregistrée dans le dashboard MCC
Les clés sk_test_proj_… ont été remplacées par sk_proj_… en production
FAQ
Pourquoi ne pas appeler /pay-create directement depuis React ?+
Puis-je utiliser Create React App au lieu de Vite ?+
Comment afficher un état de chargement pendant la redirection ?+
Faut-il un compte marchand Digicel séparé ?+
Que faire si le même webhook arrive en double ?+
Mon SPA est servi depuis un CDN, où dois-je héberger le backend ?+
Lectures recommandées :