Inline Installation

Firefox extensions, similar to Chrome extensions, help us modify and personalize our browsing experience by adding new features to the existing sites.

Once we’ve published our extension to the Mozilla’s Add-on store(AMO), users who browse the AMO can find the extension and install it with one-click. But, if a user is already on our site where a link is provided to the extension’s AMO listing page, they would need to navigate away from our website to the AMO, complete the install process, and then return back to our site. That is a bad user experience.

The inline installation enables us to initiate the extension installation from our site. The extension can still be hosted on the AMO but users would no longer have to leave our site to install it.

We had to try out a few suggested approaches before we got it working.

InstallTrigger

InstallTrigger is an interface included in the Mozilla’s Apps API for installing extensions. Using JavaScript, the install method of InstallTrigger can be used to start the download and installation of an extension (or anything packaged in a .xpi file) from a Web page.

A XPI(pronounced as “zippy”) is similar to a zip file, which contains manifest file and the install script for the extension.

So, let’s try to install the Grammarly Extension for Firefox. To use it, we first need its .xpi file’s location. Once we have published our extension on the AMO, we can navigate to the listings page for it and get the link for the .xpi.

For our present example, here’s the listing page for Grammarly Extension.

Here, we can get the .xpi file’s location by right clicking on the + Add to Firefox button and clicking on Copy Link Location. Note that the + Add to Firefox button would only be visible if we browse the link on a Firefox browser. Otherwise, it would be replaced by a Get Firefox Now button.

Once we have the URL, we can trigger the installation via JavaScript on our web page.

InstallTrigger.install({
  'Name of the Extension': {
    URL: "url pointing to the .xpi file's location on AMO",
  },
});

Pointing to the latest version of the Extension

When we used the URL in the above code, the .xpi file’s URL was specific to the extension’s current version. If the extension has an update, the installed extensions for existing users would be updated automatically. But the URL to the .xpi on our website would be pointing to the older version. Although the old link would still work, we would always want new users to download the latest version.

To do that, we can either fetch the listing page in the background and parse the HTML to get the latest link. But that approach can break if the HTML changes.

Or we can query the Addons Services API, which returns the information for the extension in XML format.

For the Grammarly Extension, we first need its slug-id. We can get it by looking at its listing page’s URL. From https://addons.mozilla.org/en-US/firefox/addon/grammarly-1/, we can note down the slug which is grammarly-1

Using this slug id, we can now get the extension details using https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/grammarly-1. It returns the info for the Grammarly Extension. What we are particularly interested in is the value in the <install> node. That is what the desired value is for the latest version for our .xpi.

Let’s see how we can implement the whole thing using React.

import axios from 'axios';
import cheerio from 'cheerio';

const FALLBACK_GRAMMARLY_EXTENSION_URL =
  'https://addons.mozilla.org/firefox/downloads/file/1027073/grammarly_for_firefox-8.828.1757-an+fx.xpi';
const URL_FOR_FETCHING_XPI = `https://services.addons.mozilla.org/en-US/firefox/api/1.5/addon/grammarly-1`;

export default class InstallExtension extends Component {
  state = {
    grammarlyExtensionUrl: FALLBACK_GRAMMARLY_EXTENSION_URL,
  };

  componentWillMount() {
    axios.get(URL_FOR_FETCHING_XPI).then(response => {
      const xml = response.data;
      const $ = cheerio.load(xml);
      const grammarlyExtensionUrl = $('addon install').text();
      this.setState({ grammarlyExtensionUrl });
    });
  }

  triggerInlineInstallation = event => {
    InstallTrigger.install({
      Grammarly: { URL: this.state.grammarlyExtensionUrl },
    });
  };

  render() {
    return (
      <Button onClick={this.triggerInlineInstallation}>
        Install Grammarly Extension
      </Button>
    );
  }
}

In the above code, we are using the npm packages axios for fetching the xml and cheerio for parsing the xml. Also, we have set a fallback URL as the initial value in case the fetching of the new URL from the xml response fails.