@rutiva/widget
v0.1.3
Published
Widget oficial de Rutiva para integrar pagos C2P en Venezuela. Stripe-style, zero deps, shadow DOM.
Maintainers
Readme
@rutiva/widget
Widget oficial de Rutiva para aceptar pagos C2P en Venezuela. Estilo Stripe.js — un
<script>, una línea de JS, UI lista. Zero dependencias. Shadow DOM. ~6 KB gzipped.
¿Qué es esto?
@rutiva/widget es el SDK de navegador de Rutiva. Embeber pagos C2P en tu sitio sin construir UI propia, sin manejar tokens sensibles en el cliente, sin redirigir al usuario fuera de tu página.
Cómo funciona (igual que Stripe):
┌──────────┐ 1. POST /v1/payments ┌─────────────┐
│ Browser │ ──────────────────────────▶ │ Tu backend │
│ (cliente)│ │ (sk_) │
│ │ ◀───── client_secret ─────── │ │
│ │ 2. mount widget └─────────────┘
│ │
│ widget │ 3. cliente elige banco + OTP
│ │ ──────────────────────────▶ ┌─────────────┐
│ │ │ Rutiva API │
│ │ ◀──── succeeded / failed ─── │ │
└──────────┘ └─────────────┘Tu backend crea el payment_intent con tu sk_ (server-side). El widget recibe solo el client_secret y confirma el pago. La sk_ jamás toca el navegador.
Instalación
Vía CDN (más rápido)
<script src="https://cdn.jsdelivr.net/npm/@rutiva/[email protected]/dist/rutiva.iife.js"></script>Vía npm / pnpm / yarn
npm install @rutiva/widget
# o
pnpm add @rutiva/widget
# o
yarn add @rutiva/widgetQuick start (30 segundos)
HTML vanilla
<!doctype html>
<html>
<body>
<div id="rutiva-pago"></div>
<script src="https://cdn.jsdelivr.net/npm/@rutiva/[email protected]/dist/rutiva.iife.js"></script>
<script>
const rutiva = Rutiva.Rutiva({ publishableKey: "pk_test_xxx" });
const widget = rutiva.elements({
clientSecret: "pi_abc_secret_xxx", // viene de tu backend
appearance: { theme: "auto", primaryColor: "#1A56DB" },
});
widget.mount("#rutiva-pago");
widget.on("success", (intent) => {
console.log("Pago aprobado:", intent.bank_reference);
});
widget.on("error", (err) => console.error(err));
</script>
</body>
</html>React / Vue / Svelte / TypeScript
import { Rutiva } from "@rutiva/widget";
const rutiva = Rutiva({ publishableKey: "pk_test_xxx" });
const widget = rutiva.elements({ clientSecret });
widget.mount("#rutiva-pago");
widget.on("success", (intent) => navigate("/gracias?ref=" + intent.bank_reference));Ejemplo completo React → examples/react/
Crear el client_secret en tu backend
El paso server-side. Una vez por intento de pago.
Node.js
const resp = await fetch("https://rutiva-api.onrender.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RUTIVA_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
amount: 50000, // céntimos: Bs. 500,00
currency: "VES",
customer_phone: "04141234567",
customer_id_document: "V12345678",
customer_bank_code: "0114",
}),
});
const { id, client_secret } = await resp.json();
// Enviar SOLO { id, client_secret } al navegador. Nunca sk_.Python
import httpx, os, uuid
resp = httpx.post(
"https://rutiva-api.onrender.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['RUTIVA_SECRET_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"amount": 50000,
"currency": "VES",
"customer_phone": "04141234567",
"customer_id_document": "V12345678",
"customer_bank_code": "0114",
},
)
intent = resp.json()Docs completos integración: https://rutiva-api.onrender.com/docs
API
Rutiva(options) — factory
type RutivaOptions = {
publishableKey: string; // "pk_test_..." o "pk_live_..."
apiBaseUrl?: string; // default "https://rutiva-api.onrender.com"
locale?: "es-VE" | "es" | "en"; // default "es-VE"
};Devuelve { elements, banks }.
rutiva.elements(config) — crear widget
type ElementsConfig = {
clientSecret: string;
appearance?: {
theme?: "light" | "dark" | "auto"; // "auto" sigue prefers-color-scheme
primaryColor?: string; // default "#0F172A"
borderRadius?: string; // default "8px"
fontFamily?: string; // default system font stack
};
locale?: "es-VE" | "es" | "en";
};Devuelve un RutivaWidget:
type RutivaWidget = {
mount: (target: string | HTMLElement) => void; // CSS selector o nodo
unmount: () => void; // remueve UI, conserva listeners
on: (event, handler) => () => void; // retorna off()
off: (event, handler) => void;
destroy: () => void; // unmount + clear listeners
};rutiva.banks() — listar bancos
const banks = await rutiva.banks();
// [{ code: "0114", name: "Bancaribe" }, ...]Útil si querés renderizar el dropdown de bancos en tu UI (el widget ya lo hace internamente).
Eventos
widget.on("ready", () => { /* widget montado + bancos cargados */ });
widget.on("loading", () => { /* submit en curso */ });
widget.on("success", (intent) => { /* status === "succeeded" */ });
widget.on("declined", (intent) => { /* status === "failed" */ });
widget.on("expired", (err) => { /* 15+ min desde create */ });
widget.on("error", (err) => { /* red, validación, otro */ });| Evento | Payload | Cuándo |
| ---------- | -------------------------------- | ------------------------------------------- |
| ready | — | Widget montado + bancos cargados |
| loading | — | Submit en curso |
| success | PaymentIntent | Pago aprobado |
| declined | PaymentIntent | Banco rechazó (failure_code) |
| expired | { code, message } | payment_intent expiró (>15 min) |
| error | { code, message } | Red, validación, o respuesta inesperada |
on() devuelve una función off() para desuscribir.
Theming
rutiva.elements({
clientSecret,
appearance: {
theme: "dark",
primaryColor: "#1A56DB",
borderRadius: "12px",
fontFamily: "Inter, system-ui, sans-serif",
},
});Todo va dentro de Shadow DOM. Cero CSS leak al sitio host. Tu site puede tener cualquier reset/framework — el widget se ve igual.
Modo "auto" respeta @media (prefers-color-scheme: dark).
i18n
Locales incluidos: es-VE (default), es, en.
Rutiva({ publishableKey, locale: "en" });
// o por widget
rutiva.elements({ clientSecret, locale: "en" });¿Faltan otros? Abrí un issue.
Browser support
| Navegador | Versión mínima | | -------------- | -------------- | | Chrome / Edge | últimas 2 | | Firefox | últimas 2 | | Safari | 14+ | | iOS Safari | 14+ | | IE | ❌ no soportado |
Seguridad
- CSP-friendly: corre con
Sinscript-src 'self' cdn.jsdelivr.net; connect-src 'self' rutiva-api.onrender.com;unsafe-inline, sinunsafe-eval. - SRI (Subresource Integrity) soportado:
Generá el hash con:<script src="https://cdn.jsdelivr.net/npm/@rutiva/[email protected]/dist/rutiva.iife.js" integrity="sha384-..." crossorigin="anonymous" ></script>curl -s https://cdn.jsdelivr.net/npm/@rutiva/[email protected]/dist/rutiva.iife.js \ | openssl dgst -sha384 -binary | openssl base64 -A sk_rechazado en cliente: si pasás una secret key por error, el widget lanzaRutivaError.- OTP nunca se loggea ni se filtra en eventos emitidos.
- Provenance verificado: cada release npm está firmado con sigstore (ver badge arriba).
Troubleshooting
| Síntoma | Causa probable | Solución |
| ---------------------------------------- | --------------------------------------- | ---------------------------------------------- |
| RutivaError: invalid_publishable_key | Usaste sk_ en frontend | Pasar pk_test_... o pk_live_... |
| RutivaError: invalid_client_secret | clientSecret malformado o tipeado mal | Verificar shape pi_<id>_secret_<token> |
| Evento expired | >15 min desde POST /v1/payments | Crear nuevo intent server-side |
| Widget no aparece | Selector mount inválido | Verificar #id existe o pasar HTMLElement |
| Estilos del site afectan widget | (no debería pasar) | Reportar issue — Shadow DOM debería aislar |
| status === "failed" siempre | Backend en modo test devuelve mock | Verificar credenciales sk_live_ vs sk_test_ |
Ejemplos
examples/vanilla.html— HTML puro + CDNexamples/react/— React 18 + Vite
Probar local:
pnpm dlx http-server examples/
# abrir http://localhost:8080/vanilla.htmlRoadmap
- [x]
0.1.0— flujo C2P básico (banks + OTP + confirm) - [ ]
0.2.0— theming extendido (fuentes custom, dark mode tuneable) - [ ]
0.3.0— paquete@rutiva/reactcon hookuseRutiva() - [ ]
1.0.0— API estable, semver garantizado
Contribuir
git clone https://github.com/ppsbcontest/rutiva_widget.git
cd rutiva_widget
pnpm install
pnpm test
pnpm buildVer docs/CONTRIBUTING.md + docs/ARCHITECTURE.md.
Issues y PRs bienvenidos: https://github.com/ppsbcontest/rutiva_widget/issues
Soporte
- API docs: https://rutiva-api.onrender.com/docs
- Email: [email protected]
- Issues: https://github.com/ppsbcontest/rutiva_widget/issues
License
MIT © Rutiva
