Adding event listeners

Our widgets will publish events to the following interfaces if they are available:

window.Android.postMessage

window.webkit.messageHandlers.ios.postMessage

window.ReactNativeWebView.postMessage

Events are sent as JSON with the following format:

{
	name,
	type,
	metadata
}

We also dispatch these events to the top-level window with window.dispatchEvent ( with the above payload in the event.details)

Code examples

See the below code examples of listening for widget events.

Webview

import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.fragment.app.Fragment
import com.example.basicnav.databinding.FragmentDashboardBinding
import org.json.JSONObject

const val WIDGET_URI = "https://widget-domain-that-we-provide"

class DashboardFragment : Fragment() {

    private var _binding: FragmentDashboardBinding? = null
    private val binding get() = _binding!!

    private lateinit var webView: WebView

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentDashboardBinding.inflate(inflater, container, false)
        val root: View = binding.root

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            WebView.setWebContentsDebuggingEnabled(true)
        }

        webView = binding.webView
        webView.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView, url: String) {
                val jsCode = """
                    if (window.__boot && typeof window.__boot === 'function') {
                        window.__boot('$token', { numCreditCards: $numCreditCards, hash: '$hash', widget_id: '$widgetId' });
                    }
                """
                webView.evaluateJavascript(jsCode, null)
            }
        }
        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true
        webView.addJavascriptInterface(WebAppInterface(this), "Android")
        return root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Retrieve the fragment from the arguments
        val fragment = arguments?.getString("fragment")

        val widgetId = "your-widget-id"
        val token = "token-for-user"

        Log.d("DashboardFragment", "URL fragment received: $fragment")

        // Load the WebView
        webView.loadUrl(WIDGET_URI)
    }

    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)
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

// WebAppInterface.kt
class WebAppInterface(private val fragment: DashboardFragment) {

    @JavascriptInterface
    fun postMessage(message: String) {
        // Handle the received message
        fragment.activity?.runOnUiThread {
            fragment.handleMessageFromWebView(message)
        }
    }
}
import UIKit
import WebKit

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

class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHandler {
    
    var webView: WKWebView!
    
    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 token = "your-token"
        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!) {
        // JavaScript code to be injected
        let jsCode = """
        if (window.__boot && typeof window.__boot === 'function') {
            window.__boot('\(token)', { numCreditCards: \(numCreditCards), hash: '\(hash)', widget_id: '\(widgetId)' });
        }
        """
        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.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 { WebView } from 'react-native-webview';

const WIDGET_URI = 'https://widget-domain-that-we-provide';

const DashboardScreen = () => {
  const webViewRef = useRef(null);

  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) => {
    // handle opening of external link here
  };

  const onLoadEnd = () => {
    const token = 'your-token';
    const numCreditCards = 2;
    const hash = 'your-hash';
    const widgetId = 'your-widget-id';

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

  return (
    <div>
      <WebView
        ref={webViewRef}
        source={{ uri: WIDGET_URI }}
        onMessage={handleMessage}
        onLoadEnd={onLoadEnd}
        javaScriptEnabled={true}
        domStorageEnabled={true}
      />
    </div>
  );
};

export default DashboardScreen;
// 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) {
    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');
    }
  }

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

iFrame

import React, { useEffect, useRef, useState } from 'react';
import axios from 'axios';

function App() {
  const iframeRef = useRef(null);
  const [hasBooted, setHasBooted] = useState(false);

  const fetchToken = async () => {
    // get your token here
  };

  const bootWidget = async () => {
    const token = await fetchToken();
    if (iframeRef.current && token) {
      const config = {
        action: 'boot',
        token: token,
        params: {
          widget_id: 'your_widget_id',
          // Add any additional options here
        },
      };

      // Send the message to the iframe
      iframeRef.current.contentWindow.postMessage(config, '*');
      setHasBooted(true);
    }
  };

  useEffect(() => {
    // Define the event listener function
    const handleMessage = async (event) => {
      if (event.data?.name !== "react-devtools-content-script") {
        console.log('Widget Event:', event.data);
      }

      // Handle external link event
      if (event.data?.type === 'externalLink' && event.data?.url) {
        window.open(event.data.url, '_blank');
      }
    };

    // Add the event listener
    window.addEventListener('message', handleMessage);

    // Clean up the event listener on component unmount
    return () => {
      window.removeEventListener('message', handleMessage);
    };
  }, []); 

  return (
    <div style={{ textAlign: 'center', marginTop: '20px' }}>
      <div id="widget-container">
        <iframe
          id="widget-frame"
          ref={iframeRef}
          src="your_widget_url"
          title="Widget Frame"
        />
      </div>
    </div>
  );
}

export default App;

What’s Next