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 Wall popup in front of your popup: every open is checked, and your popup opens only when the device may continue. The alternative is your popup first, with the check at the action — what npx -y toolaby@latest create makes.
| The Wall popup in front | Your popup first | |
|---|---|---|
| At every open | The policy is read and gate() asked; then your popup, or the paywall | Your popup, at once |
| A metered Free plan | Each open spends one use | Only the action spends |
Where you call gate() | Optional: the popup already checked | At each action you sell |
| Suits | Paid extensions, and those that need an account to be used at all | Free plans that hold everything or count uses at the action; features sold by plan |
To put your popup first:
- 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 the Wall's pages beside it. To put the Wall popup back in front, reverse steps 1 and 2.
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) };
},
},
});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 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.
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 → A number of 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 the checkout page in a new tab, since the page is not your extension's.
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 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 → A number of uses, then spend them. |
paid | Access → Nothing. |
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 Customers → Grant access. |
See Testing for purchases that fail, renew or are refunded.