Prebuilt UI in a mobile webview

This guide describes how to embed the Prebuilt UI in a native iOS or Android app using a webview and mobile-bootstrap.js.

Use it if you are building your own webview integration. Insurely also provides mobile SDKs for Android and iOS if you would rather not set the webview up yourself.

Before you start

You need the Customer ID and Config name from Insurely (see Introduction).

How it works

The webview loads the Prebuilt UI directly as the top-level page, using mobile-bootstrap.js.

Your app:

  1. Loads {baseUrl} in a webview.
  2. Injects a script that sets your configuration on window.insurely, loads {baseUrl}/assets/mobile-bootstrap.js, and forwards the interface's postMessage events to native code.
  3. Reacts to those events. If you need Swedish BankID, that includes OPEN_SWEDISH_BANKID, which your app must handle by opening the BankID app.

{baseUrl} is the environment you were given:

EnvironmentBase URL
Productionhttps://blocks.insurely.com
Testhttps://blocks.test.insurely.com

The examples below use the production URL.

Setting up the integration

The snippets below are minimal examples.

Set up the webview

Load the Prebuilt UI as the webview's top-level page.

let contentController = WKUserContentController()

contentController.addUserScript(WKUserScript(
    source: bootstrapScript,
    injectionTime: .atDocumentEnd,
    forMainFrameOnly: false
))
contentController.addUserScript(WKUserScript(
    source: postMessageMapperScript,
    injectionTime: .atDocumentEnd,
    forMainFrameOnly: false
))
contentController.add(insurelyMessageHandler, name: "iOSNative")

let configuration = WKWebViewConfiguration()
configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
configuration.userContentController = contentController

let webView = WKWebView(frame: .zero, configuration: configuration)
webView.load(URLRequest(url: URL(string: "https://blocks.insurely.com")!))
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.loadWithOverviewMode = true
webView.settings.useWideViewPort = true

webView.addJavascriptInterface(InsurelyMessageHandler(), "Android")
webView.webViewClient = InsurelyWebViewClient()
webView.loadUrl("https://blocks.insurely.com")

Keep same-origin navigation inside the webview and hand everything else (external links and bankid:// URLs) to the OS:

inner class InsurelyWebViewClient : WebViewClient() {

    override fun onPageCommitVisible(view: WebView?, url: String?) {
        view?.loadUrl(BOOTSTRAP_JS)
        super.onPageCommitVisible(view, url)
    }

    override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        val requestedUrl = request?.url ?: return false
        val currentUrl = view?.url?.let { Uri.parse(it) } ?: return false

        val sameOrigin = requestedUrl.scheme == currentUrl.scheme &&
                requestedUrl.host == currentUrl.host &&
                requestedUrl.port == currentUrl.port

        if (sameOrigin) {
            view.loadUrl(requestedUrl.toString())
            return true
        }

        context.startActivity(Intent(Intent.ACTION_VIEW, requestedUrl))
        return true
    }
}

Inject your configuration and mobile-bootstrap.js

let bootstrapScript = """
    (function() {
        const bootstrapScript = document.createElement('script');
        bootstrapScript.src = "https://blocks.insurely.com/assets/mobile-bootstrap.js";
        document.head.appendChild(bootstrapScript);

        window.insurely = {
            config: {
                customerId: 'YOUR_CUSTOMER_ID',
                configName: 'YOUR_CONFIG_NAME',
            },
        };
    })();
    """

A second user script forwards every postMessage the interface sends to your native message handler:

let postMessageMapperScript = """
    (function() {
        window.originalPostMessage = window.postMessage;
        window.postMessage = function(data, ...rest) {
            window.originalPostMessage(data, ...rest);
            window.webkit.messageHandlers.iOSNative.postMessage(data);
        };
    })();
    """

BOOTSTRAP_JS (loaded in onPageCommitVisible above) sets your configuration, loads mobile-bootstrap.js, and forwards every postMessage the interface sends to the Android JavaScript interface:

private val BOOTSTRAP_JS = """
javascript:(function() {
    const bootstrapScript = document.createElement('script');
    bootstrapScript.src = 'https://blocks.insurely.com/assets/mobile-bootstrap.js';
    document.head.appendChild(bootstrapScript);

    window.insurely = {
        config: {
            customerId: 'YOUR_CUSTOMER_ID',
            configName: 'YOUR_CONFIG_NAME',
        },
    };

    if (window.Android) {
        const originalPostMessage = window.postMessage;
        window.postMessage = (data, ...rest) => {
            originalPostMessage(data, ...rest);
            Android.postMessage(JSON.stringify(data));
        };
    }
})()
""".trimIndent()

All configuration and prefill options work exactly as in the web integration. Add prefill next to config if you want to skip the company selection step or pre-fill the user's details. See the Configuration reference for the full object.

Receive events natively

Messages have the shape { name: '<EVENT>', value: <payload> }. Handle the ones your integration needs and ignore the rest.

class InsurelyMessageHandler: NSObject, WKScriptMessageHandler {

    func userContentController(
        _ userContentController: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard
            let body = message.body as? [String: Any],
            let name = body["name"] as? String
        else { return }

        switch name {
        case "OPEN_SWEDISH_BANKID":
            guard
                let value = body["value"] as? [String: Any],
                let url = value["url"] as? String
            else { return }
            openBankID(url: url)
        case "APP_CLOSE":
            // Dismiss the webview
            break
        default:
            // React to other events (COLLECTION_ID, COLLECTION_STATUS, RESULTS, ...) as needed
            break
        }
    }
}
inner class InsurelyMessageHandler {
    @JavascriptInterface
    fun postMessage(json: String?) {
        val message = JSONObject(json ?: return)
        when (message.optString("name")) {
            "OPEN_SWEDISH_BANKID" -> {
                val autostartToken = message.optJSONObject("value")?.optString("autostartToken")
                if (autostartToken != null) openBankId(autostartToken)
            }
            "APP_CLOSE" -> { /* dismiss the webview */ }
            // React to other events (COLLECTION_ID, COLLECTION_STATUS, RESULTS, ...) as needed
        }
    }
}

Threading

Methods annotated with @JavascriptInterface are called on a background thread. Switch to the main thread before touching the WebView or any UI.

Open the BankID app (Sweden)

This step applies if you need Swedish BankID. When a user authenticates on the same device, the interface sends OPEN_SWEDISH_BANKID and your app must handle it, since the flow cannot continue without it. Skip this step if you do not.

The value contains a prebuilt url on the form bankid:///?autostarttoken=<token>&redirect=null, and the autostartToken separately if you prefer to build the URL yourself.

On iOS you must replace redirect=null with a URL scheme that reopens your app. This is what brings the user back automatically once they finish authenticating in the BankID app.

func openBankID(url urlString: String) {
    guard var components = URLComponents(string: urlString) else { return }

    // Replace the redirect parameter with your own app scheme.
    // BankID requires the redirect value to be strictly percent-encoded,
    // which is stricter than URLComponents' default encoding.
    components.queryItems = (components.queryItems ?? []).filter { $0.name != "redirect" }
    let redirect = "yourapp://".addingPercentEncoding(
        withAllowedCharacters: .alphanumerics.union(CharacterSet(charactersIn: "."))
    )!

    guard
        let base = components.url?.absoluteString,
        let url = URL(string: "\(base)&redirect=\(redirect)")
    else { return }

    guard UIApplication.shared.canOpenURL(url) else {
        // BankID is not installed. Show an error, or let the user
        // authenticate with QR on another device
        return
    }
    UIApplication.shared.open(url)
}

Two things are required in your app for this to work:

  • Register your URL scheme (yourapp://) under CFBundleURLTypes in Info.plist, so BankID can reopen your app.
  • Add bankid to LSApplicationQueriesSchemes in Info.plist if you use canOpenURL as above.

On Android, redirect=null is the correct value. When the user completes authentication, the BankID app closes and Android returns the user to your app on its own.

fun openBankId(autostartToken: String) {
    val uri = Uri.parse("bankid:///")
        .buildUpon()
        .appendQueryParameter("autostarttoken", autostartToken)
        .appendQueryParameter("redirect", "null")
        .build()
    try {
        context.startActivity(Intent(Intent.ACTION_VIEW, uri))
    } catch (e: ActivityNotFoundException) {
        // BankID is not installed. Show an error, or let the user
        // authenticate with QR on another device
    }
}

When BankID returns the user to your app, the webview resumes and the interface continues the flow. Nothing further is needed from your app.

Client-side authentication

The SWEDISH_MOBILE_BANKID_SAME_DEVICE_CLIENT_SIDE_AUTHENTICATION login method is different from the other BankID login methods in that it requires the BankID flow to be initiated from the same device as the BankID authentication will be performed on. In order to make this as streamlined as possible we will in this case supply a number of requests to be executed from the end-user's device. These requests reach your app on the COLLECTION_STATUS postMessage, as Request objects in the INSTRUCTIONS field of its extraInformation. The responses are to be returned as a ResponseObject in a SUPPLEMENTAL_INFORMATION postMessage back to the page.

This is visualised in the chart below. The two highlighted bands are the relay and the BankID switch, the phases your app has to take part in.

Every set of instructions carries an etag. A new etag means a new request to execute, so your app runs it and posts the response back. An etag it has already executed is a repeat of the same instructions and must be skipped, otherwise the request is sent to the company twice.

Returning the response

Your app injects the call into the page itself:

window.postMessage(
  {
    name: 'SUPPLEMENTAL_INFORMATION',
    value: responseObject,
  },
  '*',
);

value is a ResponseObject holding the headers and body of the request your app just executed, with type set to RESPONSE_OBJECT. See the schema for the full field reference and an example.

Read the full guide before you build this

This page only covers the webview side of the flow. The complete reference for this login method (the request and response objects, and what to do if your app cannot support arbitrary request URLs) is in the Swedish Wealth API introduction, or the Swedish Insurance API introduction for insurance.

Event reference

These are the events that matter most in a webview integration:

EventWhenAction
OPEN_SWEDISH_BANKIDSame-device Swedish BankID authenticationRequired in Sweden: open the BankID app
APP_CLOSEThe user presses the close buttonDismiss the webview
PAGE_VIEW, SELECTED_COMPANY, COLLECTION_INITIATED, COLLECTION_ID, COLLECTION_STATUS, RESULTSFlow progressInformational. Use what your integration needs

Scrolling-related events (SCROLL_TO_TOP, SCROLL_TO_POSITION, …) are handled by mobile-bootstrap.js itself, so you can ignore them.

For the full list of events and their payloads, see the Configuration reference. For the order they arrive in, see Standard flow.

Next steps

Last updated on