Checkout Native Bridge Communication for apps

A native bridge, sometimes known as a JavaScript bridge, is a mechanism that facilitates communication between a WebView and native app code.

Mobile applications can integrate Billwerk+ Checkout when using WebViews. In order to receive events from Billwerk+ Checkout through WebView, you must register native bridge communication in your app's WebView.

The implemented WebView in your mobile app must load the checkout session URL created from charge session or other session types. Billwerk+ Checkout will fire the first event Init, which your WebView will receive after having the correct JavaScript interface/channel registered. See examples below.


Native Bridge Channels

List of exposed JavaScript interfaces for creating bridge between Billwerk+ Checkout and mobile app WebViews:

Channel nameWebViewPlatformFunctions
ReepayCheckoutWKWebViewNative iOSresolveMessage(reply) => void
AndroidWebViewListenerWebViewNative AndroidN/A
ReactNativeWebViewreact-native-webviewReact Native (iOS & Android)resolveMessage(reply) => void
CheckoutChannelwebview_flutter_wkwebview
webview_flutter_android
Flutter (iOS & Android)resolveMessage(reply) => void

Events

Similar to Embedded Checkout and Modal Checkout, your WebView will receive a number of events which your mobile app WebView must subscribe to.


WebView Event Types

Checkout events

EventDescription
InitFires once Checkout is loaded in WebView.
OpenFires once Checkout content has appeared.
CloseOptional Does not fire from Billwerk+ Checkout. WebView should implement this and fire event when user closes WebView.
AcceptFires once the transaction is successfully completed.
CancelFires once user clicks Cancel inside Checkout page, or Checkout only has a single payment method and cancels externally e.g. cancel from MobilePay app.
ErrorFires whenever an error has occurred in attempting to finalize the transaction.

User events

EventDescription
card_input_changeFires once any of the card input fields have been modified

WebView Event Response Signature

Each event responds with a signature of type:

{
  event: string;
  sessionState?: string;
  data?: any;
}

State of the checkout session can be of following types:

"PAYMENT_METHOD_ALREADY_ADDED",
"INVOICE_ALREADY_PAID",
"INVOICE_PROCESSING,
"SUCCESS",
"SESSION_EXPIRED",

Data object uses the same signature as Embedded and Modal Checkout signatures. Only events Open, Accept, Cancel and Error will contain data objects.

{
  id: string;			// The current session id
  invoice: string;		// Invoice/charge handle
  customer: string;		// Customer handle
  subscription: string;		// Subscription handle
  payment_method: string;	// Payment method if a new one is created
  error: string;		// The error code
}

WebView Event Reply

Init event is the first event fired from Billwerk+ Checkout:

{
  event: "Init"
}

Once received by WebView, the app must reply the event message with { isWebView: true }. It will set Billwerk+ Checkout to WebView mode, and thus continue to fire the other WebView events.

{
  isWebView: true
}

WebView Event Reply Signature

{
    isWebView: boolean; // Must be set to 'true' to receive WebView events
    isWebViewChanged?: boolean; // Optional - notify Checkout that WebView has been modified by user
    userAgent?: string; // Optional - define a custom user agent
}

Examples

Examples of how to add JavaScript channel interfaces for your WebView to receive and reply messages from/to Billwerk+ Checkout.

Hybrid apps

Hybrid apps usually imports libraries for WebView implementation. Below are some examples from our demo apps of how to subscribe the events sent from Billwerk+ Checkout.

React Native

React Native WebView has a JavaScript channel to perform communication between the app and webpage. It is pre-defined as window.ReactNativeWebView. See detailed example in our React Native demo app.

render(): ReactNode {
  return (
      <WebView
      	...
      	javaScriptEnabled={true}
        onMessage={this._handleWebViewMessageEvent}
        ...
      />
 );
}

private _handleWebViewMessageEvent = (event: WebViewMessageEvent) => {
    const rawData = event.nativeEvent.data;
    try {
      const message = JSON.parse(rawData);
      const event = message.event;
      
       switch (event) {
        case "Init":
          const customUserAgent = this._getCustomUserAgent();
          const reply = JSON.stringify({
              isWebView: true,
              userAgent: customUserAgent,
          });
           const injectedJavaScript = `
           	if (window.ReactNativeWebView.resolveMessage) {
            	window.ReactNativeWebView.resolveMessage(${reply});
              }
              `;
           this.webview.injectJavaScript(injectedJavaScript);
           break;
         default
           // handle other cases
           break;
       }
      
    } catch (error) {
      // handle error
    }
}

Flutter

Flutter WebView uses the pre-defined channel from Billwerk+ Checkout CheckoutChannel. See detailed example in our Flutter Demo app.

final WebViewController controller = WebViewController()
  ...
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..addJavaScriptChannel('CheckoutChannel', onMessageReceived: (JavaScriptMessage message) {
    _onMessageReceived(message);
  });

void _onMessageReceived(JavaScriptMessage message) {
  // handle events
}
// Reply event
final reply = {'isWebView': true};
final jsCode = '''
if (window.CheckoutChannel && typeof window.CheckoutChannel.resolveMessage === 'function') {
    window.CheckoutChannel.resolveMessage(${jsonEncode(reply)});
}
''';
_controller.runJavaScript(jsCode);

Native apps

Native apps uses Android WebView and iOS WKWebView.

Android WebView

Android WebView uses the pre-defined channel from Billwerk+ Checkout AndroidWebViewListener. See detailed example in our Android demo app.

class MyWebView(private val context: Context) {

    fun showWebViewBottomSheet(sessionUrl: String) {
        val bottomSheetDialog = BottomSheetDialog(context)
        val bottomSheetView = View.inflate(context, R.layout.bottom_sheet_dialog, null)

        val webView = bottomSheetView.findViewById<WebView>(R.id.webview)
        webView.settings.javaScriptEnabled = true
      	webView.addJavascriptInterface(MyWebViewListener(context, bottomSheetDialog), "AndroidWebViewListener")

        webView.loadUrl(sessionUrl)
        
        ...
    }
}
class MyWebViewListener(private val context: Context, private val dialog: BottomSheetDialog) {

    @JavascriptInterface
    fun postMessage(jsonMessage: String) {
        val mapType = object : TypeToken<Map<String, Any>>() {}.type
        val messageMap: Map<String, Any> = Gson().fromJson(jsonMessage, mapType)

        for ((key, value) in messageMap) {
            if(key == "event"){
                handleEvents(value.toString())
            }
        }
    }
    
     private fun handleEvents(event: String) {
        when (event) {
            "Init" -> Log.d("AndroidWebViewListener", "Checkout initiated")
            "Open" ->  Log.d("AndroidWebViewListener", "Checkout opened")
            "Close" ->  Log.d("AndroidWebViewListener", "Checkout closed")
            "Accept" -> Log.d("AndroidWebViewListener", "Checkout payment succeeded")
            "Cancel" -> Log.d("AndroidWebViewListener", "Checkout cancelled")
            "Error" ->  Log.d("AndroidWebViewListener", "Error occurred")
            else -> Log.d("AndroidWebViewListener", "Unhandled event: $event")
        }
    }
}

iOS WKWebView

iOS WKWebView uses pre-defined channel from Billwerk+ Checkout ReepayCheckout. See detailed example in our iOS demo app.

struct MyWebView: UIViewRepresentable {
  
  ...
 
  func makeUIView(context: Context) -> WKWebView {
    let prefs = WKWebpagePreferences()
    prefs.allowsContentJavaScript = true

    let configuration = WKWebViewConfiguration()
    configuration.defaultWebpagePreferences = prefs

    let webView = WKWebView(frame: .zero, configuration: configuration)
    context.coordinator.webView = webView

    let contentController = webView.configuration.userContentController

    if #available(iOS 17.0, *) {
      contentController.addScriptMessageHandler(context.coordinator, contentWorld: .page, name: "ReepayCheckout")
    } else {
      contentController.add(context.coordinator, name: "ReepayCheckout")
    }

    webView.navigationDelegate = context.coordinator
    return webView
  }
  
  ...
  
}

Reply the event message in versions below iOS 17:

func replyWithResolveMessage(message: String) {
    let responseScript = "window.webkit.messageHandlers.ReepayCheckout.resolveMessage(\(message))"
    webView?.evaluateJavaScript(responseScript, completionHandler: { _, error in
      if let error = error {
        fatalError("Error injecting JavaScript: \(error.localizedDescription)")
      }
    })
}

Reply the event message in version iOS 17 and above:

func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) {
    if let response = message.body as? [String: Any] {
        let eventName = response["event"] as? String

        if (eventName == "Init") {
            let reply = [
              "isWebView": true,
            ]
            replyHandler(reply, nil)
        } else {
          // Handle other events
        }

    } else {
      fatalError("Error: Unsupported message type")
    }   
}

Further development

Enhance users payment experience by adding a seamless app switch flow between your app and external payment apps. Read more regarding redirect to external payment apps such as Vipps MobilePay and return to your app here.