Documentation
Native bridge (web ⇄ app)
Call the app from your page and receive events back, in both directions
The native bridge is a two-way channel between your website and the app. Your page asks the app something with WebToApp.call(method, params) and gets a promise back. The app tells your page when something happens with WebToApp.on(name, handler): it came to the foreground, the network changed, a notification was tapped, a rewarded ad finished, a purchase went through. It is always available in every app build, there is nothing to switch on in the dashboard, and it is also the transport behind the RevenueCat add-on.
Your existing code keeps working
The native bridge is built on the same window.FlutterWebView.postMessage as the JavaScript Bridge. Legacy messages such as userId, showRewardedAd and getLocation, and their DOM events, work unchanged. The new API adds to them; you do not have to migrate anything. Apps built before the native bridge existed do not have WebToApp.call: request a new build to get it.
Wait for the bridge
The app installs the client (window.WebToApp.call, .on, …) on every page and then fires a WebToAppBridgeReady event on window. In a regular browser none of this exists: window.WebToApp and window.FlutterWebView are undefined and the event never fires, so use a short timeout to decide that you are not in the app.
function whenBridgeReady(timeoutMs) {
return new Promise(function (resolve) {
if (window.WebToApp && typeof window.WebToApp.call === 'function') {
resolve(true); // already installed
return;
}
var timer = setTimeout(function () { resolve(false); }, timeoutMs || 1500);
window.addEventListener('WebToAppBridgeReady', function () {
clearTimeout(timer);
resolve(true);
}, { once: true });
});
}
async function main() {
if (!(await whenBridgeReady())) return; // regular browser
const info = await WebToApp.call('app.getInfo');
// → { bridgeVersion: 2, platform: 'ios', capabilities: { purchases: true, paywalls: true, push: 'fcm', ads: false } }
}
main();Android: do not assume it exists at parse time
On iOS the client is injected before your page runs. On Android it is injected when the page starts loading, which can be after your first inline script has already run. Always wait for WebToAppBridgeReady (or use the hosted helper, which does) instead of calling WebToApp.call straight away.
The WebToApp API
| Member | Description |
|---|---|
WebToApp.bridgeVersion | 2 |
WebToApp.call(method, params?, { timeoutMs? }) | Returns a promise. Default timeout is 30 000 ms (10 minutes for paywall and purchase calls). On failure it rejects with an Error that has .code (and .details), see the errors below. |
WebToApp.on(name, handler) | Subscribe to an event from the app. handler(data, { name, ts }), where ts is epoch milliseconds. Returns a function that unsubscribes. The name '*' receives every event. |
WebToApp.once(name, handler), WebToApp.off(name, handler) | Subscribe for one event only, or remove a handler. |
WebToApp.last(name) | The last data received for that event since this page loaded, or undefined. Use it to read state you subscribed too late to see. |
window.WebToApp already carries platform, isMobileApp(), getPlatform() and Compliance (see App Detection); the bridge adds the members above.
// Call with a custom timeout
const state = await WebToApp.call('app.getState', {}, { timeoutMs: 5000 });
// Subscribe, unsubscribe, one-shot, and read the last value
const off = WebToApp.on('app.background', function () { saveDraft(); });
off(); // stop listening
WebToApp.once('app.foreground', function () { refreshSession(); });
const lastNetwork = WebToApp.last('network.changed'); // { online, connection } or undefined
// Debug: log everything the app sends
WebToApp.on('*', function (data, meta) { console.log(meta.name, data, meta.ts); });How it travels
You normally never touch this: the injected client takes care of ids and timeouts. It is documented so you can debug what goes over the wire.
| Direction | Mechanism |
|---|---|
| Web → App | FlutterWebView.postMessage(JSON.stringify({ type: 'wta.call', id, method, params })) |
| App → Web (answer) | WebToAppResponse event on window, with detail = { id, ok: true, result } or { id, ok: false, error: { code, message } } |
| App → Web (event) | WebToAppEvent event on window, with detail = { name, data, ts } |
Methods
app.* methods always exist. purchases.* methods need the RevenueCat add-on enabled for that platform; otherwise they reject with not_enabled.
| Method | Params | Result |
|---|---|---|
app.getInfo | none | { bridgeVersion: 2, platform: 'ios' | 'android', capabilities: { purchases: boolean, paywalls: boolean, push: 'onesignal' | 'fcm' | null, ads: boolean } } |
app.getState | none | { lifecycle: 'foreground' | 'background', online: boolean, connection: 'wifi' | 'cellular' | 'ethernet' | 'other' | 'none' } |
purchases.getOfferings | none | Offerings |
purchases.purchase | { packageId, offeringId? } (offeringId defaults to the current offering) | { customerInfo, productIdentifier, transactionId } |
purchases.restore | none | CustomerInfo |
purchases.getCustomerInfo | none | CustomerInfo |
purchases.logIn | { userId } | { customerInfo, created } |
purchases.logOut | none | CustomerInfo (anonymous). A no-op result if already anonymous. |
purchases.getAppUserId | none | { appUserId, isAnonymous } |
purchases.setAttributes | { attributes?: { [key]: string | null }, email?, displayName? } | {} |
purchases.presentPaywall | { offeringId?, requiredEntitlement?, displayCloseButton? } | { result: 'purchased' | 'restored' | 'cancelled' | 'error' | 'notPresented', customerInfo }. With requiredEntitlement the paywall only appears if the user lacks it (notPresented otherwise). |
purchases.presentCustomerCenter | none | {}, resolved when the user closes it |
Offerings and CustomerInfo are described in the RevenueCat guide.
Events
| Event | data | When |
|---|---|---|
purchases.customerInfoUpdated | CustomerInfo | After setup, after every purchase, restore, log in and log out, and whenever RevenueCat reports a change (renewal, expiry, refund, a deferred purchase approved) |
app.foreground, app.background | {} | The app comes to the foreground or goes to the background. Refresh sessions and entitlements on app.foreground. |
network.changed | { online, connection } | Connectivity changed |
push.received | { provider: 'fcm' | 'onesignal', title, body, data } | A notification arrives while the app is in the foreground |
push.opened | { provider, title, body, data, openUrl } | The user taps a notification. openUrl is the openUrl from the notification's data, or null. |
ads.rewardEarned | { amount, type } | The user finished a rewarded ad that you started with the showRewardedAd message |
Events are fire-and-forget and are not replayed. A handler added after an event fired will not see it. WebToApp.last(name) only covers events since the current page loaded, so on a fresh page ask for the state instead: app.getState or purchases.getCustomerInfo.
Errors
Every rejection carries a code (a snake_case string) and a message.
| Code | Meaning |
|---|---|
timeout | No answer within timeoutMs (raised by the client in your page). |
unsupported_method | This build does not know the method (the app was built before it shipped). |
invalid_params | A parameter is missing or has the wrong type. |
not_enabled | The integration is off for this app, or this platform has no key. |
paywalls_disabled | paywalls is off in the RevenueCat settings. |
purchase_cancelled | The user dismissed the purchase sheet. Not a failure: do not show an error. |
payment_pending | The purchase is deferred (Ask to Buy, a slow payment method). The entitlement arrives later on purchases.customerInfoUpdated. |
product_already_purchased | The user already owns it: call purchases.restore. |
network_error | Offline, or RevenueCat could not be reached. |
store_problem | An App Store or Google Play error. |
not_allowed | Purchases are disabled on the device (parental controls and similar). |
invalid_package | packageId or offeringId was not found in the offerings. |
unknown | Anything else; read message for the details. |
try {
const info = await WebToApp.call('purchases.getCustomerInfo');
} catch (e) {
if (e.code === 'not_enabled' || e.code === 'unsupported_method') {
// RevenueCat is not set up for this build: hide the purchase UI
} else if (e.code === 'timeout') {
// the app did not answer in time
} else {
console.error(e.code, e.message);
}
}Worked example: a two-way flow
Your page asks the app for its state, then keeps itself in sync with what happens natively: refresh the session when the user comes back, react to a notification tap, reward a finished ad and show an offline banner. refreshSession, showOfflineBanner, trackEvent and grantCredits stand for your own code.
async function setupNativeBridge() {
// 0. Inside the app? (a regular browser never gets the bridge)
if (!(await whenBridgeReady(1500))) return;
// 1. Web asks the app for its state
const state = await WebToApp.call('app.getState');
// → { lifecycle: 'foreground', online: true, connection: 'wifi' }
showOfflineBanner(!state.online);
// 2. App tells the web it is back in the foreground: refresh the session
WebToApp.on('app.foreground', function () {
refreshSession();
});
// 3. Connectivity changes while the app is open
WebToApp.on('network.changed', function (data) {
showOfflineBanner(!data.online); // data = { online, connection }
});
// 4. The user tapped a notification. The app opens openUrl itself;
// use the event for analytics or to update the page you are on.
WebToApp.on('push.opened', function (data) {
// data = { provider, title, body, data, openUrl }
trackEvent('push_opened', { provider: data.provider, title: data.title });
});
// 5. A rewarded ad finished: the web starts it, the app reports the reward
WebToApp.on('ads.rewardEarned', function (reward) {
grantCredits(reward.amount); // reward = { amount, type }
});
document.getElementById('watch-ad').addEventListener('click', function () {
window.FlutterWebView.postMessage(JSON.stringify({ type: 'showRewardedAd' }));
});
}
setupNativeBridge();This example uses whenBridgeReady from above. showRewardedAd needs the AdMob add-on with a rewarded ad unit, see the AdMob guide.
Troubleshooting
WebToApp.call is not a function/WebToAppis undefined: you are in a regular browser, the script ran before the Android client was injected (wait forWebToAppBridgeReady), or the app was built before the native bridge shipped (request a new build).unsupported_method: the app does not know that method yet. Request a new build.not_enabledon apurchases.*call: RevenueCat is off, or this platform has no key. Checkcapabilities.purchasesfromapp.getInfo, then see the RevenueCat guide.- A handler never fires: events are not replayed, so subscribe as early as possible, and read
WebToApp.last(name)or ask withapp.getStatefor what you missed. Also check you are subscribing to the exact event name, or to'*'to see everything. timeout: the app did not answer withintimeoutMs. Raise it for slow calls, or check that the page is still loaded in the app.