Passer au contenu principal

Documentation Index

Fetch the complete documentation index at: https://developers.ligdicash.com/llms.txt

Use this file to discover all available pages before exploring further.

Ce guide explique comment intégrer LigdiCash dans une application mobile. Il couvre l’architecture recommandée, l’ouverture de la page de paiement via WebView, la détection du retour utilisateur et la confirmation du paiement côté backend. Pré-requis : vous avez un backend exposant une route d’initiation du paiement et une route de statut. Ne jamais appeler l’API LigdiCash directement depuis l’application mobile. Voir Architecture recommandée.

Architecture mobile

App mobile
    |
    | POST /api/paiement/initier

Votre backend   →   LigdiCash
    |
    | { pay_url, transaction_id }

App mobile
    |
    | Ouvre pay_url dans une WebView native

Page de paiement LigdiCash
    |
    | Redirige vers return_url ou cancel_url

WebView détecte l'URL → ferme la WebView
    |
    | GET /api/paiement/:id/statut

Votre backend (interroge LigdiCash si needed)
    |
    | { statut: "completed" | "pending" | "notcompleted" }

App mobile — affiche le résultat
Ne jamais ouvrir l’URL LigdiCash dans le navigateur système. Cela fait quitter l’application et rend le retour impossible à détecter proprement. Utilisez une WebView native ou, en fallback acceptable, SFSafariViewController / Chrome Custom Tabs avec deep link.

React Native

Installation

npm install react-native-webview
# iOS uniquement
npx pod-install

Composant WebView de paiement

React Native
import { useState } from "react";
import { ActivityIndicator, StyleSheet, View, Alert } from "react-native";
import { WebView } from "react-native-webview";

/**
 * @param {string} paymentUrl   - URL retournée par votre backend (response_text LigdiCash)
 * @param {Function} onSuccess  - Appelé quand return_url est détectée
 * @param {Function} onCancel   - Appelé quand cancel_url est détectée
 */
export function PaiementWebView({ paymentUrl, onSuccess, onCancel }) {
  const [loading, setLoading] = useState(true);

  const RETURN_URL = "https://monapp.com/paiement/succes";
  const CANCEL_URL = "https://monapp.com/paiement/annule";

  const handleNavigationChange = (navState) => {
    const { url } = navState;
    if (url.startsWith(RETURN_URL)) onSuccess();
    else if (url.startsWith(CANCEL_URL)) onCancel();
  };

  return (
    <View style={styles.container}>
      <WebView
        source={{ uri: paymentUrl }}
        onNavigationStateChange={handleNavigationChange}
        onLoadStart={() => setLoading(true)}
        onLoadEnd={() => setLoading(false)}
        onError={() => Alert.alert("Erreur", "Impossible de charger la page de paiement.")}
        javaScriptEnabled
      />
      {loading && (
        <ActivityIndicator style={styles.loader} size="large" color="#FF6B00" />
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  loader: { position: "absolute", alignSelf: "center", top: "50%" },
});

Intégration dans un écran de paiement

React Native
import { useState } from "react";
import { View, Text, TouchableOpacity, ActivityIndicator, StyleSheet } from "react-native";
import { PaiementWebView } from "./PaiementWebView";

export function EcranPaiement({ route, navigation }) {
  const { commandeId, montant, description } = route.params;

  const [payUrl, setPayUrl] = useState(null);
  const [transactionId, setTransactionId] = useState(null);
  const [statut, setStatut] = useState("idle"); // idle | loading | webview | verifying | done

  const initierPaiement = async () => {
    setStatut("loading");

    const res = await fetch("https://api.monapp.com/api/paiement/initier", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ commandeId, montant, description }),
    }).then((r) => r.json());

    if (!res.pay_url) {
      setStatut("idle");
      Alert.alert("Erreur", "Impossible d'initier le paiement. Réessayez.");
      return;
    }

    setTransactionId(res.transaction_id);
    setPayUrl(res.pay_url);
    setStatut("webview");
  };

  const verifierPaiement = async () => {
    setStatut("verifying");

    const res = await fetch(
      `https://api.monapp.com/api/paiement/${transactionId}/statut`
    ).then((r) => r.json());

    setStatut("done");

    if (res.statut === "completed") {
      navigation.replace("CommandeConfirmee", { commandeId });
    } else if (res.statut === "notcompleted") {
      navigation.replace("PaiementEchoue", { commandeId });
    } else {
      // pending : le callback n'est pas encore arrivé
      navigation.replace("PaiementEnAttente", { transactionId });
    }
  };

  if (statut === "webview") {
    return (
      <PaiementWebView
        paymentUrl={payUrl}
        onSuccess={verifierPaiement}
        onCancel={() => setStatut("idle")}
      />
    );
  }

  if (statut === "verifying") {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#FF6B00" />
        <Text style={styles.text}>Vérification du paiement…</Text>
      </View>
    );
  }

  return (
    <View style={styles.center}>
      <Text style={styles.montant}>{montant.toLocaleString()} XOF</Text>
      <Text style={styles.desc}>{description}</Text>
      <TouchableOpacity
        style={styles.bouton}
        onPress={initierPaiement}
        disabled={statut === "loading"}
      >
        {statut === "loading" ? (
          <ActivityIndicator color="#fff" />
        ) : (
          <Text style={styles.boutonTexte}>Payer maintenant</Text>
        )}
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  center: { flex: 1, justifyContent: "center", alignItems: "center", padding: 24 },
  montant: { fontSize: 32, fontWeight: "bold", marginBottom: 8 },
  desc: { fontSize: 16, color: "#666", marginBottom: 32 },
  bouton: {
    backgroundColor: "#FF6B00",
    paddingHorizontal: 32,
    paddingVertical: 16,
    borderRadius: 8,
    minWidth: 200,
    alignItems: "center",
  },
  boutonTexte: { color: "#fff", fontSize: 18, fontWeight: "600" },
  text: { marginTop: 16, fontSize: 16, color: "#666" },
});

Flutter

Dépendance

pubspec.yaml
dependencies:
  webview_flutter: ^4.0.0
  http: ^1.0.0

Widget WebView de paiement

Flutter
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

class PaiementWebView extends StatefulWidget {
  final String paymentUrl;
  final VoidCallback onSuccess;
  final VoidCallback onCancel;

  const PaiementWebView({
    required this.paymentUrl,
    required this.onSuccess,
    required this.onCancel,
    super.key,
  });

  @override
  State<PaiementWebView> createState() => _PaiementWebViewState();
}

class _PaiementWebViewState extends State<PaiementWebView> {
  late final WebViewController _controller;
  bool _loading = true;

  static const _returnUrl = "https://monapp.com/paiement/succes";
  static const _cancelUrl = "https://monapp.com/paiement/annule";

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setNavigationDelegate(NavigationDelegate(
        onPageStarted: (_) => setState(() => _loading = true),
        onPageFinished: (_) => setState(() => _loading = false),
        onNavigationRequest: (request) {
          if (request.url.startsWith(_returnUrl)) {
            widget.onSuccess();
            return NavigationDecision.prevent;
          }
          if (request.url.startsWith(_cancelUrl)) {
            widget.onCancel();
            return NavigationDecision.prevent;
          }
          return NavigationDecision.navigate;
        },
      ))
      ..loadRequest(Uri.parse(widget.paymentUrl));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Paiement"),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: widget.onCancel,
        ),
      ),
      body: Stack(
        children: [
          WebViewWidget(controller: _controller),
          if (_loading) const Center(child: CircularProgressIndicator()),
        ],
      ),
    );
  }
}

Écran de paiement complet

Flutter
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'paiement_webview.dart';

class EcranPaiement extends StatefulWidget {
  final String commandeId;
  final int montant;
  final String description;

  const EcranPaiement({
    required this.commandeId,
    required this.montant,
    required this.description,
    super.key,
  });

  @override
  State<EcranPaiement> createState() => _EcranPaiementState();
}

class _EcranPaiementState extends State<EcranPaiement> {
  String _statut = "idle";
  String? _payUrl;
  String? _transactionId;

  Future<void> _initierPaiement() async {
    setState(() => _statut = "loading");

    final res = await http.post(
      Uri.parse("https://api.monapp.com/api/paiement/initier"),
      headers: {"Content-Type": "application/json"},
      body: jsonEncode({
        "commandeId": widget.commandeId,
        "montant": widget.montant,
        "description": widget.description,
      }),
    );

    final data = jsonDecode(res.body) as Map<String, dynamic>;

    if (data["pay_url"] == null) {
      setState(() => _statut = "idle");
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Impossible d'initier le paiement.")),
        );
      }
      return;
    }

    setState(() {
      _payUrl = data["pay_url"] as String;
      _transactionId = data["transaction_id"] as String;
      _statut = "webview";
    });
  }

  Future<void> _verifierPaiement() async {
    setState(() => _statut = "verifying");

    final res = await http.get(
      Uri.parse("https://api.monapp.com/api/paiement/$_transactionId/statut"),
    );

    final data = jsonDecode(res.body) as Map<String, dynamic>;
    final statut = data["statut"] as String?;

    if (!mounted) return;

    if (statut == "completed") {
      Navigator.of(context).pushReplacementNamed(
        "/commande-confirmee",
        arguments: widget.commandeId,
      );
    } else if (statut == "notcompleted") {
      Navigator.of(context).pushReplacementNamed("/paiement-echoue");
    } else {
      // pending — le callback n'est pas encore arrivé
      Navigator.of(context).pushReplacementNamed(
        "/paiement-en-attente",
        arguments: _transactionId,
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_statut == "webview") {
      return PaiementWebView(
        paymentUrl: _payUrl!,
        onSuccess: _verifierPaiement,
        onCancel: () => setState(() => _statut = "idle"),
      );
    }

    if (_statut == "verifying") {
      return const Scaffold(
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 16),
              Text("Vérification du paiement…"),
            ],
          ),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(title: const Text("Paiement")),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                "${widget.montant.toString()} XOF",
                style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 8),
              Text(widget.description, style: const TextStyle(color: Colors.grey)),
              const SizedBox(height: 32),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: _statut == "loading" ? null : _initierPaiement,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFFF6B00),
                    padding: const EdgeInsets.symmetric(vertical: 16),
                  ),
                  child: _statut == "loading"
                      ? const CircularProgressIndicator(color: Colors.white)
                      : const Text(
                          "Payer maintenant",
                          style: TextStyle(fontSize: 18, color: Colors.white),
                        ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Gérer l’état “pending”

Quand le callback n’est pas encore arrivé au moment où l’utilisateur revient dans l’application, la transaction est pending. Deux approches :
Affichez un écran « Vérification en cours » et interrogez votre backend toutes les 3 secondes pendant 30 secondes maximum.
Flutter — polling
Future<String> attendreStatut(String transactionId) async {
  for (var i = 0; i < 10; i++) {
    await Future.delayed(const Duration(seconds: 3));

    final res = await http.get(
      Uri.parse("https://api.monapp.com/api/paiement/$transactionId/statut"),
    );
    final statut = jsonDecode(res.body)["statut"] as String;

    if (statut != "pending") return statut;
  }
  return "pending"; // toujours pending après 30s
}

Points de vigilance

La détection de return_url ou cancel_url dans la WebView est un signal d’interface, pas une preuve de paiement. Confirmez toujours le statut via votre backend avant d’afficher une confirmation à l’utilisateur.
Sur iOS, WKWebView (utilisé par react-native-webview et webview_flutter) bloque les requêtes vers des URLs HTTP si App Transport Security est activé. Utilisez des URLs HTTPS pour votre return_url et cancel_url.
Passez le transaction_id dans les paramètres de query de return_url et cancel_url (?txn=txn_abc123) pour l’extraire facilement dans la WebView sans avoir à le stocker en état global.

Pages associées