Key event handling

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


Missing access token event

If an embedded component 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 component. 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 embedded component 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 embedded component is clicked. This will automatically be sent when the user clicks the "Exit" button (shown on the initial/home page of the component) 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