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. 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
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_launcherwithLaunchMode.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
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
completeUrlandfailUrlto a page on your site. The page shows the result and a button that opens your app with a universal link (iOS), an app link (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
completeUrlandfailUrlto a URL with your app's custom scheme, for examplemyapp://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()
<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 or from Get Payment.
For the best wallet experience in a native app, you can also use the native SDKs: Apple Pay for iOS apps and Google Pay for Android apps.
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
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
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
WKWebViewsupports 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
- iOS: Google Pay is not available in iOS WebViews. MONEI does not show the button there.
- Android: Android
WebViewsupports 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)
}
<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. In Flutter, webview_flutter_android exposes the same setting (setPaymentRequestEnabled) and needs the same <queries> entries.
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
Visa Click to Pay is not offered inside iOS embedded WebViews. Mastercard Click to Pay is still offered.
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 yourWKUIDelegate. - Android: call
settings.setSupportMultipleWindows(true)and implementWebChromeClient.onCreateWindow. - Flutter:
webview_flutterloads a new window in the same WebView. If PayPal does not complete, use the in-app browser.
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 yourWKNavigationDelegate. - Android:
WebViewClient.shouldOverrideUrlLoading. UseIntent.parseUri(url, Intent.URI_INTENT_SCHEME)forintent://links. - Flutter:
NavigationDelegate.onNavigationRequest, returningNavigationDecision.preventafter you launch the link. - React Native:
onShouldStartLoadWithRequest, returningfalseafter you open the link withLinking.openURL.
Common questions
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, and read the other embedded WebView differences. The in-app browser needs less work and supports more payment methods.
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 instead.
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 or a WebView that catches the navigation.
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 and Google Pay conditions above, or open the checkout in the in-app browser.