Appearance
WebView Integration
Feature Type
PWA Editor Feature - Hosting the editor inside a native iOS or Android application, and the host-side responsibilities that come with it.
Embedding the editor in a native WebView is not the same as embedding it in an iframe. A browser gives a web page a set of guarantees - the back button navigates, confirm() shows a dialog, a swipe traverses history. A WebView gives it none of these by default. Each one is a delegate the host application chooses to implement or leave unimplemented, and where it is left unimplemented the editor's own handling cannot substitute for it.
This page covers what the editor does on its side, what the host has to do on its side, and what simply cannot be made to work.
Talking to the Editor
The editor pushes messages up to the host at key points - project saved, user exited, checkout started, order confirmed - over the platform's native bridge:
| Platform | Transport |
|---|---|
iOS (WKWebView) | window.webkit.messageHandlers.appHandler.postMessage(...) |
Android (WebView) | window.AndroidPbaiAtelier.sendToNative(...) |
| React Native | window.ReactNativeWebView.postMessage(...) |
The message shapes, the full list of scenarios, and worked handler examples all live in Channel Messaging - start there. The rest of this page assumes you are already receiving those messages, because the navigation guidance below depends on them.
Android Bridge Name
The Android interface is registered as AndroidPbaiAtelier. Registering a different name silently drops every message.
Back Navigation
What the Editor Does
On the upload and editor screens the editor traps a back navigation and raises its own exit confirmation, rather than letting the user leave a project mid-flight. On checkout it deliberately does not - back there is left to the host and the browser.
That trap is built on a spare history entry. It is reliable in a browser. It is conditional in a WebView, and the conditions differ per platform.
iOS (WKWebView)
allowsBackForwardNavigationGestures is false by default. Two coherent choices:
- Leave it
false. The edge swipe does nothing, the user navigates only through your chrome and ours, and back never reaches the editor at all. Simplest, and the one we recommend unless you have a reason otherwise. - Set it
true. The edge swipe traverses WebView history and the editor's trap applies. This works well on iOS: the swipe delivers touch events to the page as it happens, which is what lets the editor arm its guard in time to catch it.
What you should not do is enable it and also implement your own swipe handling on top - you will get two competing interpretations of the same gesture.
Android (WebView)
The system back button and the edge-swipe gesture are the same input, and both are delivered to your Activity, not to the WebView. If you do nothing, back closes the activity and the user loses the editor instantly, with no confirmation and no channel message.
Forward it explicitly:
kotlin
onBackPressedDispatcher.addCallback(this) {
if (webView.canGoBack()) webView.goBack() else finish()
}Android Cannot Always Confirm
Even correctly forwarded, the editor can only trap back on Android once the user has interacted with the page. Chrome's history-manipulation policy ignores history entries added by a page that the user has not engaged with - deliberately, to stop pages trapping people who have not chosen to be there. The same policy gates beforeunload and the Navigation API, so there is no alternative route to it.
In practice the gap is small: normal in-editor navigation carries the interaction forward, so a user who has done any work is covered. What is exposed is a screen the user has landed on and touched nothing on. On iOS this gap does not exist, because the swipe is itself the interaction.
The Reliable Signal Is the Channel Message
Because neither platform guarantees the trap, do not treat "the user pressed back" as your exit signal. Treat the channel message as the exit signal. The editor emits a message when the user genuinely leaves - saving, discarding, or deleting the project - and those fire on every platform regardless of how navigation is wired. See User Saves and Exits Editor and the scenarios around it.
Dialogs and Alerts
A WebView suppresses JavaScript dialogs unless the host implements the delegate that displays them. There is no error and no console warning - confirm() simply returns false and execution continues.
| Platform | Delegate | Methods |
|---|---|---|
| iOS | WKUIDelegate | runJavaScriptAlertPanelWithMessage, runJavaScriptConfirmPanelWithMessage, runJavaScriptTextInputPanelWithPrompt |
| Android | WebChromeClient | onJsAlert, onJsConfirm, onJsBeforeUnload |
The Editor's Own Dialogs Are Unaffected
The exit confirmation, the delete-project warning and every other prompt the editor shows are rendered in HTML inside the page. They need no delegate and work everywhere, including a WebView with no WKUIDelegate at all.
What the delegates buy you is the browser's own "leave site?" prompt on beforeunload. The editor registers that handler while an upload or unsaved work is in flight, as a last line of defence for a departure it could not intercept - a hard close, a swipe it was not armed for. Without the delegate, that prompt never appears and the departure is silent.
Whether it is worth implementing is a judgement call. If your app can only leave the editor through your own chrome, you already control that moment and the prompt adds little. If the user can close a tab, background the app, or navigate away by routes you do not mediate, it is the only thing standing between them and a lost project.
Common Pitfalls
These are ordered by how badly they break the product, not by how likely you are to hit them. The first two stop the editor working at all.
The Photo Picker Never Opens (Android)
<input type="file"> does nothing in an Android WebView unless the host implements WebChromeClient.onShowFileChooser(). No error, no picker, no console output - the tap simply does not respond. For a product whose entire first step is choosing photos, this is a total failure that looks like a frontend bug.
kotlin
webView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, params: FileChooserParams?
): Boolean {
// launch your picker, then hand the result back through filePathCallback
return true
}
}You must call filePathCallback exactly once, including with null when the user cancels. Dropping it leaves the input permanently unresponsive for the rest of the session - a second tap will not re-trigger the chooser.
iOS needs no equivalent; WKWebView presents the system picker itself.
window.open Is Silently Swallowed
Every native wrapper - React Native, WKWebView, Android WebView - drops window.open without raising anything. Any flow that opens a second window (the Google Photos picker, marketing links, a partner site) dies quietly.
The editor already detects this and hands the URL to your bridge instead, as an openExternalUrl action. You must handle that action or those flows have no route out. See Opening External URLs.
The Content Process Is Killed on Large Selections (iOS)
WKWebView renders in a separate process with its own memory ceiling, and the editor is memory-hungry exactly when it matters most: a user selecting 100+ photos triggers checksums and thumbnail generation across the whole set. Exceed the ceiling and iOS terminates the content process - the user sees a blank white view, and every piece of in-flight upload state goes with it.
Implement the termination callback and recover rather than leaving a white screen:
swift
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
webView.reload()
}This Is Indistinguishable From a Crash in Our Telemetry
A killed content process fires no pagehide, so our abandonment tracking never sees it. If your users report uploads that "just stopped" on iOS with large batches and we have no matching event, this is the first thing to check.
Pull-to-Refresh Destroys In-Flight Work (Android)
If the WebView is wrapped in a SwipeRefreshLayout, a downward swipe reloads the page. In the editor that discards an upload in progress and any unsaved edits, and the gesture is easy to trigger by accident while scrolling a photo list.
Disable it on editor routes, or drop it entirely - the editor has no content that benefits from a manual refresh.
Storage Is Sandboxed and Evictable
Cookies, localStorage and IndexedDB inside a WebView belong to your app installation, not to any browser. Two consequences:
- The editor stages upload state in IndexedDB. If the OS reclaims that storage, or the app clears it on logout, an interrupted upload cannot resume.
- Analytics identity does not carry from a browser session, and commonly does not survive an app restart. That is a separate topic with its own remedy - see Cross-Frame Identity for the
nativeClientIdparameter.
Departure Signals Are Weaker Than in a Browser
The editor reports abandoned uploads on pagehide, and that event is best-effort even in a browser. In a WebView it is weaker still: an app suspended by the OS, force-quit by the user, or terminated for memory may never fire it. Treat our abandonment figures from WebView traffic as a floor, not a count.
Checklist
| Item | iOS | Android |
|---|---|---|
| Bridge registered under the expected name | appHandler | AndroidPbaiAtelier |
| Channel messages received and acted on | Required | Required |
| File chooser wired up | Automatic | onShowFileChooser() - required |
openExternalUrl bridge action handled | Required | Required |
| Back input routed into the WebView | allowsBackForwardNavigationGestures (opt-in) | OnBackPressedDispatcher → goBack() (required) |
| JavaScript dialogs displayed | WKUIDelegate | WebChromeClient |
| Content-process termination recovered | webViewWebContentProcessDidTerminate | n/a |
| Pull-to-refresh disabled on editor routes | n/a | Recommended |
| Analytics identity injected | nativeClientId | nativeClientId |
| Exit treated as the channel message, not the gesture | Required | Required |