SubSovereign Web Component — guía de integración (Vue, Svelte, Angular, HTML plano)
Esta guía cubre sdk-web/ — el SDK para navegador independiente del framework. Incluye un cliente de datos basado en fetch y dos Componentes Web, <subsovereign-paywall> y <subsovereign-withdrawal>, que funcionan en cualquier sitio — Vue, Svelte, Angular o una página HTML plana — sin pasos de compilación ni dependencias. (¿Desarrollas en React? Usa la guía de React en su lugar.)
Antes de empezar
Necesitas un servidor SubSovereign en ejecución, una app registrada en el panel de control (un appId y una clave de API con rol de SDK), y un paywall publicado.
Paso 1 — Cargar y configurar
<script type="module">
import { SubSovereign } from './sdk-web/src/index.js'; // importing registers the elements
SubSovereign.configure({
apiKey: 'YOUR_SDK_KEY',
appId: 'your-app-id',
baseUrl: 'https://subs.yourdomain.com/api/v1', // YOUR self-hosted server
userId: currentUser.id, // your own stable user id
locale: 'en',
});
</script>
Paso 2 — Comprobar qué puede acceder el usuario
const { hasAccess } = await SubSovereign.checkEntitlements();
if (hasAccess) showPremiumContent();
else showPaywall();
Para un bloqueo simple existe hasAccess(), opcionalmente para un nivel de acceso concreto. Falla de forma segura: ante un error de red devuelve false, así un fallo puntual nunca desbloqueará contenido de pago por accidente.
if (await SubSovereign.hasAccess('pro')) showPremiumContent();
checkEntitlements() lanza una excepción ante un fallo para que puedas mantener al usuario con el último acceso conocido; hasAccess() nunca lanza excepciones. Elige el método cuyo comportamiento ante fallos se ajuste a tu caso.
Paso 3 — Mostrar el paywall y vender
<subsovereign-paywall id="pw"></subsovereign-paywall>
<script type="module">
const pw = document.getElementById('pw');
pw.config = await SubSovereign.getPaywallConfig(); // platform defaults to 'web'
pw.addEventListener('select-product', (e) => {
startCheckout(e.detail.productId); // wire to your checkout
});
</script>
El tamaño es responsabilidad de tu página. El ancho predeterminado es de 420 px; amplíalo con la propiedad personalizada CSS (las propiedades personalizadas atraviesan el shadow DOM) y/o escala de forma uniforme:
subsovereign-paywall { --ss-paywall-max-width: 900px; transform: scale(1.4); }
Conceder la compra: este SDK no incluye llamadas de validación en el lado del cliente. Completa el pago con tu proveedor, concede el acceso en el lado del servidor — tu backend llamando a tu servidor SubSovereign, o el puente de facturación personalizado firmado (POST /entitlements/external) — y vuelve a comprobar:
const again = await SubSovereign.checkEntitlements();
if (again.hasAccess) unlock();
Flags de características
Activa características desde el servidor sin necesidad de desplegar. Falla de forma segura: ante un error de red obtienes {}, así cada flag se interpreta como desactivado.
const flags = await SubSovereign.getFeatureFlags(); // { newPlayer: true, ... }
if (flags.newPlayer) showNewPlayer();
Atribución de motor de respuesta de IA (GEO)
Si usas las funciones de Adquisición/GEO, captura de dónde vinieron los visitantes — el referente de IA existe solo en la página de aterrizaje, y el ID de usuario existe solo después del registro, por lo que es un proceso de dos pasos de almacenamiento temporal y envío:
import { ssCaptureLanding } from '@subsovereign/web';
// On every page of your marketing site, ONCE THE VISITOR HAS ACCEPTED YOUR CONSENT BANNER:
onConsentAccepted(() => ssCaptureLanding());
// …later, the moment the user signs up / signs in, after SubSovereign.configure():
await SubSovereign.recordAttributionTouch();
El SDK no almacena nada en el dispositivo del visitante hasta que llames a ssCaptureLanding(). Importar el paquete no escribe nada. El almacenamiento temporal (referente + etiquetas UTM) es un dato personal según el RGPD y un almacenamiento según la Ley de ePrivacy, por lo que cuándo capturarlo es una decisión de consentimiento tuya, no nuestra — llámalo desde el controlador de aceptación de tu banner. Sin captura, recordAttributionTouch() solo reporta lo que la página de registro puede ver — etiquetas UTM en esa URL y un referente si no es tu propio sitio — por lo que una referencia de IA o búsqueda que ocurrió en una página de aterrizaje anterior no se atribuye. Ese es el coste de no capturar, y es la elección del visitante.
recordAttributionTouch() envía el primer aterrizaje almacenado (supera a la página actual), borra el almacenamiento temporal solo después de un envío exitoso para que un inicio de sesión posterior vuelva a intentarlo, y nunca lanza excepciones. Si tus páginas de aterrizaje no pueden llamar a configure() (un sitio estático de marketing sin usuario), ssFlushAttribution(userId, { apiUrl, apiKey }) realiza el mismo envío con credenciales explícitas — o establece window.SS_API_URL y window.SS_API_KEY.
El origen de tu sitio web debe estar en los ALLOWED_ORIGINS del servidor, o el navegador no reportará nada en silencio.
Privacidad (RGPD y CCPA)
await SubSovereign.recordConsent({ purpose: 'analytics', granted: true });
await SubSovereign.requestErasure(); // "forget me" — GDPR Art. 17
const myData = await SubSovereign.exportMyData(); // data export — GDPR Art. 20
await SubSovereign.setDoNotSell(true); // CCPA "Do Not Sell" / GPC signal
Las tres llamadas de privacidad lanzan excepciones ante fallos (ApiError, con .status). Un usuario que ejerce un derecho legal debe ser informado cuando no se ha procesado — captura el error y comunícalo; nunca muestres un éxito silencioso. Los registros de facturación se conservan en el servidor según el RGPD Art. 17(3)(b) y se anonimizan.
Botón de desistimiento de la UE (Pasaporte de Cumplimiento)
<subsovereign-withdrawal subscription-id="SUB_ID" appearance="auto"></subsovereign-withdrawal>
Botón etiquetado de forma destacada → confirmación única → envío idempotente → reconocimiento, según la Directiva (UE) 2023/2673. Para interfaces personalizadas: SubSovereign.withdraw(subscriptionId, opts) y SubSovereign.getWithdrawalConfig().
Apariencia — light, dark o auto (predeterminado), y nada más. El diálogo pinta su propia tarjeta y su propio texto, por lo que es legible en cualquier página anfitriona; auto sigue la configuración del navegador del visitante (prefers-color-scheme), y el atributo puede cambiarse en cualquier momento — la tarjeta se vuelve a pintar. No hay forma de pasar un color: se trata de un aviso estatutario, y el texto y los colores están fijos para que se lea igual para cada cliente de cada inquilino. Las palabras llegan desde tu servidor en el idioma del cliente (locale en configure()); con un servidor más antiguo, caen a inglés en lugar de quedar en blanco.
El elemento emite withdrawal-shown, withdrawal-submitted (detalle: el recibo) y withdrawal-error (detalle: { reason, status? }) para tus análisis. Si olvidas subscription-id, el cliente verá el error traducido y tú recibirás un error en la consola y un evento withdrawal-error. Si el elemento no puede cargar sus ajustes (clave incorrecta, ID de app incorrecto, tu servidor rechaza el origen de esta página), el cliente aún verá el botón — en inglés — y tú recibirás una console.warn que nombra la causa probable más un evento withdrawal-error con reason: 'config-unavailable' y, cuando exista, el estado HTTP (un origen rechazado o una URL incorrecta no tienen ninguno). Escucha este evento en todos los entornos: es la forma en que descubres que clientes alemanes ven inglés. Ten en cuenta que se dispara al cargar, antes de cualquier acción del cliente — no lo cuentes como una cancelación fallida. Una mala configuración nunca es silenciosa para el desarrollador ni visible para el cliente.
Referencia rápida
| Quieres… | Usa |
|---|---|
| Configurar el SDK | SubSovereign.configure(config) |
| Ver qué desbloqueó el usuario | await checkEntitlements() → { hasAccess, … } (lanza excepción ante fallo) |
| Bloqueo simple (fallo seguro) | await hasAccess('pro') → boolean |
| Cargar y mostrar el paywall | pw.config = await getPaywallConfig() + evento select-product |
| Ajustar el tamaño del paywall | --ss-paywall-max-width · transform: scale(…) |
| Conceder tras la compra | en el servidor (tu backend o el puente de facturación personalizado), luego vuelve a comprobar |
| Flags de características (fallo seguro) | await getFeatureFlags() → { [flag]: boolean } |
| Atribución GEO | ssCaptureLanding() tras el consentimiento · recordAttributionTouch() una vez identificado |
| Registrar consentimiento | await recordConsent({ purpose, granted }) |
| RGPD / CCPA (lanza excepción ante fallo) | requestErasure() · exportMyData() · setDoNotSell(bool) |
| Desistimiento de la UE | <subsovereign-withdrawal subscription-id="…" appearance="light|dark|auto"> · withdraw() |
Pasos siguientes
- Apps en React: guía de React. Profundización en la capa de datos: Web & React Native.
- ¿Nuevo en estos conceptos? Lee Cómo funciona SubSovereign.