September 21, 2020

Chrome Extensions (How to Build & Monetise)

Kameron Tanseli @kamerontanseli

MetaScan is a Chrome Extension that allows you to validate meta tag setups and preview what they’ll look like before publishing your website.

Starting Off Light

Much like how we have Create React App for building front end applications, we also have the Chrome Extension CLI for quickly spinning up Chrome Extensions. This saved me hours of setting up Webpack and configuring manifests and I highly recommend you use it!

I made a very conscious decision early on to try writing the initial MVP without a framework. I wanted the end result to be small in size as my users would be downloading the entire extension before using it.

It did get to a point where I did need a view library just for capturing events and handling memory issues, so I opted for using lit-html .

As the extension itself was quite simple in terms of state management I wrote my own small redux implementation:

const isDev = process.env.NODE_ENV === 'development';

let state = {
  license: {
    activated: false,
  },
  activation: {
    error: false,
  },
  currentTab: 'validations',
  validations: {
    config: {
      twitter: true,
    },
    passed: [],
    failed: [],
    siteInfo: {},
  },
};

export function reducer(action = {}, state = {}) {
  switch (action.type) {
    case 'ACTIVATION_FAILURE':
      state.activation.error = true;
      return state;
    case 'LICENSE_VALID':
      state.activation.error = false;
      state.license.activated = true;
      return state;
    case 'NAVIGATION':
      state.currentTab = action.payload.currentTab;
      return state;
    case 'VALIDATION_SET': {
      state.validations.passed = action.payload.passed;
      state.validations.failed = action.payload.failed;
      state.validations.siteInfo = action.payload.siteInfo;
      return state;
    }
    case 'VALIDATION_TWITTER_CHANGE': {
      state.validations.config.twitter = action.payload.twitter;
      return state;
    }
    default:
      return state;
  }
}

export const dispatch = (callback) => (action) => {
  const oldState = JSON.parse(JSON.stringify(state));
  if (isDev) console.log('Previous State', oldState);
  state = reducer(action, oldState);
  if (isDev) console.log('New State', state);
  callback(state);
};

It was quite simple to hook this up with my LitHTML views:

const emit = dispatch((state) =>
    render(
		app(state, emit), 
		document.getElementById('app')
	  )
);

Dealing with Communication

I wasted 1-2 hours on trying to figure out how to pass information from my content script (a script that executes code on the user’s current tab) to my extension popup.

Turns out that in order to communicate with the content script you need to first fetch the Tab ID:

contentScript.js

chrome.runtime.onMessage.addListener(
(request, sender, sendResponse) => {
  switch (request.type) {
    case 'FETCH_META_DATA': {
      sendResponse({
        ...validateMetaData(),
        siteInfo: getSiteInfo(),
      });
      break;
    }
    case 'ERROR': {
      console.error(request.payload);
      break;
    }
    default:
      break;
  }
}
);

popup.js

chrome.tabs.getSelected(null, tab => {
	chrome.tabs.sendMessage(
		tab.id, { type: 'FETCH_META_DATA'}, (response) => {}
	);
})

Monetisation & Distribution

Now that the UI and Business Logic was sorted I needed to find a way to monetize my creation. Taking inspiration from CSS Scan I opted to host my extension on Gumroad.

Distribution

I could have gone the route of uploading my extension to the Chrome Web Store. However, as the extension is essentially useless without a License Key I would have failed the Extension Review.

Instead, the extension build is uploaded as a zip to GumRoad. It’s quite a simple process to add the extension to Chrome and as my customers were developers I didn’t think the extra step of unzipping and adding via chrome://extensions was that difficult.

Licensing

Fortunately, GumRoad has an inbuilt License Key API.

Unfortunately, it requires a server to call it, so I had to add to build a small web service to call from my extension.

Note: The API only accepts URL encoded requests and not JSON (this will save you 30 minutes of wondering why it doesn’t work):

const result = await axios({
  url: `https://api.gumroad.com/v2/licenses/verify`,
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  data: `product_permalink=<permalink>&license_key=${
    encodeURIComponent(license_key)
  }`,
});
  1. 2

    Thanks for this Kameron, it's very helpful! Would you mind providing more detail on how you made the web service to call from your extension so that you could access the GumRoad License Key API?

    1. 1

      I made a lambda that was open to CORS and basically used that to call GumRoad:

      .post("/api/validate_key", async (req, res) => {
          const { license_key } = req.body;
          try {
            const result = await axios({
              url: `https://api.gumroad.com/v2/licenses/verify`,
              method: "POST",
              headers: { "Content-Type": "application/x-www-form-urlencoded" },
              data: `product_permalink=xxxxxx&license_key=${encodeURIComponent(
                license_key
              )}`,
            });
            console.log(result.data);
            res.statusCode = 200;
            return res.end(
              JSON.stringify({
                status: 200,
                active: result.data.success,
              })
            );
          } catch (error) {
            console.error(error);
            res.statusCode = 200;
            return res.end(JSON.stringify({ status: 200, active: false }));
          }
        })
      
Today's Top Milestones
  • Passed 2,000 downloads!
    Just 2 weeks after posting my previous milestone, I’ve reached 2,000 podcast downloads. I’ve been trying to post as consistently as I can since the ep
  • Finally found a good acquisition channel
    And it's plain old search ads 😂 I cannot believe it took me that long to lean into them. I actually did try some adwords right when I started, but at
  • Logos for design startups are here!
    Hey there! After three weeks of hard work from our designer Lucie, the logo catalog for design startups is complete! This means that instead of havin
  • Temporarily pausing work on Good Code 👮
    Hi there everyone 👋 For the past 2 months, I've worked on Good Code — a website where you can download free Adobe XD templates that you can use to im
  • Development updates and changes
    So I've been getting a lot of feedback in the past few weeks and I've finally managed to get the design and the front-end done to a point where the te
  • Finished MVP and asked Beta Testers for Help
    I finished the first version of Product Explorer and asked on Twitter for help. Far more people than I had expected expressed their interest in beta-t
  • From 0 to integrations, in 5 minutes
    Today is a special day for Pizzly. We're live on Product Hunt 🚀. I strongly believe that being an open-source project does not mean we shouldn't do p
  • 100k all-time users 🚀
    Today, Hoppscotch crossed 100k all-time users within 1 year of initial commit. Hoppscotch is an open sourced API request builder. Online alternative t
  • Go-to-Market strategies for SaaS companies
    SaaS Boss Episode 44 - Today I interview Dekker Fraser and we talk about Go-to-Market strategies for SaaS companies. Dekker is a veteran Silicon Valle
  • Shipped a new feature!
    *Here the problem I'm solving:* - For me and people like me, the new tab seems to be not of any work. - Just some random website icons and google sear