Counting Calculi

Replace Your Chrome Extension's Inline Install

How to recreate a smooth extension installation experience without Chrome's inline install.

by Kevin McLaughlin

Google Sheets to Jupyter Notebooks

With the latest release of Chrome 71, Google has, due to security concerns, removed the inline-install feature for Chrome extensions, which let users install a web application’s associated extension directly from the app itself.

While this doesn’t matter much to standalone extensions like session buddy, it does make it difficult for apps like Owl & Scroll, which rely on an integrated chrome extension for essential functions, to create a smooth and pleasant onboarding experience for users.

With inline-install, users could install a chrome extension without ever leaving the app. This allowed developers to control the onboarding experience. Without inline-install, users have to leave the app for the chrome store to install the extension and then come back to the app of their own volition like so:

For users familiar with how Chrome extensions work, this may only be a small bump. But for users that aren’t as familiar, this may cause them to get lost in the chrome store, not realizing that they have to return to the app.

Quick Setup

While nothing will be as smooth as inline-install, there is, fortunately, a workaround that is nearly as good. At a high level, here’s how to implement the workaround:

  1. Send the user to your extension’s install page in the chrome store via a new tab link in your app.
  2. When the user installs the extension, inject its content scripts into the requisite web pages so the user doesn’t have to refresh any pages (a good idea even with inline-install).
  3. And, finally send the user back to the app.

For those pressed for time, here’s the code and what the user experience looks like in Owl & Scroll:

As you can see, after the user agrees to install the extension, they are automatically sent back to the app.

To use the code in your app and extension, just:

  1. Add your extension’s install link (in webApp.htnl) to your app.
  2. Add the background.js code to the top of your extension’s background.js file.
  3. Update the appDomainURLMatch and appDomainURLCononical variables for your app. The appDomainURLMatch should be whatever chrome match patterns fit your app’s domain and the appDomainURLCononical variable is the default (ie canonical) URL of your app.

This should work for any extension without any modification (except the variables above) so feel free to skip the rest of this post. But for the curious, we’ll briefly go over how it works.

How It Works

The first bit of code is simple enough. It’s just a link to the extension’s install page that opens in a new tab. You can put this wherever you’d like in your app.

<a target="_blank" rel="noopener noreferrer" href="https://chrome.google.com/webstore/detail/ehncpahhfadkdfneafgehhdfjfiddpje">
    Install Extension
</a>

Next, is the appDomainURLMatch. This is used to find the tab containing your app in the makeAppActive function using chrome’s url match patterns:

var appDomainURL = "*://app.owlandscroll.com/*";

The appDomainURLCononical variable is the default (ie canonical) URL of your app.

var appDomainURL = "*://app.owlandscroll.com/*";

If the user doesn’t already have your app open in another chrome tab (ie if the user installs your extension directly from the chrome store instead of from your link above), the makeAppActive function will open the app in a fresh tab using the canonical link above like so:

You should already have your content scripts defined in your manifest.js. Here’s what ours manifest looks like as an example:

/* content_scripts portion of our manifest.js */
"content_scripts": [
    {
      "matches": ["https://read.amazon.com/notebook*", "https://read.amazon.com/kp/notebook*"],
      "css": ["styles.css"],
      "js": [
        "libs/jquery-3.3.1.min.js",
        "parser.js",
        "insertionMarkup.js",
        "index.js"
      ],
      "run_at": "document_end"
    },
    {
      "matches": ["https://app.owlandscroll.com/*", "https://dev.owlandscroll.com/*" ],
      "js": [
        "config.js",
        "appMessages.js"
      ],
      "run_at": "document_end"
    }
  ]

The longest and most complex bit of code in our background.js file injects all your content scripts into their appropriate web pages like so:

/*
 * Inject content scripts when the user installs the extension so the user doensn't have to refresh the page
 */
chrome.runtime.onInstalled.addListener(function() {
  //Get content scripts from manifest
  chrome.manifest = chrome.app.getDetails();
  var scripts = chrome.manifest.content_scripts;
  var appTab;
  // Loop through all tabs of all windows...
  chrome.windows.getAll({ populate: true }, function(windows) {
    windows.map(function(window) {
      window.tabs.map(function(tab) {
        // ...loop through all content scripts urls matches...
        scripts.map(function(script) {
          script.matches.map(function(scriptUrl) {
            // ...if the tab url matches a content script url...
            if (tab.url.match(scriptUrl)) {
              // ...inject all javasScripts from the content script into the tab...
              if (script.js) {
                script.js.map(function(javasScript) {
                  chrome.tabs.executeScript(tab.id, { file: javasScript });
                });
              }
              // ...inject all css from the content script into the tab
              if (script.css) {
                script.css.map(function(css) {
                  chrome.tabs.insertCSS(tab.id, { file: css });
                });
              }
            }
          });
        });
      });
    });
  });
  makeAppActive();
  console.log("Extension and content scripts installed");
});

Note that the code above has so many loops because an extension may install multiple, different scripts on multiple, different web pages. For instance, Owl & Scroll injects a script called appMessages.js into the app, which lets the app and extension communicate with each other. Separately, it injects several css and js files into the Kindle Notes and Highlights page, which adds a button to import user’s highlights into Owl & Scroll.

Finally, at the end of the onInstall code, you’ll see that we send the user back to our app with the makeAppActive function:

const makeAppActive = () => {
  chrome.tabs.query({ url: config.appDomainURL }, tabs => {
    if (tabs.length) {
      var tab = tabs[0];
      chrome.tabs.sendMessage(tab.id, { sendingUserToApp: true }, function(
        response
      ) {
        console.log(response);
      });
      chrome.tabs.update(tab.id, { selected: true });
    } else {
      chrome.tabs.create({ url: appDomainURLCononical });
    }
  });
};

This code loops through all of the user’s tab to find the first one that matches your appDomainURLMatch. If it doesn’t find any it opens the app in a fresh tab at your ppDomainURLCononical.

And that’s it. With just a link to your extension’s install page and few lines of code added to the background.js file of your extension, you can (almost) replace chrome’s now missing inline-install feature.

To make an extra smooth experience, you can replace inline-install’s successCallback function with either sendRequest or using your own version of appMessage.js, which we’ll describe in a later post.