Intégrer MonCash dans une application React Native
Architecture mobile end-to-end : backend Node qui crée le paiement, Linking.openURL pour ouvrir MonCash, deep link mcc://payment-return pour le retour, webhook signé HMAC-SHA256 pour la confirmation.
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 du flux mobile
La règle d'or sur mobile : la clé secrète ne doit jamais quitter votre serveur. Tout binaire React Native publié peut être décompressé et son JS lu. Le flux correct sépare strictement client et serveur.
┌──────────────┐ 1. demande montant ┌──────────────┐
│ │ ───────────────────────▶ │ │
│ React │ │ Votre │
│ Native │ ◀─── 2. paymentUrl ───── │ backend │
│ (client) │ │ (Node/Go/…) │
│ │ │ │
└──────┬───────┘ └──────┬───────┘
│ 3. Linking.openURL(paymentUrl) │
▼ │
┌──────────────┐ │
│ Navigateur │ 4. user paie sur MonCash │
│ système │ ──────────────────────────────▶ │
│ + MonCash │ │
└──────┬───────┘ │
│ 5. redirect mcc://payment-return?ref=… │
▼ │ 6. webhook
┌──────────────┐ │ payment.completed
│ React │ 7. GET /status?ref=… │ (HMAC signé)
│ Native │ ──────────────────────────────▶ │
│ (retour) │ ◀─── statut confirmé ────────── │
└──────────────┘ │Prérequis
- Node.js 18+ sur le backend
- React Native 0.73+ ou Expo SDK 50+
- Un projet MonCashConnect actif — créer un projet
- Backend déjà déployé en HTTPS (Vercel, Fly, Railway, etc.)
Déclarer le schéma deep link
Expo (managed) — dans app.json :
{
"expo": {
"name": "MaBoutique",
"slug": "ma-boutique",
"scheme": "mcc",
"ios": {
"bundleIdentifier": "com.maboutique.app"
},
"android": {
"package": "com.maboutique.app",
"intentFilters": [
{
"action": "VIEW",
"category": ["BROWSABLE", "DEFAULT"],
"data": [{ "scheme": "mcc", "host": "payment-return" }]
}
]
}
}
}React Native bare — iOS (ios/MaBoutique/Info.plist) :
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.maboutique.app</string>
<key>CFBundleURLSchemes</key>
<array>
<string>mcc</string>
</array>
</dict>
</array>React Native bare — Android (android/app/src/main/AndroidManifest.xml) :
<activity android:name=".MainActivity" android:launchMode="singleTask">
<intent-filter android:label="filter_react_native">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mcc" android:host="payment-return" />
</intent-filter>
</activity>launchMode="singleTask" est important : sans lui, Android peut créer une nouvelle instance de l'app au retour du deep link, ce qui réinitialise l'état React et perd la commande en cours.Backend — endpoint create-payment
Le backend conserve la clé secrète et expose un endpoint que l'app appelle. Exemple Node/Express avec le SDK officiel :
npm install @moncashconnect/sdk express// server.js
import express from "express";
import { MonCashConnect } from "@moncashconnect/sdk";
const app = express();
app.use(express.json());
const mcc = new MonCashConnect({ secretKey: process.env.MCC_SECRET_KEY });
// Appelé par l'app RN avec { amount, orderId }
app.post("/api/payments", async (req, res) => {
const { amount, orderId } = req.body;
if (!amount || !orderId) {
return res.status(400).json({ error: "amount et orderId requis" });
}
try {
const payment = await mcc.payments.create({
amount,
referenceId: orderId,
// Deep link vers l'app — pas une URL https://
returnUrl: "mcc://payment-return",
});
res.json({
paymentUrl: payment.paymentUrl,
reference: payment.reference,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Lecture de statut — pas de clé sur le client
app.get("/api/payments/:reference", async (req, res) => {
try {
const status = await mcc.payments.retrieve(req.params.reference);
res.json(status);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(3000);returnUrl peut être un schéma personnalisé (mcc://payment-return) ou une Universal Link (https://app.maboutique.com/payment-return). Les Universal Links sont plus robustes mais nécessitent un fichierapple-app-site-association et un assetlinks.json.Code React Native — initier le paiement
# Expo (recommandé)
npx expo install expo-linking
# Bare RN — Linking est déjà fourni par react-native// screens/CheckoutScreen.tsx
import { useState } from "react";
import { View, Text, Button, Alert, ActivityIndicator } from "react-native";
import * as Linking from "expo-linking"; // ou: import { Linking } from "react-native";
const API = "https://api.maboutique.com";
export default function CheckoutScreen() {
const [loading, setLoading] = useState(false);
const pay = async () => {
setLoading(true);
try {
// 1. Demander au backend de créer le paiement
const orderId = "ORD-" + Date.now();
const res = await fetch(`${API}/api/payments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 500, orderId }),
});
const { paymentUrl, reference } = await res.json();
// 2. Persister la référence pour la retrouver au retour
await AsyncStorage.setItem("pendingRef", reference);
// 3. Ouvrir MonCash dans le navigateur système
const supported = await Linking.canOpenURL(paymentUrl);
if (!supported) {
Alert.alert("Impossible d'ouvrir MonCash sur cet appareil.");
return;
}
await Linking.openURL(paymentUrl);
} catch (err) {
Alert.alert("Erreur", String(err));
} finally {
setLoading(false);
}
};
return (
<View style={{ padding: 20 }}>
<Text>Total : 500 HTG</Text>
{loading
? <ActivityIndicator />
: <Button title="Payer avec MonCash" onPress={pay} />}
</View>
);
}WebBrowser.openBrowserAsync d'Expo : la page MonCash refuse le rendu dans un in-app browser quand les conditions sécurisées de l'app Digicel ne sont pas remplies. Linking.openURL ouvre Safari/Chrome système, qui sont supportés.Gérer le retour deep link
Placez un listener au niveau racine (App.tsx) pour capter mcc://payment-return?ref=… que l'app soit en arrière-plan ou fermée :
// App.tsx — listener global de deep link
import { useEffect } from "react";
import * as Linking from "expo-linking";
import AsyncStorage from "@react-native-async-storage/async-storage";
const API = "https://api.maboutique.com";
export default function App() {
useEffect(() => {
// Cas 1 : app déjà ouverte, deep link reçu en runtime
const sub = Linking.addEventListener("url", ({ url }) => handle(url));
// Cas 2 : app lancée par le deep link à froid
Linking.getInitialURL().then((url) => { if (url) handle(url); });
return () => sub.remove();
}, []);
return <NavigationContainer>{/* ... */}</NavigationContainer>;
}
async function handle(url: string) {
const { hostname, queryParams } = Linking.parse(url);
if (hostname !== "payment-return") return;
// ref peut venir du callback OU du storage si l'URL est tronquée
const ref =
(queryParams?.ref as string | undefined) ??
(await AsyncStorage.getItem("pendingRef"));
if (!ref) return;
// Demander le statut au backend — JAMAIS confiance aux paramètres URL
const res = await fetch(`${API}/api/payments/${ref}`);
const tx = await res.json();
if (tx.status === "completed") {
// Naviguer vers l'écran "Merci"
} else if (tx.status === "pending") {
// Webhook pas encore arrivé — poll quelques secondes ou afficher "en cours"
}
}Webhook backend — source de vérité
Le mobile peut perdre la connexion ou être tué par l'OS avant le retour. Le webhook est la seule façon fiable de confirmer un paiement.
// server.js (suite)
import crypto from "crypto";
// IMPORTANT : raw body, pas express.json()
app.post("/webhooks/moncash",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("X-MCC-Signature") || "";
const timestamp = req.header("X-MCC-Timestamp") || "";
const rawBody = req.body.toString("utf8");
const expected = crypto
.createHmac("sha256", process.env.MCC_WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(rawBody);
if (event.event === "payment.completed") {
// Marquer la commande payée + déclencher push notification
sendPushNotification(event.reference, "Paiement confirmé");
} else if (event.event === "payment.failed") {
sendPushNotification(event.reference, "Paiement échoué");
}
res.status(200).send("OK");
},
);Liste de contrôle avant publication
Le schéma mcc:// est déclaré dans app.json (Expo) ou Info.plist + AndroidManifest.xml (bare RN)
La clé sk_proj_… n'apparaît jamais dans le bundle — uniquement sur le backend
L'app appelle votre backend, pas /pay-create directement
Linking.openURL est utilisé, pas WebBrowser ni WebView
Le listener Linking.addEventListener est installé au niveau racine
Linking.getInitialURL() est appelé pour les cold-starts par deep link
Le statut est lu via le backend, jamais déduit des paramètres de l'URL de retour
Le webhook backend vérifie le HMAC-SHA256 en temps constant (timingSafeEqual)
Le webhook est idempotent — même événement reçu N fois = 1 livraison
Push notifications configurées pour confirmer un paiement quand l'app est fermée
AsyncStorage stocke la référence pendingRef au cas où le deep link arrive sans ?ref
FAQ
Puis-je mettre la clé sk_proj_… directement dans l'app React Native ?+
Faut-il Expo ou React Native CLI ?+
Pourquoi un deep link plutôt qu'une WebView ?+
Si l'utilisateur quitte avant le retour, comment savoir s'il a payé ?+
Le deep link mcc:// fonctionne-t-il en mode dev avec Expo Go ?+
Comment tester sans déployer le backend ?+
Lectures recommandées :