I figured out how to get it to work, so posting some solutions here in case it helps others in the future!
All of this is with Chrome extension manifest v3, since Chrome is deprecating v2.
To iframe a Bubble app into a popup upon clicking the extension icon
If a user has an extension installed, it’ll show up in the ‘extension strip’ that’s to the right of the URL bar. You can make it so that when the user clicks on the extension’s icon, a popup appears, anchored to the icon, that loads whatever HTML/CSS/JS you want - it can also load an iframed Bubble app.
At its most basic, you need something like:
manifest.json
Notably here, you’re using the “action” capability to signify you want to open the file “popup.html” as a popup upon click
{
"name": "Blah",
"description": "Lorem ipsum",
"version": "0.1",
"manifest_version": 3,
"action": {
"default_popup": "popup.html"
},
...
}
popup.html
Notably here, there’s a div that’s for the iframe, and otherwise the HTML just invokes popup.js
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<script src="popup.js"></script>
<div id="iframe_div">
<div class="lds-ring">
<div></div>
<div></div>
<div></div>
<div></div>
</div>
</div>
</body>
</html>
popup.js
Notably, this does the actual loading of the iframe
let iframe;
let iframe_url = "bubble.io"; // This is the URL of what you want to iframe - this URL will load when the popup first opens
let width = 360;
let height = 400;
let iframe_onload = function () {
// This function fires when the iframe is finished loading
// Remove loading spinner
let loading = document.getElementsByClassName('lds-ring')[0];
if (loading) {
loading.remove();
}
}
window.onload = function () {
// Load iframe
setTimeout(function () {
let group = document.getElementById("iframe_div");
let i = document.createElement('iframe');
i.frameBorder = 0;
i.width = width;
i.height = height;
i.src = iframe_url;
i.onload = iframe_onload;
iframe = group.appendChild(i);
}, 100);
...
}
Communicating with the Bubble app in the iframe
This is where things get a bit annoying - when a website is running within an iframe, there are restrictions around how you can communicate with it.
Running code when the iframe loads a particular page
Sometimes, you may want the Chrome extension to run some arbitrary code when it detects that the user has navigated to a certain page of your Bubble app within the iframe. To do that, add this to popup.js:
window.addEventListener("message", (event) => {
const iframeUrl = event.data;
if (iframeUrl.includes("code=st_")) {
// Arbitrary code here
}
...
})
Getting the Bubble app’s page in the iframe to update
Say you want the code in the extension to make some kind of change on a page in the Bubble app that’s iframed. For example, maybe you want the Bubble app page to reflect a success / fail status based on logic in the code.
To do this, you need a content script - this is code that runs from the extension when the browser detects you’re on a certain URL.
First, add this to manifest.json, to load the content script on the right page(s):
...
"content_scripts": [
{
"matches": ["https://your.bubbleapp.url.here.io/*"],
"js": ["bubble-content-script.js"],
"all_frames": true
}
],
...
The way we’ll make this work is to inject code into the iframed Bubble app that opens a ‘connection’ that can pass messages to the rest of the Chrome extension code.
In bubble-content-script.js, you have this:
let port = chrome.runtime.connect({name: 'test'});
port.onMessage.addListener((msg, port) => {
if (msg.type == 'arbitrary-message-code-word-here') {
// Change HTML element on the Bubble app page to reflect success
document.getElementById('some_element_id').innerHTML = 'Success';
// Hide another element
document.getElementById('another_element_id').style.display = "none";
} else if (msg.type == 'a-different-message-code-word') {
...
}
});
Then in another extension code file, like popup.js, you can do something like this to send the iframed page a message:
window.onload = function () {
// Establish connection with Bubble app page's content script
let iframePort; // Can be used later in this function
chrome.runtime.onConnect.addListener(port => {
iframePort = port;
port.onMessage.addListener((msg, port) => {
console.log(msg);
});
port.postMessage('from-popup');
});
...
// Later on
iframePort.postMessage('arbitrary-message-code-word-here')
}