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.
On mobile, opening the payment URL in the system browser pulls the user out of the app and makes the return tricky to handle. A native WebView keeps the user inside the app, lets you automatically detect the end of the payment flow, and triggers verification without any extra action from the user.
Native WebView vs external browser
| Approach | Automatic return | UX | Recommended |
|---|
| Native WebView | Yes — URL-based detection | Smooth, stays in the app | ✅ |
| SFSafariViewController / Chrome Custom Tabs | Partial — deep link required | Briefly leaves the app | Acceptable |
| System browser | No | Leaves the app | ✗ |
SFSafariViewController (iOS) and Chrome Custom Tabs (Android) do not let you intercept redirects to your return URLs. They are not suitable for this use case without setting up a deep link.
React Native
Dependency: react-native-webview
npm install react-native-webview
# iOS only
npx pod-install
import { useState } from "react";
import { ActivityIndicator, StyleSheet, View } from "react-native";
import { WebView } from "react-native-webview";
export function PaymentWebView({ paymentUrl, onSuccess, onCancel, onError }) {
const [loading, setLoading] = useState(true);
const handleNavigationChange = (navState) => {
const { url } = navState;
if (url.startsWith("https://myapp.com/payment/success")) {
onSuccess();
} else if (url.startsWith("https://myapp.com/payment/cancel")) {
onCancel();
}
};
return (
<View style={styles.container}>
<WebView
source={{ uri: paymentUrl }}
onNavigationStateChange={handleNavigationChange}
onLoadStart={() => setLoading(true)}
onLoadEnd={() => setLoading(false)}
onError={() => onError?.()}
javaScriptEnabled
/>
{loading && (
<ActivityIndicator style={styles.loader} size="large" color="#0066cc" />
)}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
loader: { position: "absolute", alignSelf: "center", top: "50%" },
});
Replace https://myapp.com/payment/success and https://myapp.com/payment/cancel with the exact return_url and cancel_url passed at invoice creation.
Flutter
Dependency: webview_flutter
dependencies:
webview_flutter: ^4.0.0
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;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onPageStarted: (_) => setState(() => _loading = true),
onPageFinished: (_) => setState(() => _loading = false),
onNavigationRequest: (request) {
final url = request.url;
if (url.startsWith("https://myapp.com/payment/success")) {
widget.onSuccess();
return NavigationDecision.prevent;
} else if (url.startsWith("https://myapp.com/payment/cancel")) {
widget.onCancel();
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
},
))
..loadRequest(Uri.parse(widget.paymentUrl));
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(controller: _controller),
if (_loading)
const Center(child: CircularProgressIndicator()),
],
);
}
}
iOS (Swift / WKWebView)
import UIKit
import WebKit
class PaymentViewController: UIViewController, WKNavigationDelegate {
private var webView: WKWebView!
var paymentUrl: URL!
var onSuccess: (() -> Void)?
var onCancel: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
webView = WKWebView(frame: view.bounds)
webView.navigationDelegate = self
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)
webView.load(URLRequest(url: paymentUrl))
}
func webView(
_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
) {
let url = navigationAction.request.url?.absoluteString ?? ""
if url.hasPrefix("https://myapp.com/payment/success") {
decisionHandler(.cancel)
dismiss(animated: true) { self.onSuccess?() }
} else if url.hasPrefix("https://myapp.com/payment/cancel") {
decisionHandler(.cancel)
dismiss(animated: true) { self.onCancel?() }
} else {
decisionHandler(.allow)
}
}
}
Presenting the ViewController:
let vc = PaymentViewController()
vc.paymentUrl = URL(string: data.response_text)!
vc.onSuccess = {
// dismiss is handled automatically, trigger verification
self.verifyPayment()
}
vc.onCancel = {
self.showCancellationMessage()
}
present(vc, animated: true)
Android (Kotlin / WebView)
import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class PaymentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val paymentUrl = intent.getStringExtra("payment_url") ?: return finish()
val webView = WebView(this).apply {
settings.javaScriptEnabled = true
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val url = request.url.toString()
return when {
url.startsWith("https://myapp.com/payment/success") -> {
setResult(RESULT_OK)
finish()
true
}
url.startsWith("https://myapp.com/payment/cancel") -> {
setResult(RESULT_CANCELED)
finish()
true
}
else -> false
}
}
}
loadUrl(paymentUrl)
}
setContentView(webView)
}
}
Starting the Activity and retrieving the result:
val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
when (result.resultCode) {
Activity.RESULT_OK -> verifyPayment()
Activity.RESULT_CANCELED -> showCancellationMessage()
}
}
val intent = Intent(this, PaymentActivity::class.java).apply {
putExtra("payment_url", data.response_text)
}
launcher.launch(intent)
Do not forget to declare PaymentActivity in your AndroidManifest.xml:
<activity android:name=".PaymentActivity" />
After return detection
The flow is identical across platforms:
Close the WebView
Pop or dismiss the WebView component to go back to the previous screen.
Show a verification screen
Display a loading indicator — “Verifying payment…” — while your backend is queried.
Call your backend
Your backend calls the confirm endpoint with the token stored at invoice creation.
Display the result
Based on the returned status: confirm the order (completed), inform the user (notcompleted), or invite them to wait (pending).
Detecting return_url is a UI signal, not proof of payment. A user can navigate manually to that URL. Always confirm the payment server-side before fulfilling the order.
Related pages