SubSovereign
All guides

SubSovereign en React (web) — guía de integración

Esta guía cubre @subsovereign/react (sdk-react/) — la capa de interfaz de usuario (UI) de la paywall para aplicaciones web en React. Renderiza la paywall que diseñas en el panel de control de SubSovereign como componentes reales de React, desde el mismo motor de renderizado que usa la vista previa del panel de control, por lo que lo que apruebes en el panel es exactamente lo que se implementa.

Este SDK dibuja la paywall; no gestiona facturación ni derechos de acceso (entitlements). Combínalo con el SDK de datos para Web/JS (@subsovereign/js-sdk) para checkEntitlements() y la validación de compras — ambos están diseñados para usarse juntos.

Antes de empezar

Necesitarás un servidor SubSovereign en ejecución, una app registrada en el panel de control (un appId y una clave de API de rol SDK), y una paywall publicada (panel de control → Paywall → Publicar).

Paso 1 — Instalación

El paquete está en sdk-react/ y aún no está en npm — añádelo como dependencia de ruta o git (o copia la carpeta en tu proyecto):

npm install ./sdk-react   # path dependency while it's pre-npm
import { Paywall, usePaywallConfig } from '@subsovereign/react';

Paso 2 — Cargar la paywall publicada

usePaywallConfig obtiene la configuración para esta app/usuario. La variante A/B se elige en el servidor mediante userId, por lo que el mismo usuario siempre verá la misma 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'
});

Pasa null en lugar de parámetros para omitir la obtención (por ejemplo, mientras el usuario no haya iniciado sesión).

Paso 3 — Renderizarla

<Paywall
  config={config}
  loading={loading}
  loadingState={<Spinner />}          // optional
  emptyState={<FallbackPaywall />}    // optional — shown when no config is published
  onSelectProduct={(productId) => startCheckout(productId)}
/>

onSelectProduct se activa cuando el usuario pulsa un producto — conéctalo a tu proceso de pago (Stripe en web). Tras un pago exitoso, valida con el SDK de datos y vuelve a comprobar los derechos de acceso (entitlements):

await SubSovereign.validateStripeSubscription({ subscriptionId, productId, accessLevelId: 'pro' });
const { hasAccess } = await SubSovereign.checkEntitlements();
if (hasAccess) unlockProFeatures();

Si ya tienes una configuración (obtenida mediante el SDK de datos), omite el hook y pásala directamente. Para un control total existe el renderizado sin procesar: <PaywallRenderer config={config} onSelectProduct={…} />.

Botón de desistimiento (Compliance Passport) para la UE

Si vendes suscripciones a consumidores en la UE a través de internet, la Directiva (UE) 2023/2673 exige una función de desistimiento claramente etiquetada. El SDK la incluye lista para usar:

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"
    />
  );
}

No puedes cambiar el texto. La consola ofrece dos etiquetas para la función de desistimiento — "Desista del contrato aquí" y "Cancelar aquí" — y labelKey transporta la que el inquilino (tenant) haya elegido; las propias palabras provienen del servidor en el idioma del cliente. No hay forma de pasar tu propio texto. Anteriormente, la propiedad label aceptaba texto libre y ahora se ignora con una advertencia en la consola: una notificación estatutaria que puede reescribirse es una que puede suavizarse para convertirla en marketing, razón por la que tampoco puedes pasar un color.

🚩 Pasa también locale. Formatea la marca de tiempo en el acuse de recibo. Sin ella, la fecha se formatea según el dispositivo, por lo que un cliente alemán en un navegador estadounidense verá 9/3/2026 — que leerá como 9 de marzo, no 3 de septiembre. Se trata de la fecha en el registro de un acto legal, y la ambigüedad funciona en ambos sentidos.

🚩 Pasa strings, o la notificación estatutaria estará en inglés para todos. Las palabras las sirve tu servidor SubSovereign en el idioma que solicites — son nuestras, no tuyas, a propósito, para que una notificación legal no pueda suavizarse para convertirla en marketing — y el botón renderiza lo que se le pasa. No las obtiene por sí mismo porque el mismo contrato de componente debe funcionar en Roku, donde un componente de interfaz de usuario no puede realizar operaciones de red. useWithdrawalConfig es la única línea que conecta ambos.

Si la obtención falla, el botón sigue apareciendo, en inglés, en lugar de no aparecer. No incluir una función de desistimiento accesible ante el consumidor es tu incumplimiento; mostrarla en el idioma incorrecto, no. El hook devuelve error para que puedas registrarlo; nunca se lo muestres al cliente.

Apariencia — light, dark o auto (valor por defecto), y nada más. El diálogo pinta su propia tarjeta y su propio texto, por lo que es legible en cualquier página, clara u oscura; auto sigue la configuración del navegador del visitante, y donde no se detecta preferencia, resuelve en dark. No hay forma deliberada de pasar un color: se trata de una notificación estatutaria, y su redacción y colores están fijos para que se lea igual para cada cliente de cada inquilino. Si aplicas la misma norma en otra parte de tu interfaz de usuario, resolveAppearance(appearance, scheme) se exporta (scheme es 'light' | 'dark' | null).

Muestra el botón etiquetado destacado, confirma una vez, envía idempotentemente (un reintento de red nunca puede crear dos desistimientos) y muestra el acuse de recibo. El botón en sí no realiza ninguna operación de red más allá de la presentación, por lo que el control basado en config.enabled y config.jurisdictions depende de ti, como se ha indicado anteriormente. Se exportan piezas de nivel inferior (fetchWithdrawalConfig, useWithdrawal, submitWithdrawal) para interfaces de usuario personalizadas.

Referencia rápida

Quieres… Usa
Cargar la paywall publicada usePaywallConfig(params){ config, loading, error }
Dibujarla <Paywall config onSelectProduct … />
Control total del renderizado <PaywallRenderer config onSelectProduct />
Desistimiento en la UE, en el idioma del cliente useWithdrawalConfig({…, locale}) → pasa strings, locale y labelKey a <WithdrawalButton>
Derechos de acceso (entitlements) / validación la guía de Web y React Native

Pasos siguientes