Pro entitlement runs through the Billing interface (lib/pay/billing.ts). It is wired to
ExtPay (extpay@3.1.2) and live:
lib/pay/config.ts:EXTPAY_APP_ID = 'bokal-test'— ⚠ PERMANENT. This IS the live production app id (registered on extensionpay.com, display name "Bokal", Stripe connected LIVE; the-testsuffix is historical). Never change it — a new id would orphan every existing license. There is no "swap to a production id" step: ExtPay serves test mode to unpacked installs and live mode to store installs on the SAME id.USE_MOCK_BILLING = false(→ExtPayBilling; flip totruefor theMockBillinglocal-flag path used in dev/QA/E2E, no account needed).ExtPayBilling(billing.ts):getEntitlement()→extpay.getUser().paid;openUpgrade()→extpay.openPaymentPage()(opens a tab — no popup blocker). Network errors propagate sosyncEntitlementCachefalls back to the cached value (14-day offline grace).- SW (
entrypoints/background.ts): deliberately constructs NO ExtPay and does NOT callstartBackground()— that function is only a relay for an extensionpay.com content script Bokal doesn't ship, and merely constructingExtPay()writes an install marker tostorage.sync. The panel constructs the client lazily insidebilling.ts, gated by engagement (see below).
- Free users never contact ExtPay.
syncEntitlementCacheis gated byshouldContactBilling()(lib/pay/engagement.ts): it returns false unless the user has opened the upgrade page (bokal:proEngaged) or holds a paid cache within grace. The SW never constructs ExtPay at all (see above). So a user who never buys/opens Pro makes zero network calls to extensionpay.com and zero off-device writes. This is what keeps the "nothing leaves your device" claim true (seedocs/store/privacy-policy.md). - No
onPaidcontent script. Using ExtPay'sonPaid/onTrialStartedwould require a declarative content script onhttps://extensionpay.com/*. To keep the manifest free of any content script (and any new host access), Bokal instead re-checks entitlement on panelvisibilitychange(App.tsx) — so a completed purchase is picked up when the user returns from the payment tab. Manifest adds no permissions, nohost_permissions, no content scripts (ExtPay needs onlystorage, already present).
- Register the extension id
bokal-teston extensionpay.com (or change the id in config to your registered one), connect your Stripe account, and create the plans (Monthly $4.99 / Annual $19.99 / Lifetime $29.99 launch price, Lifetime default). DONE for Bokal — app + Stripe are live; this section is kept as the generic runbook. - Build + load unpacked (
pnpm --filter @bokal/cookie-manager build, load.output/chrome-mv3). - Grant Bokal access to a site, open the panel, click ★ Unlock Pro → ExtPay page (test mode
asks for your ExtPay account password) → Stripe test checkout → pay with
4242 4242 4242 4242(any future expiry / CVC). Return to the panel → the profiles panel should unlock. - In DevTools, confirm a FREE session (never clicking Unlock Pro) makes no request to extensionpay.com.
- DONE — do not repeat: app id is live and permanent (see top), Stripe is LIVE, plans created
($4.99/mo · $19.99/yr · $29.99 lifetime launch price → ~$39 around 2026-09-14),
USE_MOCK_BILLING = false. An earlier revision of this file said to "swap to the production id before launch" — that instruction is OBSOLETE and following it now would orphan paid licenses. - (Post-launch, M13) wire the 7-day reverse trial via
extpay.openTrialPage()+user.trialStartedAt, triggered on saving the 2nd profile — deliberately deferred until there is traffic to A/B against.