SubSovereign
All guides

⚠️ यह मार्गदर्शिका अंग्रेज़ी में है / 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