Juha-Matti Santala
Community Builder. Dreamer. Adventurer.

New Firefox extension to help Pokemon TCG players

My explorations in the intersection of Pokemon TCG and technology continue as this week I published a new Firefox extension. You can install it from addons.mozilla.org.

Let's set up the scene: a new Pokemon TCG set arrives and you've ordered 100 booster codes for the new set from your favorite game store. Often, those sites send them to you via email in a plain-text list like this:

Here are your codes:
NOT-REAL-CODE-123
NOT-REAL-CODE-456
NOT-REAL-CODE-789
... [repeat 100-200 times]

The way you'd then redeem these codes is by going to Pokemon's website or opening up the Pokemon TCG Online client and copy-pasting them one by one, redeeming a set every 10 codes. If you miss a character or accidentally got whitespace included in your copy, the code will fail. And it's so easy to lose track of what was the last code you copied successfully if you lose track.

The extension

To help with that, I built a Firefox extension that uses Firefox's Sidebar feature and adds a layer of UI around the codes to solve most of the issues above (still can't fix the "1 at the time, redeem every 10 codes" thing though – although when writing this blog post, I got some ideas).

The way this extension works is simple: you select a chunk of text, any text, and right click to open the context menu and select Find Pokemon TCG redeem codes from the menu. The extension will then find all codes that match the format (13 characters from the set of 0-9, A-Z, optionally separated by dashes).

The extension then loads the codes into a Sidebar, and creates a UI with the code and a Copy button. When you click on Copy, it'll copy that code (without mistakes or extra whitespace) to your clipboard and mark the code as used by coloring it green. That helps you see which ones you've already copied.

Once you're done, you can hit Clear to remove the codes from the table and enjoy your cards.

If you're a fellow Pokemon TCG player, install this extension from the Firefox add-on store AMO and check out my other TCG extension as well. I've also built a few other tools and listed my favorites from other creators on this website.

A look under the hood

For those who want to learn and see how it was built, I'll share a few insights here. The entire code is available in GitHub with an MIT license.

This project uses three main features for extensions in Firefox: context menu, sidebar and storage.

Context menu

To use the context menu (the thing that pops up when you right click something in the browser), you'll first need to declare the permission for that in the manifest.json. In my background script, I then create a new menu item for this:

browser.menus.create({
  id: "find-codes",
  title: "Find Pokemon TCG redeem codes",
  contexts: ["selection"],
});

browser.menus.onClicked.addListener((info, tab) => {
  switch (info.menuItemId) {
    case "find-codes":
      browser.sidebarAction.open().then(() => {
        setTimeout(() => {
          browser.runtime.sendMessage({
            action: "addCode",
            codes: processCodes(info.selectionText),
          });
        }, 300);
      });
      break;
  }
});

Inside the create function, I define contexts: ["selection"] so that the menu item is only available when some text is selected. When the item is clicked, it opens the sidebar, process the selection to extract the codes with regular expressions and sends them to the sidebar script. Since it takes a moment for the sidebar script to run before it's ready to listen for messages, I had to add a short timeout.

I'm still not sure what the proper or best way to deal with the delay is. Timeouts are not super robust but it works for now.

Sidebar UI is an HTML page with its own CSS and Javascript that is rendered to a separate sidebar section in Firefox. I have there a bit of instruction text and a table that gets filled with the content based on the codes selected.

The sidebar script does a few things: it listens to the message from menu, loads and stores the code info from/to the extension storage and renders the content.

browser.runtime.onMessage.addListener((message) => {
  if (message.action === "addCode") {
    processCodes(message.codes);
  }
});

The event listener listens to any messages and if it receives one for "addCodes", it'll process them.

function processCodes(codes) {
  const processedCodes = codes.reduce((acc, cur) => {
    return { ...acc, [cur]: false };
  }, {});
  storeToStorage(processedCodes);
  render(processedCodes);
}	

Processing codes means three things: 1) turning them from a string array (like ["NOT-REAL-CODE-123", "DEF-NOT-REAL-345"]) into an object with the string as key and a false as the value. This keeps track of which codes have already been redeemed, 2) adds them to the storage and 3) renders the sidebar pane.

When rendering, it will create a table row (<tr>) for each code, put the code itself into one cell and a button to the other. The button then gets an event listener for the click event.

function onClickCopy(ev) {
  const target = ev.target;
  const parent = target.parentElement.parentElement;

  const code = parent.querySelector("td");
  const codeToCopy = code.textContent.trim();
  code.classList.add("copied");
  target.parentElement.classList.add("copied");

  navigator.clipboard.writeText(codeToCopy).then(() => {
    browser.storage.local.get("ptcgo-redeem-codes").then((results) => {
      let codes = results["ptcgo-redeem-codes"];
      codes = {
        ...codes,
        [codeToCopy]: true,
      };
      browser.storage.local.set({
        "ptcgo-redeem-codes": codes,
      }).then(() => { console.log('Successfully stored in onCLickCopy')}).catch(err => {
        console.log(err)
      });
    });
  });
}

The click listener does a few things itself. The main functionality is to copy the code to the clipboard but it also adds a CSS class to the <td> to denote its been copied and updates the store with this individual code being marked true.

Storage

A sidebar UI renders from scratch every time it's opened. This means that if we want to provide any kind of persistence, we need to store our data somewhere. To do that, we use the Storage API.

I update the store on two occasions: 1) when adding code(s) into the sidebar and 2) when copying an element to clipboard.

function storeToStorage(codes) {
  browser.storage.local.get("ptcgo-redeem-codes").then((fromStorage) => {
    const combinedCodes = {
      ...codes,
      ...fromStorage['ptcgo-redeem-codes'],
    };

    browser.storage.local.set({
      "ptcgo-redeem-codes": combinedCodes,
    }).then(() => { console.log('Successfully stored in storeToStorage')}).catch(err => {
      console.log(err)
    });
  });
}

To store the codes, I first load the existing ones from the storage, combine them with the new ones and then storing them back. I decided to default existing state in store ({...codes, ...fromStorage['ptcgo-redeem-codes']}) so that an existing code marked as copied won't get overriden.

The case of marking something as copied was already in the earlier example above.

Using this storage means the user can close the sidebar if needed (or by accident) and they won't lose their progress.

Original idea

To give credit where credit is due, I saw a similar concept done in the PTCGOStore.com, built in to their redeem section. It was so good I decided to move my patronage to them. However, sometimes they don't have all the codes or they may be out of stock so I'll still need to occasionally buy from other places. This blog post is not in any way endorsed nor sponsored by them.

Syntax Error

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