Embedding Mobile App Guidelines

Embedding a hosted Breeze payment page inside a native mobile app keeps the checkout in your app instead of bouncing the user out to a browser. Unlike the web, which container you choose changes which payment methods and sign-in flows actually work - a raw WebView silently loses Google Pay and blocks federated sign-in, while a system browser container keeps both.

This page covers how to pick a container on iOS and Android, and what each one can and cannot do.

ℹ️

Short version: hand the payment page to the external browser - Safari on iOS, the default browser on Android. That is the only container where every payment method and sign-in flow is available, and it needs a return_url deep link to bring the user back.

If the flow has to stay inside your app, use an in-app browser tab: SFSafariViewController on iOS, Chrome Custom Tabs on Android. These are the recommended in-app option, and cost almost nothing against the external browser. Reach for a raw WebView only when you genuinely need the checkout inline in your own layout, and accept the trade-offs below.

How It Works

  1. Your application creates a payment session on your backend server
  2. The container loads the hosted payment URL
  3. Users complete payment within the container
  4. Payment results are communicated back to your application via URL callbacks, deep links, or dismissal callbacks
  5. Your application closes the container and validates the payment page status against your backend server
  6. Your application handles the success or failure states appropriately

Choosing a container - iOS

MethodMin iOSApple PayGoogle PayGoogle / federated sign-inShares Safari sessionInline in your UI
External Safari (openURL)10.0
SFSafariViewController9.0⚠️
ASWebAuthenticationSession12.0
WKWebView (raw embed)8.0⚠️
SwiftUI WebView26.0⚠️

Rows are ordered most-capable first. SwiftUI's WebView uses the same WebKit engine as WKWebView, so it behaves identically for payments - being newer does not lift any of the restrictions.

Choosing a container - Android

MethodGoogle PayGoogle / federated sign-inShares browser sessionInline in your UICustom User-Agent
External browser (ACTION_VIEW)
Chrome Custom Tabs
WebView (raw embed)

Ordered to match the iOS table above. The external browser and Chrome Custom Tabs are equally capable here, so choose between them on whether leaving your app is acceptable.

Apple Pay is iOS-only and so does not appear here.

What each capability depends on

Apple Pay - supported in WKWebView since iOS 13 with no entitlement required, but it is disabled if your app injects JavaScript before the payment. A single evaluateJavaScript call or WKUserScript is enough, whether it is a User-Agent readout, an analytics bridge or a console hook. It works normally in SFSafariViewController and Safari. It is not a payment surface in ASWebAuthenticationSession.

Google Pay - a plain WKWebView reports not-ready, so Google Pay never offers itself there. On Android, Google blocks payment sheet rendering in WebView contexts entirely. It works in Safari, in an external browser, and in Chrome Custom Tabs. In SFSafariViewController it is plausible but unverified - same Safari engine, but we have not confirmed it end to end; test it before relying on it. It is also unavailable in social in-app browsers such as Facebook's and Instagram's, which impose their own restrictions - worth knowing if a meaningful share of your traffic arrives from those apps.

Google / federated sign-in - blocked inside WKWebView and Android WebView: Google returns disallowed_useragent for embedded webviews, per RFC 8252 §8.1. It works in SFSafariViewController, ASWebAuthenticationSession, Chrome Custom Tabs, and external browsers, because those are real system browsers.

Session sharing - WKWebView and Android WebView keep their own isolated data stores. SFSafariViewController stopped sharing Safari's cookies and session at iOS 11, so it has no access to AutoFill, history, or website data. ASWebAuthenticationSession shares the Safari session by default; opt out with prefersEphemeralWebBrowserSession.

Inline embedding - only WKWebView, SwiftUI WebView and Android WebView are views you place in your own layout. Apple forbids embedding SFSafariViewController, which must be presented modally, and the browser handoff methods are system-owned.

Autofill - not supported on iOS WebViews, for security reasons. Android WebViews support it.

⚠️

The raw WebView trade-off

A raw WebView is the only way to put the checkout inline in your own UI, and it is also the only container that loses both Google Pay and federated sign-in. If your checkout offers Google Pay, or if buyers sign in with Google, a raw WebView will visibly break those paths - not with an error, but by silently omitting the option.

Inside a WebView, the methods that remain available are card (manual entry), Apple Pay and crypto.

Implementation examples

iOS

External Safari - recommended

UIApplication.shared.open(URL(string: paymentUrl)!)
// Every payment method and sign-in flow works here.
// The user leaves your app, so a return_url deep link is required to bring
// them back - see Best practices below.

SFSafariViewController - recommended for in-app

import SafariServices

let vc = SFSafariViewController(url: URL(string: paymentUrl)!)
present(vc, animated: true)
// In-app Safari: shares Safari's engine, so federated sign-in works, and the
// user stays inside your app.

WKWebView - inline embed

import WebKit

let webView = WKWebView()
webView.load(
    URLRequest(url: URL(string: paymentUrl)!, cachePolicy: .reloadIgnoringLocalCacheData)
)
// Present `webView` (push / modal).
// ⚠️ Google login is blocked here (disallowed_useragent), Google Pay is
//    unavailable, and any evaluateJavaScript / WKUserScript before payment
//    also disables Apple Pay.

Android

External browser - recommended

context.startActivity(
    Intent(Intent.ACTION_VIEW, Uri.parse(paymentUrl))
)
// Every payment method and sign-in flow works here.
// The user leaves your app, so a return_url deep link is required to bring
// them back - see Best practices below.

Chrome Custom Tabs - recommended for in-app

import androidx.browser.customtabs.CustomTabsIntent

CustomTabsIntent.Builder().build()
    .launchUrl(context, Uri.parse(paymentUrl))
// In-app Chrome tab: shares Chrome's engine, so federated sign-in works, and
// the user stays inside your app.

WebView - inline embed

import android.webkit.WebSettings
import android.webkit.WebView

val webView = WebView(context)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
webView.loadUrl(paymentUrl)
// ⚠️ Google login is blocked here (disallowed_useragent) and Google Pay
//    is unavailable in WebView contexts.

React Native

react-native-webview is a raw platform WebView on both iOS and Android, so the WebView row of each table above applies to it - including the loss of Google Pay and federated sign-in.

import React from 'react';
import { View } from 'react-native';
import { WebView } from 'react-native-webview';

const PaymentWebView = ({ paymentUrl }: { paymentUrl: string }) => (
  <View style={{ flex: 1 }}>
    <WebView
      source={{ uri: paymentUrl }}
      javaScriptEnabled={true}
      domStorageEnabled={true}
      cacheEnabled={false}
      cacheMode="LOAD_NO_CACHE"
    />
  </View>
);

To learn that the payment finished, use a return_url deep link and confirm the result against your
own backend - see Best practices below. Do not infer it by watching the WebView's URL: matching on a
path or a query string is guesswork about a URL we may change, and it tells you what the payer's
browser did rather than what we recorded.

Best practices

1. Deep link redirection

Always support a return_url - a deep link or universal link. If the deep link fails, fall back to an HTTPS universal link your app can open. This matters most for the browser handoff methods, where the user is outside your app when the payment completes.

2. Do not cache the payment page

A payment page is single-use, and its status changes on our side rather than in the page. A stale
copy therefore shows a payer a page that may already have been paid, expired or cancelled.

We serve the page document with Cache-Control: no-cache, so a client that honours it revalidates
before reusing anything. An embedded WebView is the case where that is not enough: Android's
WebView defaults to LOAD_DEFAULT, which will serve a cached document without revalidating when
the network is slow or unavailable, and back-navigation inside a WebView can restore a page from
memory rather than reloading it. If you embed inline, set the cache mode explicitly:

ContainerHow
Android WebViewsettings.cacheMode = WebSettings.LOAD_NO_CACHE
iOS WKWebViewURLRequest(url:, cachePolicy: .reloadIgnoringLocalCacheData)
react-native-webviewcacheEnabled={false}, plus cacheMode="LOAD_NO_CACHE" on Android

SFSafariViewController, Chrome Custom Tabs and the external browser need nothing — they honour the
headers, and expose no cache setting to override them anyway.

Two things worth knowing before you reach for a bigger hammer:

  • These settings also bypass the asset cache, not just the document. Our static assets are served
    public, max-age=43200 on purpose, and their filenames are content-hashed, so caching them is
    safe. Turning the cache off refetches the whole bundle on every load, which costs you first-paint
    time on a slow connection. If that matters more than the edge cases above, leaving the cache alone
    and relying on no-cache is a defensible choice.
  • Do not use an ephemeral or incognito store to achieve itwebsiteDataStore = .nonPersistent()
    on iOS, incognito in react-native-webview. Those clear cookies as well as the cache, which
    signs the payer out of any session they had and prevents saved payment methods from being offered.

3. Verify status on your backend

When your app detects that the payment status has changed, always validate it against your backend. Do not trust a client-side signal - a URL callback, a dismissal, or a message from the page - on its own.

4. Test on real hardware

Google's disallowed_useragent enforcement, and the difference in cookie behaviour between SFSafariViewController and WKWebView, are most faithfully reproduced on real devices rather than simulators. Apple Pay sandbox testing additionally requires a sandbox tester Apple ID signed in to iCloud - see Apple's Apple Pay Sandbox Testing.

Sources


Did this page help you?