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.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.
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 ispending. Two approaches:
- App-side polling
- Push notification
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
}
Your backend receives the LigdiCash callback and sends a push notification to the app via FCM or APNs. The app refreshes the screen when it arrives.This is the cleanest approach: it removes polling and ensures the user sees the result as soon as LigdiCash confirms it, even if they moved the app to the background.
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.Related pages
- Native mobile integration (hosted payin) — detailed WebView for iOS (Swift) and Android (Kotlin)
- Recommended architecture — backend proxy structure
- Callback security — server-side re-verification
- E-commerce guide — the same flow on the web
