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

# Native mobile integration

> Integrating hosted payin in an iOS, Android, React Native, or Flutter app: WebView, return detection, and payment confirmation.

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           | ✗           |

<Note>
  `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.
</Note>

## React Native

**Dependency:** [`react-native-webview`](https://github.com/react-native-webview/react-native-webview)

```bash theme={null}
npm install react-native-webview
# iOS only
npx pod-install
```

```jsx React Native theme={null}
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%" },
});
```

<Tip>
  Replace `https://myapp.com/payment/success` and `https://myapp.com/payment/cancel` with the exact `return_url` and `cancel_url` passed at invoice creation.
</Tip>

## Flutter

**Dependency:** [`webview_flutter`](https://pub.dev/packages/webview_flutter)

```yaml pubspec.yaml theme={null}
dependencies:
  webview_flutter: ^4.0.0
```

```dart Flutter theme={null}
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)

```swift iOS (Swift) theme={null}
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:**

```swift iOS (Swift) theme={null}
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)

```kotlin Android (Kotlin) theme={null}
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:**

```kotlin Android (Kotlin) theme={null}
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`:

```xml AndroidManifest.xml theme={null}
<activity android:name=".PaymentActivity" />
```

## After return detection

The flow is identical across platforms:

<Steps>
  <Step title="Close the WebView">
    Pop or dismiss the WebView component to go back to the previous screen.
  </Step>

  <Step title="Show a verification screen">
    Display a loading indicator — "Verifying payment…" — while your backend is queried.
  </Step>

  <Step title="Call your backend">
    Your backend calls the `confirm` endpoint with the token stored at invoice creation.
  </Step>

  <Step title="Display the result">
    Based on the returned `status`: confirm the order (`completed`), inform the user (`notcompleted`), or invite them to wait (`pending`).
  </Step>
</Steps>

<Warning>
  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.
</Warning>

## Related pages

* [Redirect the customer](/en/payment-api/hosted-payin/redirect-customer) — web popup pattern and overview of opening modes
* [Verify the status](/en/payment-api/hosted-payin/verify-status) — call `confirm` after the redirect
* [Common pitfalls](/en/payment-api/hosted-payin/common-pitfalls) — blocked iframe and other frequent errors
