<img height="1" width="1" style="display:none;" alt="" src="https://dc.ads.linkedin.com/collect/?pid=557769&amp;fmt=gif%20https://dc.ads.linkedin.com/collect/?pid=557769&amp;fmt=gif">

Developing a modern browser extension

How we developed and released our Smart Spending Extension for Google Chrome, Mozilla Firefox and Microsoft Edge.

Last month, we released our Smart Spending Extension for Google Chrome, Mozilla Firefox and Microsoft Edge. This is built using Typescript

This is not our first attempt at delivering an extension. We had developed prototypes as native Firefox plugins (using XUL) and as COM extension objects. We even launched some but withdrew them after a while because we could not achieve a high-quality experience, reflecting our Smart Spending product. They were hard to develop, unique to each particular browser engine, and painful to maintain.

In recent years this has changed. The popularity of Google’s Chrome browser has led to its extension API implementation becoming a standard, with all major browsers now supporting it (Safari is coming in 2020). This means we have a great common platform to build on, with clear abstractions, and JavaScript is now the primary development language. These factors have significantly reduced the time required to build and (importantly) maintain any extension.

We’ve made use of several of these APIs in the Smart Spending Extension that we’ve recently launched. We chose a couple of them to write about in this blog.

OAuth

One of the differences between Reward Gateway and our competition is our support for customising the offer list on each client site. Our clients can decide which offers are visible to their employees. This means we must understand who is using the extension so that we only show the offers available to them.

Before the browser API, managing user identity and authentication was a significant activity involving proprietary token exchange. However, with the browser API’s OAuth support through the identity component, this is much simpler.

This left us to develop a simple OAuth web flow integration. This web flow takes the user through the steps to determine which client site they are a member of and authenticate using the normal credentials (including enterprise single sign-on.)

Sample

const responseUrl = await browserIdentity.launchWebAuthFlow({
url: <url>,
    interactive: true
});

const match = /code=([^&]+)/.exec(responseUrl);
const code = match && match[1];
if (!code) {
throw new AuthFlowError();
}

return await this.getAccessTokenByCode(code);

The <url> is the URL used to authenticate with the provider. This contains the client ID, scopes, and a dynamic redirect URI generated by the browser.

The provider will perform authentication and the browser will pop-up a dialog letting them confirm any necessary approvals.

The browser then captures the redirect to the redirect URI and gives control back, letting us extract the token and complete the OAuth flow.

Messages

When an offer is available, we need to provide users with really clear information about the offer. This required us to start loading content scripts into the pages themselves. These content scripts run in the context of the page they are embedded in. This makes (effective) cross-domain requests impossible. The browser API allows extensions to operate a ‘background process’ to handle this and provides APIs for message passing.

We inject our content script on every tab that is opened. This script waits for the central background process to inform it of what content to show, once the API  has returned. This is how we deliver the notification when you click the toolbar icon.

Sample

await browserTabs.executeScript('popup.js', tabId);
browserTabs.sendMessage(tabId, new PopupData(<some data>));

Here we inject our script into a tab which will run in the loaded page’s context. This context is isolated from our background process where we track the relevant information, so we have to send messages to it.

On the pop-up side we register a runtime listener which can then alter the loaded page:

browserRuntime.addListener('onMessage', (data: PopupData) => {
    const popup = generateElements(data);
    window.addEventListener('load', () => {
        document.body.appendChild(popup);
    });
});

Window and Tab

Alerting people to relevant information as they navigated a single site is easy. You simply check on each page load if they have moved away from the domain they were on and if you need to show an alert for it. Across multiple tabs and windows, this becomes more problematic and tracking tab/window state is important.

With older browsers like Internet Explorer, some of these tabs and windows actually had the same render process loaded. This gave the illusion that there was no problem until one more tab or window was opened and a new (separate) render process was spawned. This required native cross-process communication to keep state synchronised.

Thankfully, again, the browser API has provided a uniform way to manage this with the tab API and includes a unique per-tab ID. This allows us to track and maintain a state for each tab without having to know which process is responsible for it.

Conclusion

Having a standard API for development has allowed us to rapidly build an extension that works across all browsers. Being able to use TypeScript to do this made our lives even easier, particularly with the amount of async logic. This has allowed us to focus on creating a great user experience, fitting for our Smart Spending product. We hope you enjoy using it and we are very excited to see the uptake it achieves.

Our Engineering team is growing. If you have any questions about the extension or are interested in challenges like this we’d love to hear from you at rg.co/careers