Gate your extension
Check what a device may do before the action, and show the paywall when it may not.
Gating is three calls. gate() decides at the action and enforces. has() answers for display: a lock, a badge, a greyed button. showPaywall() answers a refusal with the paywall, in your brand and the buyer's language.
This page shows where each call goes: in the background, in your popup or side panel, in a content script, and in a page open in a tab. What the Free plan holds and which plan holds which feature are set in the dashboard; see Access and features.
Before you begin
- An extension wired to a tool:
toolaby.jsandtoolaby.config.jsbeside your code. See the Quickstart. - Under the tool's Pricing: the Free plan (Access), your plans, and the features each plan holds.
1. Choose what opens first
npx -y toolaby@latest wire puts the Toolaby popup in front of your popup. It opens yours at once, unless the person may not use the extension yet: then it shows the sign-in or the plans. Keep it in front: whatever you choose under Access in the dashboard shows at the next open, with no new build (on Live within about six minutes: installed copies ask Toolaby at most every five).
npx -y toolaby@latest create makes a new extension this way too. The alternative is your popup first, with every check in your code, for a popup that draws Toolaby's panel itself: Plasmo, or a design of your own. Such a popup draws the panel only when the person may not use the extension yet, or after a refusal, never under its own controls while they may.
| The Toolaby popup in front | Your popup first | |
|---|---|---|
| At every open | Your popup, at once; the sign-in or the plans first when nothing is free and no plan is held, or an account is required and none signed in | Your popup, at once |
| A Free plan that counts uses | Your gate() spends them at the paid action. Code that calls no gate(): each open spends one | Only the action spends |
Where you call gate() | At each action you sell — or nowhere, and each open is the use | At each action you sell |
| A change under Access | Shows at the next open | Shows where your code calls gate() |
| Suits | Every extension | A popup that draws the panel itself, or code that switches its popup with chrome.action.setPopup() |
countOpens in toolaby.config.js says whether an open is a use, and wire and upgrade write it from your code: false once it calls toolaby.gate(). To put the Toolaby popup in front of an extension whose popup is first, run npx -y toolaby@latest upgrade <tool-id> --toolaby-popup.
To put your popup first, run npx -y toolaby@latest upgrade <tool-id> --own-popup, which does steps 1 to 3, and step 4 too in a plain extension or a WXT project, or do them by hand:
- In
manifest.json, setaction.default_popupto your own page, andside_panel.default_pathto yours if you have one. - In
toolaby.config.js, setappPopup: ''andappSidePanel: ''. - Keep
toolaby-popup.html,toolaby-sidepanel.htmland their scripts in the extension:showPaywall()opens them. - Put the account bar in your popup and side panel:
<toolaby-account></toolaby-account>. See The account bar.
From then on, upgrade keeps your popup first and writes Toolaby's pages beside it. To put the Toolaby popup back in front, run upgrade <tool-id> --toolaby-popup.
The rest of this page applies to both.
2. Gate an action in the background
Where your extension does the work it charges for, ask first. Return the permit when it refuses, so the page that asked can show the paywall.
import { toolaby } from './toolaby.js';
toolaby.startBackground({
handlers: {
async exportAll({ tableIds }) {
const permit = await toolaby.gate({ feature: 'export' });
if (!permit.allowed) return { permit };
return { ok: true, file: await buildExport(tableIds) };
},
},
});This background is a module ("type": "module" in the manifest), which imports the client. In a classic background, which the Toolaby background loads with importScripts(), toolaby is already a global: leave out the import line.
gate() answers on the device when it can. It makes one request to count a metered use, or to ask about a feature the device's last token does not list. See gate() for every answer.
3. Show the paywall from your popup
import { toolaby } from './toolaby.js';
document.getElementById('export').addEventListener('click', async () => {
const r = await chrome.runtime.sendMessage({ action: 'exportAll', tableIds });
if (r.permit) return toolaby.showPaywall(r.permit);
save(r.file);
});The popup loads this script as a module: <script type="module" src="popup.js"></script>.
The popup gives way to the paywall for that reason: only the plans that hold export, the free-use meter, or the sign-in doors. It has a Back button. Once the device may continue (the buyer bought, signed in, or holds the feature), it returns to your popup by itself.
In a side panel, the paywall opens at the panel's width when your page says it is a side panel:
<html data-toolaby-surface="sidepanel">4. Draw locks with has()
const locked = !(await toolaby.has('export'));
exportButton.classList.toggle('locked', locked);
exportButton.addEventListener('click', () => {
if (locked) return toolaby.showPaywall({ feature: 'export' });
runExport();
});has() makes no request and spends nothing. It does not stop anything by itself: the action still calls gate(), since code on the buyer's machine can be changed.
Open the paywall from a lock with showPaywall(), rather than a screen of your own or openPaymentPage(). Toolaby counts the paywall it shows, and what opened it, for Analytics → Features; a screen of your own is not counted.
Call toolaby.refresh() once when your popup opens, so a purchase made elsewhere is known before you draw.
5. Count uses at the action
With Access → Free uses, pass the number of uses the action spends:
const permit = await toolaby.gate(invoices.length);
if (!permit.allowed) return { permit }; // reason 'limit', remaining, limitIt spends them only when it allows. Show what is left from the device's record, which needs no request:
const { usage } = await toolaby.getUser();
if (!usage.unlimited) meter.textContent = `${usage.remaining} of ${usage.limit} free`;6. From a content script
A content script bundled with toolaby.js (WXT, CRXJS and Plasmo bundle them) calls the same methods; each asks the background. showPaywall() there opens a new tab, since the page is not your extension's: the checkout page, or the sign-in page for a sign_in refusal. For a refused build it opens nothing.
A content script that is not bundled sends the same messages the client sends:
const permit = await chrome.runtime.sendMessage({ action: 'toolaby.gate', count: 1, feature: 'export' });
if (!permit.allowed) chrome.runtime.sendMessage({ action: 'toolaby.open', page: 'payment', plan: permit.needs?.[0] });7. From a page open in a tab
An editor or a report page of your extension, open in a tab, would lose its work if it navigated away. There showPaywall() opens the checkout page in a new tab, or the sign-in page for a sign_in refusal, and leaves the page as it is. After the purchase the extension is told, onPaid fires in the background, and the page's next has() answers yes.
8. Refused builds
When the tool's Versions refuse this build, every gate() answers { allowed: false, blocked: true, message }. showPaywall() shows your message in the popup or side panel.
9. Try every refusal on Test
| Refusal | How to produce it |
|---|---|
feature | A feature on a paid plan only, and a device that holds no plan. |
limit | Access → Free uses, then spend them. |
paid | Access → Nothing free. |
sign_in | Pricing → Access → Require an account, and sign out from the account bar. |
blocked | Versions: a minimum above the build's version. |
| Unlocked | Buy with 4242 4242 4242 4242, or Grant access on your customer's page under Customers. Sign in in the extension first, or add yourself with Create customer. |
See Testing for purchases that fail, renew or are refunded.