Key event handling

The events detailed below will need to be handled appropriately for the widget to fully integrate with your app.


Missing access token event

If a widget is loaded without a token being passed in, and guest user functionality isn't configured, a missing access token event will be sent back from the widget. This can then be used to trigger a token fetch in your app.

Event format:

{
  type:"MH_WIDGET_EVENT",
  name: "MISSING_ACCESS_TOKEN"
}

Suggested handling

import android.content.Intent
import android.net.Uri
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import org.json.JSONObject

val webView: WebView = findViewById(R.id.webView)
webView.settings.javaScriptEnabled = true

val widgetUrl = "widget-url"
var token = null
val numCreditCards = 2
val hash = "your-hash" // this is received from the universal link
val widgetId = "your-widget-id"

// Set up a JavaScript interface to handle messages from the WebView
webView.addJavascriptInterface(object {
    @JavascriptInterface
    fun postMessage(message: String) {
        handleMessageFromWebView(message)
    }
}, "Android")

webView.webViewClient = object : WebViewClient() {
    override fun onPageFinished(view: WebView, url: String) {
        // Inject JavaScript to listen for messages
        val jsCode = """
            window.addEventListener('message', function(event) {
                if (event.data) {
                    Android.postMessage(JSON.stringify(event.data));
                }
            });
        """
        webView.evaluateJavascript(jsCode, null)
    }
}

webView.loadUrl(widgetUrl)

fun handleMessageFromWebView(message: String) {
    Log.d("DashboardFragment", "Received message from WebView: $message")
    try {
        val jsonObject = JSONObject(message)
        when (val type = jsonObject.getString("name")) {
            "MISSING_ACCESS_TOKEN" -> {
                fetchToken("your-fetch-token-url")
            }
            else -> {
                Log.d("DashboardFragment", "Received message $type")
            }
        }
    } catch (e: Exception) {
        Log.e("DashboardFragment", "Error parsing JSON message", e)
    }
}

private fun fetchToken(url: String) {
    try {
				// fetch your token here
        token = "new-fetched-token"

        // Boot the widget with the new token
        val jsCode = """
            if (window.__boot && typeof window.__boot === 'function') {
                window.__boot('$token', { numCreditCards: $numCreditCards, hash: '$hash', widget_id: '$widgetId' });
            }
        """
        webView.evaluateJavascript(jsCode, null)
    } catch (e: Exception) {
        Log.e("DashboardFragment", "Error fetching token", e)
    }
}
<!DOCTYPE html>
<html>

  <body>
    <!-- Container for the widget -->
    <div id="widget-container">
      <iframe
        id="widget-frame"
        style="width: 100%; height: 100%; border: none"
      ></iframe>
    </div>

    <script type="text/javascript">
      let token = null

      function bootWidget() {
        const frame = document.getElementById("widget-frame")
        frame.src = window.location.origin

        // Wait for iframe to load then send boot message
        frame.onload = function () {
          const config = {
            action: "boot",
            token: token,
            params: {
              widget_id: widgetId,
              hash,
              ...options,
            },
          }

          // Add a small delay to ensure widget is ready
          setTimeout(() => {
            frame.contentWindow.postMessage(config, "*")
          }, 100)
        }
      }

      const getToken = async () => {
				// api call to get your token here
        return "your-token"
      }

      // Listen for messages from the widget
      window.addEventListener("message", async function (event) {
        if (
          event.data?.source !== "react-devtools-content-script" &&
          event.data?.type === "MH_WIDGET_EVENT" &&
          event.data?.name !== "click"
        ) {
          console.log("Widget Event:", event.data)
        }

        if (
          event.data?.name === "MISSING_ACCESS_TOKEN"
        ) {
          const fetchedToken = await getToken()
          if (fetchedToken) {
            token = fetchedToken
            bootWidget()
          }
        }
      })

      document.getElementById("updateButton").onclick = bootWidget
    </script>
  </body>
</html>

import UIKit
import WebKit

// Define a struct for the payload
struct Payload: Codable {
    let name: String
    let url: String?
}

class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
    
    var webView: WKWebView!
    var token = "your-token" // Initial token, can be updated after fetching
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set up the web view configuration
        let preferences = WKPreferences()
        let configuration = WKWebViewConfiguration()
        configuration.preferences = preferences
        configuration.userContentController.add(self, name: "ios")
        
        // Initialize the web view
        webView = WKWebView(frame: self.view.frame, configuration: configuration)
        webView.navigationDelegate = self
        self.view.addSubview(webView)
        
        // Define the widget URL and parameters
        let widgetUrl = "https://widget-url"
        let numCreditCards = 2 // Example of additional config
        let hash = "your-hash" // Received from the universal link
        let widgetId = "your-widget-id"
        
        // Load the URL
        if let url = URL(string: widgetUrl) {
            webView.load(URLRequest(url: url))
        }
    }
    
    // WKNavigationDelegate method
    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        let jsCode = """
        if (window.__boot && typeof window.__boot === 'function') {
            window.__boot('\(token)', { numCreditCards: \(numCreditCards), hash: '\(hash)', widget_id: '\(widgetId)' });
        }
        window.addEventListener('message', function(event) {
            if (event.data) {
                window.webkit.messageHandlers.ios.postMessage(JSON.stringify(event.data));
            }
        });
        """
        webView.evaluateJavaScript(jsCode, completionHandler: nil)
    }
    
    // WKScriptMessageHandler method
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        if message.name == "ios", let jsonString = message.body as? String {
            print("Received message from JS: \(jsonString)")
            guard let jsonData = jsonString.data(using: .utf8) else {
                fatalError("Failed to convert string to data")
            }
            do {
                let decoder = JSONDecoder()
                let payload = try decoder.decode(Payload.self, from: jsonData)
                
                if payload.name == "MISSING_ACCESS_TOKEN" {
                    fetchToken()
                }
            } catch {
                print("Error decoding JSON:", error)
            }
        }
    }
    
    private func fetchToken() {
        // Fetch token here
        token = "your-token"
        
        // Boot the widget with the new token
        let jsCode = """
        if (window.__boot && typeof window.__boot === 'function') {
            window.__boot('\(token)', { numCreditCards: \(numCreditCards), hash: '\(hash)', widget_id: '\(widgetId)' });
        }
        """
        webView.evaluateJavaScript(jsCode, completionHandler: nil)
    }
}
import React, { useRef, useState } from 'react';
import { View, Alert } from 'react-native';
import { WebView } from 'react-native-webview';

const App = () => {
  const webViewRef = useRef(null);
  const [token, setToken] = useState('your-token'); // Initial token

  const widgetUrl = 'https://widget-url';
  const numCreditCards = 2;
  const hash = 'your-hash';
  const widgetId = 'your-widget-id';

  const handleWebViewMessage = (event) => {
    try {
      const message = JSON.parse(event.nativeEvent.data);
      if (message.name === 'MISSING_ACCESS_TOKEN') {
        fetchToken();
      } 
    } catch (error) {
      console.error('Error parsing message from WebView:', error);
    }
  };

  const fetchToken = () => {
    // Simulate fetching a new token
    const newToken = 'new-fetched-token';
    setToken(newToken);

    // Boot the widget with the new token
    const jsCode = `
      if (window.__boot && typeof window.__boot === 'function') {
        window.__boot('${newToken}', { numCreditCards: ${numCreditCards}, hash: '${hash}', widget_id: '${widgetId}' });
      }
    `;
    webViewRef.current.injectJavaScript(jsCode);
  };

  const injectedJavaScript = `
    (function() {
      if (window.__boot && typeof window.__boot === 'function') {
        window.__boot('${token}', { numCreditCards: ${numCreditCards}, hash: '${hash}', widget_id: '${widgetId}' });
      }
      window.addEventListener('message', function(event) {
        if (event.data) {
          window.ReactNativeWebView.postMessage(JSON.stringify(event.data));
        }
      });
    })();
  `;

  return (
    <View style={{ flex: 1 }}>
      <WebView
        ref={webViewRef}
        source={{ uri: widgetUrl }}
        onMessage={handleWebViewMessage}
        injectedJavaScript={injectedJavaScript}
        javaScriptEnabled={true}
      />
    </View>
  );
};

export default App;
// main.dart

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';

void main() {
  runApp(
    MaterialApp(theme: ThemeData(useMaterial3: true), home: const WebViewApp()),
  );
}

class WebViewApp extends StatefulWidget {
  const WebViewApp({super.key});

  @override
  State<WebViewApp> createState() => _WebViewAppState();
}

class _WebViewAppState extends State<WebViewApp> {
  late final WebViewController controller;
  bool _hasBooted = false;

  Future<String?> fetchToken() async {
    try {
      final response = await http.get(
        Uri.parse('your_token_url'),
      );
      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        return data['access_token'];
      } else {
        debugPrint('Failed to fetch token: ${response.statusCode}');
      }
    } catch (e) {
      debugPrint('Error fetching token: $e');
    }
    return null;
  }

  @override
  void initState() {
    super.initState();
    controller =
        WebViewController()
          ..setJavaScriptMode(JavaScriptMode.unrestricted)
          ..loadRequest(
            Uri.parse('your_widget_url'),
          )
          ..addJavaScriptChannel(
            'Android',
            onMessageReceived: handleMessageFromWebView,
          )
          ..addJavaScriptChannel(
            'webkit.messageHandlers.iosListener',
            onMessageReceived: handleMessageFromWebView,
          )
          ..setNavigationDelegate(
            NavigationDelegate(
              onPageFinished: (String url) async {
                if (!_hasBooted) {
                  final token = await fetchToken();
                  if (token != null) {
                    await controller.runJavaScript(
                      'window.__boot("$token", {widget_id: "your_widget_id"})',
                    );
                    _hasBooted = true;
                  }
                }
              },
            ),
          );
  }

  void handleMessageFromWebView(JavaScriptMessage message) async {
    debugPrint('\x1B[33m[WebView] ${message.message}\x1B[0m'); // Yellow color
    try {
      final jsonObject = jsonDecode(message.message);
      final type = jsonObject['type'] as String;

      switch (type) {
        case 'MISSING_ACCESS_TOKEN':
          final newToken = await fetchToken();
          if (newToken != null) {
            await controller.runJavaScript(
              'window.__boot("$newToken", {widget_id: "your_widget_id"})',
            );
          }
          break;
        default:
          debugPrint('\x1B[36m[WebView Event] $type\x1B[0m'); // Cyan color
      }
    } catch (e) {
      debugPrint('\x1B[31m[WebView Error] $e\x1B[0m'); // Red color
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter WebView')),
      body: WebViewWidget(controller: controller),
    );
  }
}

External web link event

When the widget journey requires a link out to an external site, we emit an event with type = "externalLink". Handling this appropriately will be required for any open banking flows, and also will be needed to link out to other sites or areas of your own app.

Event format:

{
  url:  "https://www.google.com", 
  type: "externalLink"
}

Suggested handling

You will need to use native platform apis to open the url passed in via the event.

import android.content.Intent
import android.net.Uri
import android.util.Log
import org.json.JSONObject


fun handleMessageFromWebView(message: String) {
  Log.d("DashboardFragment", "Received message from WebView: $message")
  try {
    val jsonObject = JSONObject(message)
    when (val type = jsonObject.getString("type")) {
      "externalLink" -> {
        val url = jsonObject.getString("url")
        openExternalLink(url)
      }
      else -> {
        Log.d("DashboardFragment", "Received message $type")
      }
    }
  } catch (e: Exception) {
    Log.e("DashboardFragment", "Error parsing JSON message", e)
  }
}

private fun openExternalLink(url: String) {
  try {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    startActivity(intent)
  } catch (e: Exception) {
    Log.e("DashboardFragment", "Error opening external link", e)
  }
}

const handleExternalLink = async (event) => {
  // Handle external link event
  if (event.data?.type === 'externalLink' && event.data?.url) {
    window.open(event.data.url, '_blank');
  }
};
import UIKit
import WebKit

struct Payload: Codable {
    let type: String
    let url: String
}

// WKScriptMessageHandler method
  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "ios", let jsonString = message.body as? String {
    print("Received message from JS: \(jsonString)")
    guard let jsonData = jsonString.data(using: .utf8) else {
      fatalError("Failed to convert string to data")
    }
    do {
      let decoder = JSONDecoder()
      let payload = try decoder.decode(Payload.self, from: jsonData)

      if payload.type == "externalLink", let url = URL(string: payload.url) {
        print("OPEN URL: \(payload.url)")
        if UIApplication.shared.canOpenURL(url) {
          UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
          print("Cannot open URL")
        }
      }
    } catch {
      print("Error decoding JSON:", error)
    }
	}
}

import React, { useRef } from 'react';
import { Linking, Alert } from 'react-native';

const handleMessage = (event) => {
  try {
    const message = JSON.parse(event.nativeEvent.data);
    console.log('Received message from WebView:', message);

    if (message.type === 'externalLink') {
      const url = message.url;
      openExternalLink(url);
    } else {
      console.log('Received message type:', message.type);
    }
  } catch (error) {
    console.error('Error parsing JSON message:', error);
  }
};

const openExternalLink = (url) => {
      Linking.canOpenURL(url)
      .then((supported) => {
        if (supported) {
          Linking.openURL(url);
        } else {
          console.error('Cannot open URL:', url);
          Alert.alert('Error', 'Cannot open the link.');
        }
      })
      .catch((err) => console.error('An error occurred', err));
};

 
// main.dart

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';

void handleMessageFromWebView(JavaScriptMessage message) {
  debugPrint('\x1B[33m[WebView] ${message.message}\x1B[0m'); // Yellow color
  try {
    final jsonObject = jsonDecode(message.message);
    final type = jsonObject['type'] as String;

    switch (type) {
      case 'externalLink':
      final url = jsonObject['url'] as String;
      openExternalLink(url);
      default:
      debugPrint('\x1B[36m[WebView Event] $type\x1B[0m'); // Cyan color
    }
  } catch (e) {
    debugPrint('\x1B[31m[WebView Error] $e\x1B[0m'); // Red color
  }
}

void openExternalLink(String url) async {
  final uri = Uri.parse(url);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri);
  } else {
    debugPrint('Could not launch $url');
  }
}



Close event

This is fired when a CTA to leave the widget is clicked. This will automatically be sent when the user clicks the "Exit" button (shown on the initial/home page of the widget) but can be sent for other CTAs as well.

Event format:

{
  metadata: {route: 'indexRoute', baseURI: {YOUR_DOMAIN}, hash: ''},  
  name:"close",  
  type:"MH_WIDGET_EVENT"
}

Suggested handling

You will need to use native platform apis to link back to the desired area of your app.

import android.content.Intent
import android.net.Uri
import android.util.Log
import org.json.JSONObject


fun handleMessageFromWebView(message: String) {
  Log.d("DashboardFragment", "Received message from WebView: $message")
  try {
    val jsonObject = JSONObject(message)
    when (val type = jsonObject.getString("name")) {
      "close" -> {
        openExternalLink(YOUR_APP_URL)
      }
      else -> {
        Log.d("DashboardFragment", "Received message $type")
      }
    }
  } catch (e: Exception) {
    Log.e("DashboardFragment", "Error parsing JSON message", e)
  }
}

private fun openExternalLink(url: String) {
  try {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
    startActivity(intent)
  } catch (e: Exception) {
    Log.e("DashboardFragment", "Error opening external link", e)
  }
}

const handleCloseEvent = async (event) => {
  if (event.data?.name === 'close') {
    window.open({YOUR_APP_URL}, '_blank');
  }
};
import UIKit
import WebKit

struct Payload: Codable {
    let type: String
    let url: String
}

// WKScriptMessageHandler method
  func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
    if message.name == "ios", let jsonString = message.body as? String {
    print("Received message from JS: \(jsonString)")
    guard let jsonData = jsonString.data(using: .utf8) else {
      fatalError("Failed to convert string to data")
    }
    do {
      let decoder = JSONDecoder()
      let payload = try decoder.decode(Payload.self, from: jsonData)

      if payload.name == "close", let url = URL(YOUR_APP_URL) {
        print("OPEN URL: \(payload.url)")
        if UIApplication.shared.canOpenURL(url) {
          UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
          print("Cannot open URL")
        }
      }
    } catch {
      print("Error decoding JSON:", error)
    }
	}
}

import React, { useRef } from 'react';
import { Linking, Alert } from 'react-native';

const handleMessage = (event) => {
  try {
    const message = JSON.parse(event.nativeEvent.data);
    console.log('Received message from WebView:', message);

    if (message.name === 'close') {
      openExternalLink(YOUR_APP_URL);
    } else {
      console.log('Received message type:', message.type);
    }
  } catch (error) {
    console.error('Error parsing JSON message:', error);
  }
};

const openExternalLink = (url) => {
      Linking.canOpenURL(url)
      .then((supported) => {
        if (supported) {
          Linking.openURL(url);
        } else {
          console.error('Cannot open URL:', url);
          Alert.alert('Error', 'Cannot open the link.');
        }
      })
      .catch((err) => console.error('An error occurred', err));
};

 
// main.dart

import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';

void handleMessageFromWebView(JavaScriptMessage message) {
  debugPrint('\x1B[33m[WebView] ${message.message}\x1B[0m'); // Yellow color
  try {
    final jsonObject = jsonDecode(message.message);
    final type = jsonObject['type'] as String;

    switch (name) {
      case 'close':
      openExternalLink(YOUR_APP_URL);
      default:
      debugPrint('\x1B[36m[WebView Event] $type\x1B[0m'); // Cyan color
    }
  } catch (e) {
    debugPrint('\x1B[31m[WebView Error] $e\x1B[0m'); // Red color
  }
}

void openExternalLink(String url) async {
  final uri = Uri.parse(url);
  if (await canLaunchUrl(uri)) {
    await launchUrl(uri);
  } else {
    debugPrint('Could not launch $url');
  }
}


What’s Next