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

# Intégration mobile native

> Intégrer le payin avec redirection dans une app iOS, Android, React Native ou Flutter : WebView, détection du retour et confirmation du paiement.

Sur mobile, ouvrir l'URL de paiement dans le navigateur système fait quitter l'application et rend le retour utilisateur difficile à gérer. La WebView native maintient l'utilisateur dans l'app, permet de détecter automatiquement la fin du flux de paiement et de déclencher la vérification sans action supplémentaire de l'utilisateur.

## WebView native vs navigateur externe

| Approche                                    | Retour automatique         | UX                       | Recommandé |
| ------------------------------------------- | -------------------------- | ------------------------ | ---------- |
| WebView native                              | Oui — détection par URL    | Fluide, reste dans l'app | ✅          |
| SFSafariViewController / Chrome Custom Tabs | Partiel — deep link requis | Sort brièvement de l'app | Acceptable |
| Navigateur système                          | Non                        | Quitte l'app             | ✗          |

<Note>
  `SFSafariViewController` (iOS) et Chrome Custom Tabs (Android) ne permettent pas d'intercepter les redirections vers vos URLs de retour. Ils ne conviennent pas pour ce cas d'usage sans mettre en place un deep link.
</Note>

## React Native

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

```bash theme={null}
npm install react-native-webview
# iOS uniquement
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 PaiementWebView({ paymentUrl, onSuccess, onCancel, onError }) {
  const [loading, setLoading] = useState(true);

  const handleNavigationChange = (navState) => {
    const { url } = navState;

    if (url.startsWith("https://monapp.com/paiement/succes")) {
      onSuccess();
    } else if (url.startsWith("https://monapp.com/paiement/annule")) {
      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>
  Remplacez `https://monapp.com/paiement/succes` et `https://monapp.com/paiement/annule` par vos `return_url` et `cancel_url` exactes passées à la création de la facture.
</Tip>

## Flutter

**Dépendance :** [`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 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;

  @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://monapp.com/paiement/succes")) {
            widget.onSuccess();
            return NavigationDecision.prevent;
          } else if (url.startsWith("https://monapp.com/paiement/annule")) {
            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 PaiementViewController: 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://monapp.com/paiement/succes") {
            decisionHandler(.cancel)
            dismiss(animated: true) { self.onSuccess?() }
        } else if url.hasPrefix("https://monapp.com/paiement/annule") {
            decisionHandler(.cancel)
            dismiss(animated: true) { self.onCancel?() }
        } else {
            decisionHandler(.allow)
        }
    }
}
```

**Présenter le ViewController :**

```swift iOS (Swift) theme={null}
let vc = PaiementViewController()
vc.paymentUrl = URL(string: data.response_text)!
vc.onSuccess = {
    // fermeture effectuée automatiquement, lancer la vérification
    self.verifierPaiement()
}
vc.onCancel = {
    self.afficherMessageAnnulation()
}
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 PaiementActivity : 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://monapp.com/paiement/succes") -> {
                            setResult(RESULT_OK)
                            finish()
                            true
                        }
                        url.startsWith("https://monapp.com/paiement/annule") -> {
                            setResult(RESULT_CANCELED)
                            finish()
                            true
                        }
                        else -> false
                    }
                }
            }
            loadUrl(paymentUrl)
        }

        setContentView(webView)
    }
}
```

**Démarrer l'Activity et récupérer le résultat :**

```kotlin Android (Kotlin) theme={null}
val launcher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    when (result.resultCode) {
        Activity.RESULT_OK -> verifierPaiement()
        Activity.RESULT_CANCELED -> afficherMessageAnnulation()
    }
}

val intent = Intent(this, PaiementActivity::class.java).apply {
    putExtra("payment_url", data.response_text)
}
launcher.launch(intent)
```

N'oubliez pas de déclarer `PaiementActivity` dans votre `AndroidManifest.xml` :

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

## Après la détection du retour

Le flux est identique quelle que soit la plateforme :

<Steps>
  <Step title="Fermer la WebView">
    Dépiler ou dismisser le composant WebView pour revenir à l'écran précédent.
  </Step>

  <Step title="Afficher un écran de vérification">
    Montrez un indicateur de chargement — « Vérification du paiement en cours… » — le temps d'interroger votre backend.
  </Step>

  <Step title="Appeler votre backend">
    Votre backend appelle l'endpoint `confirm` avec le token stocké à la création de la facture.
  </Step>

  <Step title="Afficher le résultat">
    Selon le `status` retourné : confirmez la commande (`completed`), informez l'utilisateur (`notcompleted`) ou invitez-le à patienter (`pending`).
  </Step>
</Steps>

<Warning>
  La détection de `return_url` est un signal d'interface, pas une preuve de paiement. Un utilisateur peut naviguer manuellement vers cette URL. Confirmez toujours le paiement côté serveur avant d'honorer la commande.
</Warning>

## Pages associées

* [Rediriger le client](/api-paiement/payin-redirect/rediriger-client) — pattern popup web et vue d'ensemble des modes d'ouverture
* [Vérifier le statut](/api-paiement/payin-redirect/verifier-statut) — appeler `confirm` après la redirection
* [Pièges courants](/api-paiement/payin-redirect/pieges-courants) — iframe bloqué et autres erreurs fréquentes
