Package Exports
- @monetize.software/sdk-react
Readme
@monetize.software/sdk-react
React bindings for @monetize.software/sdk — Provider, hooks and
declarative paywall components. Works with the web SDK and the extension SDK
(any drop-in-compatible PaywallUI).
- Bundle: < 2 KB gzip (bindings only — the UI lives inside the SDK).
- React: >= 18, uses
useSyncExternalStorefor concurrent-safe snapshot reads. - SSR: safe out of the box. On the server, hooks return
null/{ status: 'loading' }; thePaywallUIinstance is created only on the client. - TypeScript: full type-level contract (
src/contract.ts) — if the public surface of the SDK shifts, thesdk-reactbuild fails attsc.
Installation
pnpm add @monetize.software/sdk-react @monetize.software/sdk reactQuick start
import {
PaywallProvider,
PaywallGate,
PaywallButton,
usePaywallUser
} from '@monetize.software/sdk-react';
function App() {
return (
<PaywallProvider
options={{
paywallId: 'YOUR_ID',
apiOrigin: 'https://your-paywall-domain.com',
auth: true
}}
>
<PaywallGate fallback={<UpgradeCTA />}>
<PremiumFeature />
</PaywallGate>
<PaywallButton>Upgrade</PaywallButton>
</PaywallProvider>
);
}
function UpgradeCTA() {
const account = usePaywallUser();
if (account.status === 'loading') return <p>…</p>;
if (account.status === 'guest') return <p>Hi guest! Unlock full access.</p>;
return <p>Hi, {account.user?.email ?? 'there'}! Unlock full access.</p>;
}apiOrigin must match the custom_domain configured for your paywall in the
platform.
Provider
<PaywallProvider> accepts one of two props:
// Option 1 — Provider creates the instance itself
<PaywallProvider options={{ paywallId, apiOrigin, auth: true }}>
// Option 2 — host supplies a ready instance (extension / shared singleton / tests)
import { createPaywallUI } from '@monetize.software/sdk-extension';
const paywall = createPaywallUI({ paywallId, apiOrigin });
<PaywallProvider instance={paywall}>If paywallId changes dynamically, remount the Provider via
<PaywallProvider key={paywallId} options={...}> — reactive option rebuilds are
intentionally not performed.
Hooks
| Hook | Returns | When it triggers a rerender |
|---|---|---|
usePaywall() |
PaywallUI | null |
instance change (rare) |
usePaywallState() |
{ open, view, error } |
any state-machine change |
usePaywallUser() |
PaywallUserState (loading | guest | signed_in) |
userChange / authChange |
usePaywallAccess(opts?) |
{ status, result } |
userChange / purchase_completed |
usePaywallPrices() |
{ prices, loading, error } |
bootstrap refresh |
usePaywallOffer(priceId) |
ResolvedOffer | null |
ready + 1Hz tick while countdown is live |
usePaywallOffers() |
PaywallOffer[] | null |
ready (bootstrap refresh) |
usePaywallTrial() |
TrialStatus | null |
trial_blocked / trial_expired |
usePaywallVisibility() |
VisibilityStatus | null |
ready / visibility_blocked |
usePaywallEvent(event, handler) |
— | subscribes with a stable handler ref |
All hooks are safe before the Provider mounts (they return null / loading) —
you can use them in SSR without 'use client' wrappers on the consuming subtree.
Components
<PaywallGate>
Declarative gate: loading → fallback → children.
<PaywallGate
loading={<Skeleton />}
fallback={({ open }) => <button onClick={open}>Upgrade</button>}
openOnBlocked={false} // if true — calls paywall.open() automatically
>
<PremiumFeature />
</PaywallGate><PaywallButton> / <PaywallSupportButton>
Sugar over paywall.open(). By default renders a native <button> with all
your className/disabled/aria-* props forwarded. For a custom element use
the render prop:
<PaywallButton render={({ open, ready }) => (
<MyButton onClick={open} disabled={!ready}>Upgrade</MyButton>
)} />mode switches between open() / openSupport() / openSignin() / openSignup():
<PaywallButton mode="support">Need help?</PaywallButton>
<PaywallButton mode="signin">Sign in</PaywallButton>
<PaywallButton mode="signup">Create account</PaywallButton>mode="auth" оставлен как алиас для signin (back-compat).
Для анонимного signin'а используй usePaywall().signInAnonymously() напрямую — он headless (без модалки), хост сам управляет loading-стейтом кнопки.
SSR / Next.js
'use client'; // on the Provider, not on the consumer subtree
import { PaywallProvider } from '@monetize.software/sdk-react';
export function PaywallProviders({ children }) {
return (
<PaywallProvider
options={{
paywallId: process.env.NEXT_PUBLIC_PAYWALL_ID!,
apiOrigin: process.env.NEXT_PUBLIC_PAYWALL_ORIGIN!
}}
>
{children}
</PaywallProvider>
);
}Hooks can be called from server components in typed-null scenarios (they'll
return null / loading anyway). The recommendation is to keep hook logic in a
client component.
SDK contract guard
pnpm typecheck validates src/contract.ts — it lists every
point of contact with the public SDK API (PaywallUI methods, snapshot fields,
event names). Any drift in ../sdk is caught here before it hits production.
After SDK changes, refresh the dist for type resolution:
cd ../sdk && pnpm build
cd ../sdk-react && pnpm typecheckDevelopment
pnpm install
pnpm dev # → http://localhost:5080/demo/
pnpm typecheck # TS validation + contract guard
pnpm test # vitest + @testing-library/react
pnpm test:e2e # playwright against the demo
pnpm build # ESM + CJS + d.ts → dist/API reference
Full JSDoc comments on every public export are inline in the sources:
src/PaywallProvider.tsx— Provider, lifecyclesrc/hooks/— all hookssrc/components/— declarative componentssrc/contract.ts— SDK contact points