# Accept payments in a mobile app WebView

Your app is native (iOS, Android, Flutter, React Native) and you want to take payments with the [Hosted Payment Page](https://docs.monei.com/integrations/use-prebuilt-payment-page.md). Your backend creates the payment, and your app shows its `redirectUrl`. This page tells you how to show that page inside the app, how to bring the customer back, and what changes in a WebView.

## Recommended: open the checkout in the in-app browser[​](#in-app-browser "Direct link to Recommended: open the checkout in the in-app browser")

Open the `redirectUrl` of the payment in the system in-app browser, not in an embedded WebView:

* **iOS:** `SFSafariViewController`. Apple Pay works in it as it does in Safari.
* **Android:** Custom Tabs. Every web platform feature of the browser is available in it, payment methods included.
* **Flutter:** `url_launcher` with `LaunchMode.inAppBrowserView`, which opens one of the two above.
* **React Native (Expo):** `WebBrowser.openBrowserAsync`, which opens one of the two above.

You do not configure anything for 3D Secure, Bizum, PayPal, Apple Pay or Google Pay. The checkout behaves as it does in the mobile browser.

* iOS (Swift)
* Android (Kotlin)
* Flutter
* React Native (Expo)

```
import SafariServices



let safari = SFSafariViewController(url: redirectUrl)

safari.delegate = self

present(safari, animated: true)



// SFSafariViewControllerDelegate

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {

    // Ask your backend for the payment status.

}
```

```
// implementation("androidx.browser:browser:<version>")

CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl))



// When your activity comes back to the foreground (onResume),

// ask your backend for the payment status.
```

```
await launchUrl(Uri.parse(redirectUrl), mode: LaunchMode.inAppBrowserView);

// When the app resumes, ask your backend for the payment status.
```

```
import * as WebBrowser from 'expo-web-browser';



await WebBrowser.openBrowserAsync(redirectUrl);

// The promise resolves when the customer closes the browser.

// Ask your backend for the payment status.
```

### Bring the customer back to your app[​](#return-to-app "Direct link to Bring the customer back to your app")

When the payment ends, MONEI sends the customer to the `completeUrl` of the payment. A failed, canceled or expired payment goes to the `failUrl` instead, if you set one. MONEI adds the payment `id` and `status` to the URL. You can bring the customer back in two ways:

* **A return page with a button.** Set `completeUrl` and `failUrl` to a page on your site. The page shows the result and a button that opens your app with a [universal link](https://developer.apple.com/documentation/xcode/allowing-apps-and-websites-to-link-to-your-content) (iOS), an [app link](https://developer.android.com/training/app-links) (Android) or a custom scheme. Use a button, not an automatic redirect: a browser opens another app reliably only when the customer taps a link.
* **An authentication session that closes itself.** Set `completeUrl` and `failUrl` to a URL with your app's custom scheme, for example `myapp://checkout/complete`. MONEI accepts custom-scheme URLs and redirects to them like any other URL. Open the checkout with an API that waits for that scheme. When the redirect happens, the session closes and gives your app the URL.

- iOS (Swift)
- Android (Kotlin)
- Flutter
- React Native (Expo)

```
import AuthenticationServices



let session = ASWebAuthenticationSession(url: redirectUrl, callbackURLScheme: "myapp") { callbackURL, error in

    // callbackURL is your completeUrl or failUrl with the payment id and status,

    // or nil if the customer closed the sheet.

    // Ask your backend for the payment status.

}

session.presentationContextProvider = self

// Without this, iOS asks the customer to allow the app to "sign in" with the payment site.

session.prefersEphemeralWebBrowserSession = true

session.start()
```

AndroidManifest.xml

```
<activity android:name=".CheckoutReturnActivity" android:exported="true">

  <intent-filter>

    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />

    <category android:name="android.intent.category.BROWSABLE" />

    <data android:scheme="myapp" android:host="checkout" />

  </intent-filter>

</activity>
```

Open the checkout in Custom Tabs as above. `CheckoutReturnActivity` receives the return URL in `intent.data`. Ask your backend for the payment status there.

Test this flow on a device. Chrome can block an automatic switch to an app when the customer did not just tap something, for example after a Bizum confirmation. In that case, use a return page with a button.

```
// flutter_web_auth_2

final result = await FlutterWebAuth2.authenticate(

  url: redirectUrl,

  callbackUrlScheme: 'myapp',

);

// result is your completeUrl or failUrl with the payment id and status.

// Ask your backend for the payment status.
```

On Android, `flutter_web_auth_2` also needs its callback activity in `AndroidManifest.xml`. See the plugin's setup.

```
import * as WebBrowser from 'expo-web-browser';



const result = await WebBrowser.openAuthSessionAsync(redirectUrl, 'myapp://checkout', {

  preferEphemeralSession: true

});

if (result.type === 'success') {

  // result.url is your completeUrl or failUrl with the payment id and status.

}

// Ask your backend for the payment status.
```

**Get the result from your backend, not from the return URL.** The `status` in the URL tells your app which screen to show, but it does not prove the payment succeeded. Your backend knows the status from the [webhook to your `callbackUrl`](https://docs.monei.com/integrations/use-prebuilt-payment-page.md#3-process-webhook-notification-server-side) or from [Get Payment](https://docs.monei.com/apis/rest/payments-get.md).

For the best wallet experience in a native app, you can also use the native SDKs: [Apple Pay for iOS apps](https://docs.monei.com/payment-methods/apple-pay.md#native-ios-app-integration) and [Google Pay for Android apps](https://docs.monei.com/payment-methods/google-pay.md#native-android-app-integration).

## If you use an embedded WebView[​](#embedded-webview "Direct link to If you use an embedded WebView")

An embedded WebView (`WKWebView`, Android `WebView`, `webview_flutter`, `react-native-webview`) is not a full browser. Load the `redirectUrl` in it. The checkout works, with these differences.

### Detect the result[​](#detect-result "Direct link to Detect the result")

The Hosted Payment Page sends no message to your app. When the payment ends, it navigates the WebView to your `completeUrl` or `failUrl`. Catch that navigation, close the WebView, and ask your backend for the payment status. This needs no JavaScript in the page, so Apple Pay keeps working.

* iOS (Swift)
* Android (Kotlin)
* Flutter
* React Native

```
// WKNavigationDelegate

func webView(_ webView: WKWebView,

             decidePolicyFor navigationAction: WKNavigationAction,

             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {

    if let url = navigationAction.request.url,

       url.absoluteString.hasPrefix(returnUrlPrefix) {

        decisionHandler(.cancel)

        // Close the WebView and ask your backend for the payment status.

        return

    }

    decisionHandler(.allow)

}
```

```
webView.webViewClient = object : WebViewClient() {

    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {

        if (request.url.toString().startsWith(RETURN_URL_PREFIX)) {

            // Close the WebView and ask your backend for the payment status.

            return true

        }

        return false

    }

}
```

```
controller.setNavigationDelegate(NavigationDelegate(

  onNavigationRequest: (request) {

    if (request.url.startsWith(returnUrlPrefix)) {

      // Close the WebView and ask your backend for the payment status.

      return NavigationDecision.prevent;

    }

    return NavigationDecision.navigate;

  },

));
```

```
<WebView

  source={{uri: redirectUrl}}

  onShouldStartLoadWithRequest={(request) => {

    if (request.url.startsWith(RETURN_URL_PREFIX)) {

      // Close the WebView and ask your backend for the payment status.

      return false;

    }

    return true;

  }}

/>
```

`returnUrlPrefix` is the start of your `completeUrl` and `failUrl`. It can be a page on your site or a custom-scheme URL.

### 3D Secure[​](#3d-secure "Direct link to 3D Secure")

When the card needs 3D Secure, the Hosted Payment Page takes the customer to their bank's verification page in the same WebView. When the customer finishes, the bank sends them back to the payment page. Nothing opens in a new window.

* Allow JavaScript in the WebView.
* Allow navigation to other HTTPS domains. Your navigation handler must only stop your return URL, because the bank's page is on the bank's domain.
* Do not hide or reload the WebView while the card is verified. If the page is hidden during the first verification step, for example because the customer switches apps, the payment attempt is canceled and the customer must try again.

### Apple Pay[​](#apple-pay "Direct link to Apple Pay")

* `WKWebView` supports Apple Pay. Apple disables it on pages where your app injects JavaScript (`WKUserScript`, `evaluateJavaScript`). If your app injects scripts into the checkout page, the customer gets no Apple Pay button.
* MONEI shows the Apple Pay button only when the browser reports that Apple Pay is available (`ApplePaySession.canMakePayments()`). When the WebView does not support it, the button is not shown and the other methods stay available.

### Google Pay[​](#google-pay "Direct link to Google Pay")

* **iOS:** Google Pay is not available in iOS WebViews. MONEI does not show the button there.
* **Android:** Android `WebView` supports Google Pay through the Payment Request API. You must turn it on:

```
// implementation("androidx.webkit:webkit:1.14.0") or later

if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {

    WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)

}
```

AndroidManifest.xml

```
<queries>

  <intent><action android:name="org.chromium.intent.action.PAY" /></intent>

  <intent><action android:name="org.chromium.intent.action.IS_READY_TO_PAY" /></intent>

  <intent><action android:name="org.chromium.intent.action.UPDATE_PAYMENT_DETAILS" /></intent>

</queries>
```

The customer's device needs Google Play services 25.18.30 or later and Android System WebView 137 or later. See Google's [Android WebView guide](https://developers.google.com/pay/api/android/guides/recipes/using-android-webview). In Flutter, `webview_flutter_android` exposes the same setting (`setPaymentRequestEnabled`) and needs the same `<queries>` entries.

### Bizum[​](#bizum "Direct link to Bizum")

The customer confirms a Bizum payment in their banking app. The checkout page shows "complete the payment in your banking app" and checks the payment status until it changes. It does not open the banking app for the customer.

Keep the WebView open and do not reload it while the customer is in the banking app. When they come back to your app, the page shows the result.

### Click to Pay[​](#click-to-pay "Direct link to Click to Pay")

Visa [Click to Pay](https://docs.monei.com/payment-methods/click-to-pay.md) is not offered inside iOS embedded WebViews. Mastercard Click to Pay is still offered.

### PayPal and new windows[​](#new-windows "Direct link to PayPal and new windows")

The PayPal button runs PayPal's own JavaScript SDK, which can open a new window. A WebView opens no new windows unless your app handles them:

* **iOS:** implement `webView(_:createWebViewWith:for:windowFeatures:)` in your `WKUIDelegate`.
* **Android:** call `settings.setSupportMultipleWindows(true)` and implement `WebChromeClient.onCreateWindow`.
* **Flutter:** `webview_flutter` loads a new window in the same WebView. If PayPal does not complete, use the [in-app browser](#in-app-browser).

### Links to other apps[​](#app-links "Direct link to Links to other apps")

A WebView does not open links with a non-HTTP scheme (for example `intent://` or a bank app scheme). If a payment page sends the customer to another app, catch the link and open it with the system:

* **iOS:** `webView(_:decidePolicyFor:decisionHandler:)` in your `WKNavigationDelegate`.
* **Android:** `WebViewClient.shouldOverrideUrlLoading`. Use `Intent.parseUri(url, Intent.URI_INTENT_SCHEME)` for `intent://` links.
* **Flutter:** `NavigationDelegate.onNavigationRequest`, returning `NavigationDecision.prevent` after you launch the link.
* **React Native:** `onShouldStartLoadWithRequest`, returning `false` after you open the link with `Linking.openURL`.

## Common questions[​](#common-questions "Direct link to Common questions")

### Can I use the Hosted Payment Page in an embedded WebView?[​](#hosted-page-in-webview "Direct link to Can I use the Hosted Payment Page in an embedded WebView?")

Yes. Load the `redirectUrl` in the WebView, [catch the navigation to your return URL](#detect-result), and read the other [embedded WebView](#embedded-webview) differences. The [in-app browser](#in-app-browser) needs less work and supports more payment methods.

### Does the payment page send a message to my app?[​](#post-message "Direct link to Does the payment page send a message to my app?")

No. When you load the Hosted Payment Page directly in a WebView, it sends no `postMessage` to your app. It navigates to your `completeUrl` or `failUrl`. [Catch that navigation](#detect-result) instead.

### Can completeUrl be a custom scheme like myapp\://?[​](#custom-scheme "Direct link to Can completeUrl be a custom scheme like myapp://?")

Yes. `completeUrl`, `failUrl` and `cancelUrl` accept custom-scheme URLs, and MONEI redirects to them with the payment `id` and `status` added. Use them with an [authentication session](#return-to-app) or a [WebView that catches the navigation](#detect-result).

### Why is the Apple Pay or Google Pay button missing in my app?[​](#wallet-button-missing "Direct link to Why is the Apple Pay or Google Pay button missing in my app?")

MONEI shows a wallet button only when the browser reports that the wallet is available. In a WebView, check the [Apple Pay](#apple-pay) and [Google Pay](#google-pay) conditions above, or open the checkout in the [in-app browser](#in-app-browser).
