React bindings — @monetize.software/sdk-react
Provider, hooks and declarative components on top of @monetize.software/sdk. ≤ 2 KB gzip (bindings only — the UI lives inside the SDK).
Works in both browser scenarios:
- Drop-in PaywallUI — use
<PaywallButton>/<PaywallGate>to open the modal declaratively, plus hooks (usePaywallUser,usePaywallTrial) to read state in your components. - Custom UI — skip
<PaywallButton>/<PaywallGate>, use hooks only (usePaywallPrices,usePaywallAccess,usePaywallUser) to power your own pricing cards, feature gates, and dashboards.
pnpm add @monetize.software/sdk-react @monetize.software/sdk reactRequires react: >= 18 (uses useSyncExternalStore for concurrent-safe snapshots).
Working reference app:
github.com/monetize-software/sdk-examples/tree/main/nextjs
— Next.js 16 + App Router + Tailwind. Every hook and component shown
below is wired into a runnable page; clone, set two env vars, npm install,
npm run dev.
Quick start
import {
PaywallProvider,
PaywallGate,
PaywallButton,
usePaywallUser
} from '@monetize.software/sdk-react';
function App() {
return (
<PaywallProvider
options={{
paywallId: 'YOUR_ID',
apiOrigin: 'https://pay.your-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 on the platform.
<PaywallProvider>
Accepts one of two mutually exclusive props (TypeScript discriminated union):
options — Provider creates the instance
<PaywallProvider
options={{
paywallId: '3',
apiOrigin: 'https://pay.your-domain.com',
auth: true
}}
>
<App />
</PaywallProvider>The instance is built inside useEffect (client-only); on the server the context value is null. Every hook does a graceful fallback (null / { status: 'loading' }), so the Provider is safe to render in Next.js without 'use client' shenanigans on the subtree.
The cleanup effect calls paywall.destroy() — StrictMode double-mount won’t leak.
options is not reactive. The Provider creates the instance once; switching paywallId between renders won’t rebuild PaywallUI. Force it via key:
<PaywallProvider key={paywallId} options={{ paywallId, apiOrigin }}>Reactive option swaps are intentionally not supported — use key to force a fresh instance.
What’s available
SSR / Next.js App Router
@monetize.software/sdk-react ships a 'use client' directive on its entrypoint — the bundler crosses the client boundary itself, so the host can import PaywallProvider / hooks directly in a server component without wrapping.
// app/providers.tsx
'use client';
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!,
auth: true
}}
>
{children}
</PaywallProvider>
);
}
// app/layout.tsx
import { PaywallProviders } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<PaywallProviders>{children}</PaywallProviders>
</body>
</html>
);
}Hooks called from server components return the SSR snapshot (null / loading). On the client, real data flows in after mount.
Which SDK channel does it work with?
@monetize.software/sdk-react is drop-in for any structurally compatible PaywallUI:
| Scenario | Source of PaywallUI |
|---|---|
| Web / SPA | Provider options → internally new PaywallUI() from @monetize.software/sdk. |
| Chrome Extension (React popup) | createPaywallUI() from @monetize.software/sdk-extension (Remote-proxied) → Provider instance. |
| Tests (vitest + RTL) | Mocks or a real PaywallUI → Provider instance. |
Re-exported types
For ergonomics, core SDK types are re-exported — the host doesn’t need a second import from @monetize.software/sdk:
import type {
PaywallUI,
PaywallUIOptions,
PaywallEvent,
PaywallEventHandler,
PaywallStateSnapshot,
PaywallAccessResult,
GetAccessOptions,
OpenOptions,
AnalyticsOptions,
PaywallUser,
PaywallPrice,
PaywallBootstrap,
PaywallSettings,
PaywallOffer,
Identity,
AuthSession
} from '@monetize.software/sdk-react';PaywallUserState (the discriminated union returned by usePaywallUser()) is exported directly from @monetize.software/sdk-react:
import type { PaywallUserState } from '@monetize.software/sdk-react';Single source of truth is still @monetize.software/sdk — this is pure pass-through.