React NativeExpo SDK 50+ · React Native 0.73+ · Node backend

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.

Sandbox

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é ────────── │
└──────────────┘                                 │
Les étapes 4 et 6 se déroulent en parallèle. Le webhook (6) est la source de vérité ; le deep link (5) est juste un signal UX pour rafraîchir l'écran.

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

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>
  );
}
N'utilisez pas 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");
  },
);
Couplez le webhook à des push notifications (Expo Push, FCM, APNs) pour avertir l'utilisateur même si l'app était fermée pendant le paiement.

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 ?+
Non. Tout ce qui est dans le bundle JavaScript de l'app est extractible avec un éditeur de texte. La clé secrète reste exclusivement sur votre backend ; l'app n'appelle jamais /pay-create directement.
Faut-il Expo ou React Native CLI ?+
Les deux fonctionnent. Sur Expo, déclarez 'scheme' dans app.json et utilisez expo-linking. Sur RN bare, ajoutez intent-filter Android et CFBundleURLSchemes iOS.
Pourquoi un deep link plutôt qu'une WebView ?+
L'application officielle MonCash (Digicel) gère l'authentification PIN dans son propre contexte sécurisé. Une WebView intra-app brise certains flux de sécurité et peut bloquer le paiement. Le navigateur système est le chemin recommandé.
Si l'utilisateur quitte avant le retour, comment savoir s'il a payé ?+
Le webhook est la source de vérité. Même si l'utilisateur ferme l'app pendant le paiement, le webhook payment.completed arrive sur votre backend et marque la commande payée. La réouverture de l'app peut alors lire le statut.
Le deep link mcc:// fonctionne-t-il en mode dev avec Expo Go ?+
Expo Go utilise son propre schéma (exp://). En dev, utilisez exp://192.168.x.x:8081/--/payment-return comme returnUrl. En build production, mcc:// fonctionne normalement.
Comment tester sans déployer le backend ?+
Lancez votre backend en local et exposez-le avec ngrok. Configurez l'URL ngrok comme webhook dans le dashboard MonCashConnect (mode Sandbox).