How to wait for user input from Firefox extension page before continuing
If you're new to browser extensions, check out my earlier tutorial for how to build your first extension or check out the documentation and tutorials at MDN.
The problem
I recently got an interesting question from a student about a problem they faced when developing a Firefox extension. Here's the question, paraphrased:
What I want to do is open an extension page (a user form) and wait for that page to close before having my background script continue execution.
Here was their basic code flow simplified:
So they wanted to first check if token
exists in the extension's storage and if not, open an extension page that asks for the token from the user, then retrieve it from storage and continue with the execution of the main logic.
This code above does not work. The await
on line 4 does not actually block the execution until the extension page is closed. A note here, the way this extensionPage
function creates the extension page is via windows.create
.
Instead, we need to refactor and reorganize the code a bit and then we can choose from one of (at least) three different options for how to proceed.
I put the entire code with the three options into a gist for easier reading to see how all the things come together.
Refactor logic to its own function
The first thing we want to do is to refactor the main logic (other than token management) into its own function:
function mainLogic(token) {
performAction1(token);
performAction2(token);
}
For the actual use case, the functions should be named better though. Since we don't have any domain knowledge in this example, I used vague names because naming is hard.
This way, we can trigger that function separately from different places which gives us more flexibility to solve our case.
The main part of the browserAction
listener then would be like this:
browser.browserAction.onClicked.addListener(async () => {
const { token } = await browser.storage.local.get('token');
if(!token) {
openTokenForm()
} else {
mainLogic(token)
}
})
Now we only run mainLogic
if we do have the token
.
Option 1: Save to storage, send message, retrieve from storage
The first option to solve this issue is to open the extension page that contains a form to ask for the token, store it into a storage and then use runtime.sendMessage
to send a message that it was done to the script:
We would then listen for that message in our background script with runtime.onMessage
:
You may notice that we do an extra trip to the storage here: we just stored something into storage and then immediately went and got it from there. On some use cases, you might not need that (see next option) but it might be good if there's a case where the token might be changed by something else as this would make sure that the token is what's the latest in the storage rather than what was sent by the extension page.
Option 2: Save to storage and send it in a message
The second option is mostly the same as the #1. The small change we'll make here is that we save one trip to storage and in the extension page, store it into storage and then send it as part of the message:
The only change to the previous is adding token
into the object that was sent as a message.
In the background script, we can then skip one step:
Option 3: Listen for changes in storage
Another option to know when the change has been made is to use a listener, specifically storage.local.onChanged.addListener
. The function provided to the listener is run every time the value changes.
Since this function is run on every change, we need to check that the value we're interested in got changed. Here, changes
is an object that looks like this:
{
token: {
newValue: 'value-that-was-stored',
oldValue: 'previous-value-or-undefined'
}
}
The object has keys that correspond to the keys whose values got changed in the storage and two values: newValue
and oldValue
that correspond to whatever the new and old values are respectively.
Which one to choose?
I would say it depends on the context and other functionality of your extension. For a simplified example like this, it does not make much of a difference. As an example, for more complex real-life extensions, it might be that the token
can change in multiple places but you don't want to trigger this specific function every time so in that case Option 3 would not be a great choice.
A note about Manifest V3
Above example is for Firefox MV2 extension as that is what they were building and what Firefox supports at the time of writing. If you want to do same in MV3 extension once those are available, you need to change a few things:
In manifest.json
manifest_version
from 2 -> 3 (see Migration guide)- change
browser_action
toaction
(see Migration guide) - move
https://example.com/*
frompermissions
tohost_permissions
(see Migration guide) - add add-on ID (see Extensions and the add-on ID)
In background.js
- change
browser.browserAction.onClicked.addListener
tobrowser.action.onClicked.addListener
(see Migration guide)
Those are the minimum changes needed for the extension to run in Firefox MV3 environment.
MV3 is still not fully available in Firefox so if you want to develop & test your MV3 extensions, you can use web-ext with --firefox-preview mv3
option.
I usually run mine with a full command: web-ext run --firefox-preview mv3 --firefox nightly --bc
to define browser version, MV3 support and to open browser console for easier debugging.
Feedback, ideas?
Did I miss a good way to solve this case? Or maybe I made a mistake somewhere?
Let me know in Mastodon or Twitter. And follow me in either for more exciting tech blog posts and extension content.