⚠️ यह मार्गदर्शिका अंग्रेज़ी में है / This guide is in English. Hindi is not a language the SubSovereign SDK guides are translated into (DECISIONS #109). The content below is current and correct, and it is the same guide the English readers get. The EU withdrawal notice your customers see is served in twelve European languages by your SubSovereign server, which is a separate thing from the language of this document.
SubSovereign on React (web) — integration guide
This guide covers @subsovereign/react (sdk-react/) — the paywall UI layer for React web
apps. It renders the paywall you design in the SubSovereign dashboard as real React components, from
the same renderer the dashboard preview uses, so what you approve in the dashboard is exactly what
ships.
This SDK draws the paywall; it does not talk billing or entitlements. Pair it with the
Web/JS data SDK (@subsovereign/js-sdk) for checkEntitlements() and purchase
validation — the two are designed to be used together.
Before you start
You'll need a running SubSovereign server, an app registered in the dashboard (an appId
and an SDK-role API key), and a published paywall (dashboard → Paywall → Publish).
Step 1 — Install
The package lives in sdk-react/ and is not on npm yet — add it as a path or git dependency
(or copy the folder into your project):
npm install ./sdk-react # path dependency while it's pre-npm
import { Paywall, usePaywallConfig } from '@subsovereign/react';
Step 2 — Load the published paywall
usePaywallConfig fetches the config for this app/user. The A/B variant is chosen server-side by
userId, so the same user always sees the same paywall.
const { config, loading, error } = usePaywallConfig({
apiUrl: 'https://subs.yourdomain.com/api/v1', // YOUR self-hosted server
appId: 'your-app-id',
userId: currentUser.id, // your own stable user id
apiKey: 'YOUR_SDK_KEY',
locale: 'en', // optional, default 'en'
platform: 'web', // optional, default 'web'
});
Pass null instead of params to skip fetching (e.g. while the user is signed out).
Step 3 — Render it
<Paywall
config={config}
loading={loading}
loadingState={<Spinner />} // optional
emptyState={<FallbackPaywall />} // optional — shown when no config is published
onSelectProduct={(productId) => startCheckout(productId)}
/>
onSelectProduct fires when the user taps a product — wire it to your checkout (Stripe on web).
After checkout succeeds, validate with the data SDK and re-check entitlements:
await SubSovereign.validateStripeSubscription({ subscriptionId, productId, accessLevelId: 'pro' });
const { hasAccess } = await SubSovereign.checkEntitlements();
if (hasAccess) unlockProFeatures();
If you already have a config (fetched via the data SDK), skip the hook and pass it straight in.
For full control there's the raw renderer: <PaywallRenderer config={config} onSelectProduct={…} />.
EU withdrawal button (Compliance Passport)
If you sell subscriptions to EU consumers online, Directive (EU) 2023/2673 requires a clearly labelled withdrawal function. The SDK ships it ready-made:
import { WithdrawalButton, useWithdrawalConfig } from '@subsovereign/react';
function CancelSubscription({ user, sub }: { user: { id: string; locale: string }; sub: { id: string } }) {
// Fetches the withdrawal settings AND the notice's wording in the customer's language.
const { config, strings, error } = useWithdrawalConfig({
apiUrl: 'https://subs.yourdomain.com/api/v1',
appId: 'your-app-id',
apiKey: 'YOUR_SDK_KEY',
locale: user.locale, // the CUSTOMER's language, not your console's
});
// Log it: a silent failure here is how a tenant ships English to German customers and never finds
// out. The customer is never shown this — they get the button, in English.
if (error) console.warn('SubSovereign: withdrawal settings unavailable', error);
if (config && config.enabled === false) return null; // the tenant turned it off
return (
<WithdrawalButton
apiUrl="https://subs.yourdomain.com/api/v1"
appId="your-app-id"
apiKey="YOUR_SDK_KEY"
userId={user.id}
subscriptionId={sub.id}
strings={strings} // ← without this the dialog is ENGLISH for every customer
locale={user.locale} // ← and without this the DATE is formatted for the device
labelKey={config?.labelKey} // ← the label the tenant chose in the console
appearance="auto"
/>
);
}
You cannot change the wording. The console offers two labels for the withdrawal function —
"Withdraw from contract here" and "Cancel here" — and labelKey carries whichever the tenant
chose; the words themselves come from the server in the customer's language. There is no way to pass
your own text. A label prop used to accept free text and is now ignored with a console warning: a
statutory notice that can be reworded is one that can be softened into marketing, which is the same
reason you cannot pass a colour.
🚩 Pass locale too. It formats the timestamp on the acknowledgement. Without it the date is
formatted for the device, so a German customer on an American browser is shown 9/3/2026 — which
they will read as 9 March, not 3 September. It is the date on the record of a legal act, and the
ambiguity runs both ways.
🚩 Pass strings, or the statutory notice is in English for everyone. The words are served by
your SubSovereign server in the locale you ask for — they are ours rather than yours on purpose, so
a legal notice cannot be softened into marketing — and the button renders whatever it is handed. It
does not fetch them itself, because the same component contract has to hold on Roku, where a UI
component cannot do network at all. useWithdrawalConfig is the one line that connects the two.
If the fetch fails the button still appears, in English, rather than not appearing. Failing to put
a findable withdrawal function in front of the consumer is your breach; showing it in the wrong
language is not. The hook returns error so you can log that; never show it to the customer.
Appearance — light, dark or auto (the default), and nothing else. The dialog paints its own
card and its own text, so it is legible on any page, light or dark; auto follows the visitor's
browser setting, and where no preference can be detected it resolves to dark. There is
deliberately no way to pass a colour: this is a statutory notice, and its wording and its colours are
fixed so it reads the same for every customer of every tenant. If you mirror the rule elsewhere in
your UI, resolveAppearance(appearance, scheme) is exported (scheme is 'light' | 'dark' | null).
It shows the prominent labelled button, confirms once, submits idempotently (a network retry
can never create two withdrawals), and shows the acknowledgement. The button itself does no network
beyond the submit, so gating on config.enabled and config.jurisdictions is yours to do, as above.
Lower-level pieces (fetchWithdrawalConfig, useWithdrawal, submitWithdrawal) are exported for
custom UIs.
Quick reference
| You want to… | Use |
|---|---|
| Load the published paywall | usePaywallConfig(params) → { config, loading, error } |
| Draw it | <Paywall config onSelectProduct … /> |
| Full rendering control | <PaywallRenderer config onSelectProduct /> |
| EU withdrawal, in the customer's language | useWithdrawalConfig({…, locale}) → pass strings, locale and labelKey to <WithdrawalButton> |
| Entitlements / validation | the Web/JS data SDK |
Next steps
- Data layer (entitlements, purchases, GDPR): Web & React Native guide.
- Framework-agnostic sites (Vue/Svelte/plain HTML): Web Component guide.
- New to the concepts? Read How SubSovereign works.