The popup's questions
What getUser() answers, so the popup can say the true thing without a network call.
The client answers getUser() from what it already holds, so the popup can say "renews on the 12th", "ends Friday", "payment failed" or "3 free uses left" instead of "premium".
{
paid: boolean,
via: 'subscription' | 'licence' | 'trial' | 'grant' | null,
email: string | null,
paidAt: string | null,
plan: { id, name, billing, interval } | null,
subscription: { status, periodEnd, cancelAt } | null,
trial: { endsAt, active } | null,
usage: { used, limit, remaining, unlimited },
version: { blocked, message }
}A tool inside a bundle the buyer holds answers as held — via says how the bundle is held.
Opening the pages
openPaymentPage(planId?)— the checkout, with a plan preselected.openConnectPage()— sign in, then hand the extension its link.openAccountPage()— what the buyer owns, their devices, the billing portal.
The paywall you do not design
When the device is not unlocked, the boilerplate's popup shows a panel it draws from what the platform sends — not from anything in your code:
- the free-use meter for this device, its bar turning to your accent when the uses are gone;
- the plan to buy, worded exactly as your tool's page words it — name, price, tagline, the Recommended badge — with one button that opens the checkout for it, and the other plans as quiet rows below;
- Already bought it? Sign in, and who sells it.
It wears your brand: the accent and its text colour, the corners and the colour mode you set under Configure → Branding are the tokens the panel is drawn in, so the popup and the buyer pages look like one thing. Change Branding or Pricing in the dashboard and the panel follows on every device within a day; it speaks the buyer's language, as the pages do. The panel is paywall.js and paywall.css in the boilerplate: leave them alone, and put your own popup's look in popup.css.
The facts come from GET /api/tools/<slug>?lang=<code> — public, nothing in it that the tool's page does not already show — and are kept in chrome.storage.local for a day, so the popup opens offline and at once.