Skip to main content

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.

This guide shows how to integrate LigdiCash in a mobile application. It covers the recommended architecture, opening the payment page in a WebView, detecting the user’s return, and confirming the payment on your backend. Prerequisites: you have a backend exposing a payment initiation route and a status route. Never call the LigdiCash API directly from a mobile app. See Recommended architecture.

Mobile architecture

Mobile app
    |
    | POST /api/payment/initiate

Your backend   →   LigdiCash
    |
    | { pay_url, transaction_id }

Mobile app
    |
    | Opens pay_url in a native WebView

LigdiCash payment page
    |
    | Redirects to return_url or cancel_url

WebView detects the URL → closes the WebView
    |
    | GET /api/payment/:id/status

Your backend (asks LigdiCash if needed)
    |
    | { status: "completed" | "pending" | "notcompleted" }

Mobile app — shows the result
Never open the LigdiCash URL in the system browser. That exits the app and makes it impossible to detect the return cleanly. Use a native WebView or, as an acceptable fallback, SFSafariViewController / Chrome Custom Tabs with a deep link.

React Native

Installation

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

Payment WebView component

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

/**
 * @param {string} paymentUrl   - URL returned by your backend (LigdiCash response_text)
 * @param {Function} onSuccess  - Called when return_url is detected
 * @param {Function} onCancel   - Called when cancel_url is detected
 */
export function PaymentWebView({ paymentUrl, onSuccess, onCancel }) {
  const [loading, setLoading] = useState(true);

  const RETURN_URL = "https://myapp.com/payment/success";
  const CANCEL_URL = "https://myapp.com/payment/cancel";

  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("Error", "Could not load the payment page.")}
        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%" },
});

Using it in a payment screen

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

export function PaymentScreen({ route, navigation }) {
  const { orderId, amount, description } = route.params;

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

  const initiatePayment = async () => {
    setStatus("loading");

    const res = await fetch("https://api.myapp.com/api/payment/initiate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ orderId, amount, description }),
    }).then((r) => r.json());

    if (!res.pay_url) {
      setStatus("idle");
      Alert.alert("Error", "Could not initiate the payment. Try again.");
      return;
    }

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

  const verifyPayment = async () => {
    setStatus("verifying");

    const res = await fetch(
      `https://api.myapp.com/api/payment/${transactionId}/status`
    ).then((r) => r.json());

    setStatus("done");

    if (res.status === "completed") {
      navigation.replace("OrderConfirmed", { orderId });
    } else if (res.status === "notcompleted") {
      navigation.replace("PaymentFailed", { orderId });
    } else {
      // pending: the callback hasn't arrived yet
      navigation.replace("PaymentWaiting", { transactionId });
    }
  };

  if (status === "webview") {
    return (
      <PaymentWebView
        paymentUrl={payUrl}
        onSuccess={verifyPayment}
        onCancel={() => setStatus("idle")}
      />
    );
  }

  if (status === "verifying") {
    return (
      <View style={styles.center}>
        <ActivityIndicator size="large" color="#FF6B00" />
        <Text style={styles.text}>Verifying payment…</Text>
      </View>
    );
  }

  return (
    <View style={styles.center}>
      <Text style={styles.amount}>{amount.toLocaleString()} CFA</Text>
      <Text style={styles.desc}>{description}</Text>
      <TouchableOpacity
        style={styles.button}
        onPress={initiatePayment}
        disabled={status === "loading"}
      >
        {status === "loading" ? (
          <ActivityIndicator color="#fff" />
        ) : (
          <Text style={styles.buttonText}>Pay now</Text>
        )}
      </TouchableOpacity>
    </View>
  );
}

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

Flutter

Dependency

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

Payment WebView widget

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

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

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

  @override
  State<PaymentWebView> createState() => _PaymentWebViewState();
}

class _PaymentWebViewState extends State<PaymentWebView> {
  late final WebViewController _controller;
  bool _loading = true;

  static const _returnUrl = "https://myapp.com/payment/success";
  static const _cancelUrl = "https://myapp.com/payment/cancel";

  @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("Payment"),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: widget.onCancel,
        ),
      ),
      body: Stack(
        children: [
          WebViewWidget(controller: _controller),
          if (_loading) const Center(child: CircularProgressIndicator()),
        ],
      ),
    );
  }
}

Full payment screen

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

class PaymentScreen extends StatefulWidget {
  final String orderId;
  final int amount;
  final String description;

  const PaymentScreen({
    required this.orderId,
    required this.amount,
    required this.description,
    super.key,
  });

  @override
  State<PaymentScreen> createState() => _PaymentScreenState();
}

class _PaymentScreenState extends State<PaymentScreen> {
  String _status = "idle";
  String? _payUrl;
  String? _transactionId;

  Future<void> _initiatePayment() async {
    setState(() => _status = "loading");

    final res = await http.post(
      Uri.parse("https://api.myapp.com/api/payment/initiate"),
      headers: {"Content-Type": "application/json"},
      body: jsonEncode({
        "orderId": widget.orderId,
        "amount": widget.amount,
        "description": widget.description,
      }),
    );

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

    if (data["pay_url"] == null) {
      setState(() => _status = "idle");
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text("Could not initiate the payment.")),
        );
      }
      return;
    }

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

  Future<void> _verifyPayment() async {
    setState(() => _status = "verifying");

    final res = await http.get(
      Uri.parse("https://api.myapp.com/api/payment/$_transactionId/status"),
    );

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

    if (!mounted) return;

    if (status == "completed") {
      Navigator.of(context).pushReplacementNamed(
        "/order-confirmed",
        arguments: widget.orderId,
      );
    } else if (status == "notcompleted") {
      Navigator.of(context).pushReplacementNamed("/payment-failed");
    } else {
      // pending — the callback hasn't arrived yet
      Navigator.of(context).pushReplacementNamed(
        "/payment-waiting",
        arguments: _transactionId,
      );
    }
  }

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

    if (_status == "verifying") {
      return const Scaffold(
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              CircularProgressIndicator(),
              SizedBox(height: 16),
              Text("Verifying payment…"),
            ],
          ),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(title: const Text("Payment")),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text(
                "${widget.amount.toString()} CFA",
                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: _status == "loading" ? null : _initiatePayment,
                  style: ElevatedButton.styleFrom(
                    backgroundColor: const Color(0xFFFF6B00),
                    padding: const EdgeInsets.symmetric(vertical: 16),
                  ),
                  child: _status == "loading"
                      ? const CircularProgressIndicator(color: Colors.white)
                      : const Text(
                          "Pay now",
                          style: TextStyle(fontSize: 18, color: Colors.white),
                        ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Handling the “pending” state

When the callback hasn’t arrived yet by the time the user returns to the app, the transaction is pending. Two approaches:
Show a “Verification in progress” screen and ask your backend every 3 seconds for 30 seconds maximum.
Flutter — polling
Future<String> waitForStatus(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.myapp.com/api/payment/$transactionId/status"),
    );
    final status = jsonDecode(res.body)["status"] as String;

    if (status != "pending") return status;
  }
  return "pending"; // still pending after 30s
}

Things to watch for

Detecting return_url or cancel_url in the WebView is a UI signal, not proof of payment. Always confirm the status through your backend before showing a confirmation to the user.
On iOS, WKWebView (used by react-native-webview and webview_flutter) blocks requests to HTTP URLs when App Transport Security is on. Use HTTPS URLs for your return_url and cancel_url.
Pass the transaction_id in the query parameters of return_url and cancel_url (?txn=txn_abc123) to extract it easily from the WebView without storing it in global state.