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 paramRequiredNotes
customerIdyesGateway customer id
subscriptionIdyesSubscription being cancelled
mrrCentsnoFor saved-MRR stats and price copy
currencynoISO code, default USD

2. Messages your app receives

typepayloadDo this
boolbul:offer_acceptedoffer.offerJwtPOST it to your apply endpoint (same server code as web).
boolbul:canceldetails.reasonId, details.comment, details.failedOpenRun 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.