MOBILE
Native & hybrid apps
Open the hosted page in a WebView. The widget renders as a native-feeling bottom sheet and sends the outcome back to your app as a message; your backend applies offers exactly as on the web.
1. The hosted page
https://boolbul.com/w/bb_pub_your_key?customerId=CUSTOMER_ID&subscriptionId=SUBSCRIPTION_ID&mrrCents=4900| Query param | Required | Notes |
|---|---|---|
customerId | yes | Gateway customer id |
subscriptionId | yes | Subscription being cancelled |
mrrCents | no | For saved-MRR stats and price copy |
currency | no | ISO code, default USD |
2. Messages your app receives
| type | payload | Do this |
|---|---|---|
boolbul:offer_accepted | offer.offerJwt | POST it to your apply endpoint (same server code as web). |
boolbul:cancel | details.reasonId, details.comment, details.failedOpen | Run your cancellation; forward the reason. |
boolbul:dismiss | โ | Close the WebView; the customer keeps their plan. |
Delivered through whichever bridge exists: React Native ReactNativeWebView, iOS webkit.messageHandlers.boolbul, Android / Flutter channel BoolBulNative, flutter_inappwebview handler boolbul, or window.parent.postMessage for iframes.
React Native
import { WebView } from "react-native-webview";
<WebView
source={{ uri: `https://boolbul.com/w/bb_pub_your_key?customerId=${user.customerId}&subscriptionId=${user.subscriptionId}&mrrCents=${user.priceCents}` }}
style={{ backgroundColor: "transparent" }}
onMessage={async (event) => {
const message = JSON.parse(event.nativeEvent.data);
if (message.type === "boolbul:offer_accepted") {
await api.post("/boolbul/apply", { offerJwt: message.offer.offerJwt });
} else if (message.type === "boolbul:cancel") {
await api.post("/boolbul/cancel", message.details);
closeSheet();
} else if (message.type === "boolbul:dismiss") {
closeSheet();
}
}}
/>Flutter (webview_flutter)
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('BoolBulNative', onMessageReceived: (msg) async {
final data = jsonDecode(msg.message);
switch (data['type']) {
case 'boolbul:offer_accepted':
await api.post('/boolbul/apply', {'offerJwt': data['offer']['offerJwt']});
case 'boolbul:cancel':
await api.post('/boolbul/cancel', data['details']);
Navigator.pop(context);
case 'boolbul:dismiss':
Navigator.pop(context);
}
})
..loadRequest(Uri.parse(
'https://boolbul.com/w/bb_pub_your_key?customerId=$customerId&subscriptionId=$subscriptionId'));iOS (WKWebView)
let config = WKWebViewConfiguration()
config.userContentController.add(handler, name: "boolbul") // receives boolbul:* messages
let webView = WKWebView(frame: .zero, configuration: config)
webView.isOpaque = false
webView.load(URLRequest(url: URL(string:
"https://boolbul.com/w/bb_pub_your_key?customerId=\(customerId)&subscriptionId=\(subscriptionId)")!))Android (WebView)
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(object {
@JavascriptInterface fun postMessage(json: String) = handleBoolBul(json)
}, "BoolBulNative")
webView.loadUrl("https://boolbul.com/w/bb_pub_your_key?customerId=CUSTOMER_ID&subscriptionId=SUBSCRIPTION_ID&mrrCents=4900")App store rules: if your subscription is billed through Apple or Google in-app purchases, those stores own cancellation. BoolBul works with subscriptions billed by Dodo Payments or Stripe (web checkout), including ones used inside mobile apps.