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');
}
}
Updated 2 days ago