Last post I showed how to create a Chrome browser extension that allows a web app to communicate with a native app. However, that solution only works with Chrome and Edge. In Firefox, you’d get a “TypeError: window.chrome is undefined” exception.
I find that the web app cannot send request directly to the background.js in Firefox. It needs to send the request to a content script using window.postMessage, which then passes it through to the background script. The content scripts are files that run in the context of the webpage (more info here). This flow would also work with Chrome and Edge. The new flow looks like this.

Let’s start with the content script.
const PAGE_SOURCE = 'MySampleApp';
// This event listener will receive request from the web app
window.addEventListener("message", function(event) {
// These 2 checks to ensure we only process requests from our app only
if (event.source != window)
return;
const isFromAllowedSource = event.data.source && event.data.source === PAGE_SOURCE;
if (!isFromAllowedSource)
return;
// This sends message to background.js
chrome.runtime.sendMessage(event.data);
}, false);
// This message listener will receive response from the background.js
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
// This sends response to web app
window.postMessage({ type: 'extension-result', response: message.response, requestId: message.requestId }, window.location.origin);
return true;
});
Next, we need to change the background script to handle the new flow.
const NATIVE_HOST_NAME = 'some.sample.domain.name';
chrome.runtime.onMessage.addListener(
(request, sender, sendResponse) => {
chrome.runtime.sendNativeMessage(NATIVE_HOST_NAME, request, (res) => {
chrome.tabs.sendMessage(sender.tab.id, { response: res, requestId: request.Id }, function(response) {
console.log('Message sent to tab [' + sender.tab.id + ']');
});
});
}
);
The differences in the background scripts are that we add listener for onMessage instead of onMessageExternal and we add our own callback function which sends the response to the content script.
Next we need to add a Firefox specific setting in the manifest.json to specify the ID of the extension and the content script.
{
"manifest_version": 2,
"name": "Sample Extension",
"version": "1.0.0",
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"permissions": [
"nativeMessaging"
],
"externally_connectable": {
"matches": [
"http://localhost:3000/*"
]
},
"content_scripts": [
{
"matches": [ "http://localhost:3000/*" ],
"js": [ "content.js" ]
}
],
"browser_specific_settings": {
"gecko": {
"id": "[email protected]"
}
}
}
}
Next we need to register the native app for Firefox using the following configuration NativeHostConfigFirefox.json
{
"name": "some.sample.domain.name",
"description": "Sample native app host",
"path": "native-host.bat",
"type": "stdio",
"allowed_extensions": [ "[email protected]" ]
}
Then we need to create registry key for Firefox
HKEY_LOCAL_MACHINE\Software\Mozilla\NativeMessagingHosts\some.sample.domain.name
or
HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\some.sample.domain.name
, if Chrome is installed only for the current user.
Next ,we need to change how the web app sends message to the extension. In the previous flow, sending message to the background script with window.chrome.runtime.sendMessage allows passing a callback function to handle the response. However, window.postMessage does not accept a callback function. In order to receive the response, we need to add an event listener on ‘message’. This means that if there’s an error, we might never receive a message in the event listener. We would need to implement some sort of timeout for it. My approach is to wrap the request to the content script in a Promise to make it easier to consume.
I create a class named BrowserExtension like this which wraps the request in a Promise. In the content and background scripts above, I pass the request.Id through in the response. This ID is what I use to keep track of which promise I need to resolve or reject later.
let isOnMessageListenerRegistered = false;
const promises = {};
const PAGE_SOURCE = 'MySampleApp';
export class BrowserExtension {
constructor() {
if (!isOnMessageListenerRegistered) {
window.addEventListener('message', (event) =>{
if (event.source === window && event.data.type && event.data.type === 'extension-result') {
const promise = promises[event.data.requestId];
if (!promise) {
return;
}
if (!event.data.response) {
this.rejectPromise(event.data.requestId, "No response from native app");
return;
}
if (event.data.response.Type === 'error') {
this.rejectPromise(event.data.requestId, event.data.response);
return;
}
this.resolvePromise(promise.resolve, event.data.response, event.data.requestId);
}
});
isOnMessageListenerRegistered = true;
}
}
resolvePromise(resolve, response, promiseId) {
resolve(response);
delete promises[promiseId];
}
rejectPromise(promiseId, message, timeout) {
const cb = () => {
if (promiseId in promises) {
const { reject } = promises[promiseId];
reject(new Error(message));
delete promises[promiseId];
}
};
Number.isInteger(timeout) ? setTimeout(cb, timeout) : cb();
}
sendMessage(request) {
return new Promise((resolve, reject) => {
// automatically reject this promise if it's not resolved within 5 secs
this.rejectPromise(request.Id, "Extension fails", 5000);
request.source = PAGE_SOURCE;
promises[request.Id] = { resolve, reject };
window.postMessage(request, window.location.origin);
});
};
}
Finally, the App.js needs to update to use the BrowserExtension class
import React, { useState } from "react";
import logo from './logo.svg';
import './App.css';
import { BrowserExtension } from './BrowserExtension';
function App() {
const [responseFromNativeApp, setResponseFromNativeApp] = useState("");
const handleOnclick = () => {
const browserExtension = new BrowserExtension();
browserExtension.sendMessage({ text: 'hello from web app' })
.then((response) => {
if (response) {
debugger;
setResponseFromNativeApp(response.text);
}
});
};
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
<h1>{responseFromNativeApp}</h1>
<button onClick={handleOnclick}>Send message to extension</button>
</header>
</div>
);
}
export default App;
This is the result. This also works on Chrome and Edge
