Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

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:

browser.browserAction.onClicked.addListener(async () => {
    let { token } = await browser.storage.local.get("token");
    if (!token) {
      await extensionPage();
    }
    token = await browser.storage.local.get("token");
    performAction1(token);
    performAction2(token);
}
background.js

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:

<form>
  <label for="token">Token: <input type="text" id="token" /></label>
  <button type="submit">Save</button>
</form>

<script src="extensionPage.js"></script>
extensionPage.html
document.querySelector('form').addEventListener('submit', async (ev) => {
  ev.preventDefault()
  const token = document.querySelector('#token').value;

  await browser.storage.local.set({token});

  browser.runtime.sendMessage({
    action: 'tokenSaved'
  })
})
extensionPage.js

We would then listen for that message in our background script with runtime.onMessage:

browser.runtime.onMessage.addListener(message => {
  if(message.action === 'tokenSaved') {
    browser.storage.local.get('token').then(({token}) => {
      mainLogic(token)
    });
  }
})
background.js

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:

document.querySelector('form').addEventListener('submit', async (ev) => {
  ev.preventDefault()
  const token = document.querySelector('#token').value;

  await browser.storage.local.set({token});

  browser.runtime.sendMessage({
    action: 'tokenSaved',
    token
  })
})
extensionPage.js

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:

browser.runtime.onMessage.addListener(message => {
  if(message.action === 'tokenSaved') {
    mainLogic(message.token)
  }
})
background.js

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.

browser.storage.local.onChanged.addListener(changes => {
  if('token' in changes) {
    mainLogic(changes.token.newValue)
  }
});
background.js

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

  1. manifest_version from 2 -> 3 (see Migration guide)
  2. change browser_action to action (see Migration guide)
  3. move https://example.com/* from permissions to host_permissions (see Migration guide)
  4. add add-on ID (see Extensions and the add-on ID)

In background.js

  1. change browser.browserAction.onClicked.addListener to browser.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.

Syntax Error

Sign up for Syntax Error, a monthly newsletter that helps developers turn a stressful debugging situation into a joyful exploration.