import { Balance, CheckoutResult, Identity, PaywallBootstrap, PaywallOffer, PaywallPrice, PaywallPurchaseDetailed, PaywallUser, TrialConfig } from '@monetize.software/sdk'; import { StorageAdapter } from '@monetize.software/sdk'; import { TrialStore } from '@monetize.software/sdk'; import { TransportClient } from '../shared/transport-client'; export type UserListener = (user: PaywallUser) => void; export type BalanceListener = (balances: Balance[]) => void; export type BootstrapListener = (bootstrap: PaywallBootstrap) => void; export interface RemoteBillingClientOptions { paywallId: string; apiOrigin?: string; } export declare class RemoteBillingClient { private readonly transport; readonly paywallId: string; readonly apiOrigin: string | undefined; private cachedBootstrap; private cachedUser; private cachedBalances; private identity; /** Storage proxy over transport: get/set/remove go to the offscreen * StorageAdapter (single source of truth for all tabs). PaywallUI writes * trial state here — all tabs see the same counter and it doesn't drift * between them. * * A read-modify-write race window still exists (two tabs simultaneously read * N → write N-1, drift of 1). Exact atomicity requires Phase 9: move the * entire TrialStore into offscreen and do recordBlock as a single handler * with one atomic operation. This is a rare edge case (opening the paywall * in multiple tabs within milliseconds). */ private remoteStorageAdapter; private userListeners; private balanceListeners; private bootstrapListeners; private unsubUserBroadcast; private unsubBalancesBroadcast; constructor(transport: TransportClient, opts: RemoteBillingClientOptions); bootstrap(opts?: { force?: boolean; signal?: AbortSignal; }): Promise; getCachedBootstrap(): PaywallBootstrap | null; /** Mirrors `BillingClient.activeApiOrigin`. The edge failover state lives in * the offscreen BillingClient and this method must stay sync, so the mirror * returns the configured origin: the content script never builds sibling * API URLs itself (the only consumer — the events endpoint — is the * offscreen tracker, which reads the real BillingClient). */ activeApiOrigin(): string; /** Sticky A/B assignment of this device. Mirrors * `BillingClient.getExperimentAssignment`: the offscreen BillingClient * resolves the assignment and materializes `experiment.assigned_variant` * into the bootstrap, which arrives here through the proxied bootstrap() — * so the mirror only needs to read the cached copy. */ getExperimentAssignment(): { experimentId: string; variant: string; } | null; /** Subscribe to bootstrap state. Structurally compatible with * `BillingClient.onBootstrapChange` — same microtask semantics for the * initial snapshot. In extension mode offscreen does not yet broadcast * bootstrapChange, so the listener fires only on self-initiated `bootstrap()` * calls within this RemoteBillingClient (popup re-fetches bootstrap → mirror * updates → listener fires). A cross-surface revalidate (another tab updated * bootstrap) does not reach the popup — that would require a separate * bootstrapChange broadcast in protocol.ts/server.ts. */ onBootstrapChange(cb: BootstrapListener, opts?: { immediate?: 'microtask' | 'sync' | 'none'; }): () => void; /** Shortcut over `bootstrap()` — returns the paywall prices (locale overrides * already applied in offscreen). Same caching semantics as `bootstrap()`. */ getPrices(opts?: { force?: boolean; signal?: AbortSignal; }): Promise; /** Sync snapshot of prices from the local bootstrap mirror. null = not loaded yet. */ getCachedPrices(): PaywallPrice[] | null; /** Sync snapshot of offers. null = bootstrap not loaded, [] = paywall has no * offers. Server-side targeting (countries/email/mode) is already applied by * the backend — only what's applicable to the current user is exposed. */ getCachedOffers(): PaywallOffer[] | null; getVisitorId(): Promise; getUser(opts?: { force?: boolean; signal?: AbortSignal; }): Promise; getCachedUser(): PaywallUser | null; /** Pure read of the OFFSCREEN cache — no network, no checkout-pending * consumption, no applyUser. The page-side mirror above starts empty on * every fresh load, so an offline decision made from it alone answered "no * subscription" to paying users; this asks the context that actually holds * the persisted user. Degrades to the mirror on a dead port. */ peekCachedUser(): Promise; /** Settled user for subscription-gate decisions — proxied to the offscreen * BillingClient, where auth hydration / identity sync / persisted caches * actually live (see BillingClient.getSettledUser). The mirror is updated so * a subsequent getCachedUser() stays consistent with the gate's answer. */ getSettledUser(opts?: { signal?: AbortSignal; }): Promise; /** Subscribe to user state. We mirror the offscreen broadcasts; the initial * snapshot is delivered via microtask from the local cache (if present) — * exactly like in BillingClient.onUserChange. Returns an unsubscribe function. */ onUserChange(cb: UserListener, opts?: { immediate?: 'microtask' | 'sync' | 'none'; }): () => void; getBalances(opts?: { force?: boolean; signal?: AbortSignal; }): Promise; getCachedBalances(): Balance[] | null; onBalanceChange(cb: BalanceListener, opts?: { immediate?: 'microtask' | 'sync' | 'none'; }): () => void; createCheckout(params: { priceId: string; successUrl?: string; errorUrl?: string; shopUrl?: string; trialDays?: number; idempotencyKey?: string; ignoreActivePurchase?: boolean; signal?: AbortSignal; }): Promise; /** Rich-shape list of the user's purchases (with price, currency, interval, * discount, cancel metadata). Through offscreen — there the real BillingClient * hits `/api/v1/paywall/[id]/user` with a Bearer token. Useful for the * customer-portal UI: cards + Cancel/Renew buttons. */ listPurchases(opts?: { signal?: AbortSignal; }): Promise; /** Support ticket through the offscreen BillingClient. File objects do NOT * survive chrome.runtime ports (messages are JSON-serialized, a File * degrades to `{}` — crbug.com/248548), so attachments are shipped as * base64: each file is staged in offscreen with its own request, then the * ticket references the staged ids. Bearer token / email substitution is * done by offscreen, as in the regular BillingClient. */ createSupportTicket(payload: { subject: string; content: string; email?: string; files?: File[]; }): Promise<{ ticket: { id: number; status: string; }; }>; /** Cancel a subscription through the backend. By default cancels at the end * of the current period (the user keeps access until the renewal date). * reason is required (validated by the backend) — collected via a reason * selector in the host UI. */ cancelSubscription(params: { subscriptionId: string; reason: string; signal?: AbortSignal; }): Promise<{ subscription: { status: string | null; canceled_at: string | null; cancel_at: string | null; cancel_at_period_end: boolean | null; }; }>; /** URL of the Stripe/Paddle/Chargebee customer portal. Proxies to the * offscreen BillingClient, which owns the Bearer session — so this works * from popup/options/content without access to the token. Same contract as * `BillingClient.getCustomerPortalUrl`: a backend 403 (no active * subscription / acquiring without a portal) surfaces as * PaywallError('forbidden') with `status: 403`. */ getCustomerPortalUrl(opts?: { returnUrl?: string; signal?: AbortSignal; }): Promise<{ url: string; }>; /** PaywallUI asks the billing client for storage for TrialStore and other * consumers. Returns a proxy: get/set/remove go through transport to the * offscreen storage = single source of truth for all tabs. */ getStorage(): StorageAdapter; /** Factory method for PaywallUI: instead of a local createTrialStore over the * storage proxy, we return a RemoteTrialStore — it sends each operation as * one atomic RPC to offscreen, where navigator.locks serializes the * read-modify-write. PaywallUI duck-types this method and prefers it over the * local factory when present. */ createTrialStore(config: TrialConfig): TrialStore; getIdentity(): Identity | null; setIdentity(identity: Identity | null): Promise; /** Load identity from offscreen. Used on the first connection of a * content-script — if another tab has already logged the user in, the current * one immediately picks up the identity without waiting for authChange. */ syncIdentity(): Promise; destroy(): void; private applyBootstrap; /** Update the user mirror and emit to listeners if it actually changed. Used * both for self-initiated RPCs (bootstrap/getUser) and for broadcasts from * offscreen — so the host's onUserChange handler gets a signal regardless of * who triggered the update. */ private applyUser; private applyBalances; private fireUserListeners; private fireBalanceListeners; } //# sourceMappingURL=RemoteBillingClient.d.ts.map