Saltar al contenido principal

Tu primer checkout

Recorrido de inicio a fin por el happy path del SDK. Al terminar esta guía, un asistente puede ver el evento, seleccionar boletos, apartar inventario con una reservación, completar checkout (gratis o de pago con Stripe) y ver el estado terminal de la orden.

Qué vas a construir

Un solo componente embebido de React que guía al asistente por:

  1. Mostrar el nombre, fechas, ubicación y organizador del evento.
  2. Mostrar tipos de boleto disponibles con selección de cantidad (precios por wave colapsados a una fila por tipo: "From $X").
  3. Apartar inventario con una reservación emitida por el servidor (protegida por captcha).
  4. Mostrar line items de la reservación y un countdown MM:SS hasta que expire.
  5. Confirmar checkout: los eventos gratuitos terminan de inmediato; los eventos de pago montan Stripe Elements y confirman un PaymentIntent.
  6. Consultar el estado de la orden hasta que llegue a un estado terminal (completed, failed o refunded).

Integración total: un archivo, unas 50 líneas.

Prerrequisitos

  • Instalación completa: @feelrift/react instalado, peers presentes y ubicación por framework elegida.
  • Un eventId para un evento con al menos una wave configurada y un tipo de boleto. El dashboard crea estos recursos.
  • Para eventos de pago: @stripe/stripe-js y @stripe/react-stripe-js instalados (consulta Instalación -> Eventos de pago).
  • Un returnUrl expuesto por tu sitio: la URL a la que Stripe redirige después de 3DS o wallet redirect (por ejemplo, https://your.site/checkout/return).

Ejemplo completo

"use client";

import {
AvailabilityList,
CaptchaWidget,
CheckoutForm,
Countdown,
EstimateBreakdown,
EventHeader,
ReservationSummary,
ReserveButton,
RiftEvent,
RiftProvider,
useOrderStatus,
} from "@feelrift/react";

export function TicketingEmbed() {
return (
<RiftProvider
locale="es-MX"
on={{
paymentSucceeded: (orderId, orderStatusToken) => {
// Fires after a paid `confirmPayment` resolves OK, or
// immediately for free orders. Carry the `orderId` and
// short-lived `orderStatusToken` into your post-purchase
// routing — `useOrderStatus` uses the token to poll.
// See: Guides → Securing the order-status token.
router.push(
`/order/${orderId}?t=${encodeURIComponent(orderStatusToken)}`,
);
},
paymentFailed: (err) => {
console.error("Payment failed:", err);
},
}}
>
<RiftEvent eventId="evt_summerfest_2026">
<EventHeader />

<AvailabilityList />

<EstimateBreakdown />

<CaptchaWidget />

<ReserveButton requireCaptcha />

<ReservationSummary />
<Countdown />

<CheckoutForm returnUrl="https://your.site/checkout/return" />

<OrderStatusBanner />
</RiftEvent>
</RiftProvider>
);
}

function OrderStatusBanner() {
const { data, isTerminal } = useOrderStatus();
if (!data) return null;
if (isTerminal) {
return (
<p>
Order {data.orderId}: {data.status}
</p>
);
}
return <p>Finalizing order {data.orderId}</p>;
}

Ese es todo el happy path. El resto de esta página explica para qué sirve cada pieza y cómo verificar que el flujo funciona.

Qué hace cada pieza

<RiftProvider>

Raiz del runtime del SDK. Contiene URL base de API, locale, tokens de tema y callbacks de comportamiento. Colocalo una vez cerca de la parte superior de la superficie embebida. Consulta Instalación para la ubicación específica por framework.

La prop on acepta callbacks que se disparan en transiciones de estado dentro del SDK. Los dos más comunes son paymentSucceeded (tu landing posterior a la compra) y paymentFailed (hook de Sentry). Consulta la referencia de <RiftProvider> para ver la lista completa.

<RiftEvent>

Aplica scope de evento a todo lo que contiene. Es dueño de eventId e importa @stripe/stripe-js de forma lazy cuando la configuración de Ticketing del evento tiene stripe no nulo (eventos de pago). Los eventos gratuitos omiten ese import por completo.

Nota Se soportan múltiples instancias de <RiftEvent> en la misma página, por ejemplo para un selector multi-evento. Cada una aplica scope a su propio subárbol; reservaciones y estado de checkout son por evento.

<EventHeader>

Display opcional: nombre del evento, fechas, ubicación y organizador. Lee desde useConfig(). No renderiza nada hasta que carga la configuración; luego pinta sin flicker porque la config se persiste en localStorage y se hidrata desde cache en visitas posteriores.

Omitelo si ya tienes tu propio diseno de encabezado. Llama useConfig() directamente para obtener los mismos datos.

<AvailabilityList>

Renderiza una lista plana de filas de tipos de boleto con steppers de cantidad. Las waves (tiers de precio/horario) se colapsan internamente: cada fila muestra el precio activo más barato como "From $X" y el stepper limita la cantidad al total disponible y al límite por orden más permisivo entre waves activas con inventario. El desglose por wave aparece en <EstimateBreakdown> cuando eliges una cantidad.

Lee la selección desde el contexto con scope de evento que provee <RiftEvent>. <ReserveButton> lee del mismo scope, por eso ambos pueden ser hermanos en vez de estar anidados.

Para layouts personalizados:

<AvailabilityList>
{(ticketTypes) =>
ticketTypes.map((tt) => /* your custom rendering per ticket type */)
}
</AvailabilityList>

Tip La selección vive en <RiftEvent>, así que el picker y la acción de reservar pueden tener layouts independientes: botón sticky abajo, dos columnas con resumen persistente, paneles separados con contenido intermedio. Consulta <RiftEvent> - Two-panel layout example.

<EstimateBreakdown>

Desglose de precios previo a la reservación. Aparece en cuanto seleccionas una cantidad. Recorre las waves activas de cada tipo de boleto del más barato al más caro (el mismo algoritmo que usa el servicio de reservaciones) y renderiza un árbol jerárquico: por tipo de boleto -> líneas por wave -> subtotal -> total general + aviso siempre visible.

El concepto de wave no aparece en el picker: <AvailabilityList> lo colapsa. El desglose es donde vive la transparencia por wave: si un asistente elige 2 de un tipo de boleto que tiene 1 disponible a $80 en la wave early-bird y cantidad ilimitada a $100 en la wave regular, el desglose muestra por que el total es $180, no $160.

Nota El aviso es visible por defecto y cubre una expectativa regulatoria real sobre transparencia de modificadores (fees, taxes agregados en checkout). Cambia el copy con la prop disclaimer o globalmente con <RiftProvider strings={...}>. Pasa disclaimer={null} solo si muestras copy equivalente en otro lugar de la página.

Es neutral de layout: el componente no trae position ni comportamiento sticky. Ponlo en un panel inline, drawer sticky, sidebar o modal. Ese CSS pertenece a tu app, no al SDK.

Consulta el algoritmo completo y los requisitos del aviso en Precios y estimaciones.

<CaptchaWidget>

Resuelve el reto captcha contra el altchaBaseUrl configurado en el provider. El SDK adjunta la solución como header altcha-payload en la siguiente request mutante (reservaciones, checkout). Las soluciones son de un solo uso en el servidor; el widget vuelve a idle después de consumirse para requerir una nueva resolución en la siguiente llamada.

<ReserveButton requireCaptcha />

Envía POST /events/{eventId}/ticketing/reservations con los items seleccionados en <AvailabilityList>. La prop requireCaptcha deshabilita el botón hasta que la solución captcha está lista. Úsala cuando montes un <CaptchaWidget> cerca.

Si tiene éxito, el SDK guarda el token de reservación y line items en el store persistido. Si falla, el botón muestra el error del servidor en un <div> estilizado debajo: los mensajes de rate-limit incluyen retryAfterSeconds, los mensajes de sold-out usan el string error.insufficientInventory y todo lo demás cae a un mensaje genérico.

<ReservationSummary> + <Countdown>

Después de una reservación exitosa, estos dos componentes se muestran. <ReservationSummary> muestra los line items confirmados por el servidor (pueden diferir de la estimación que mostró la UI si los precios cambiaron entre carga de página y reservación; consulta <PriceChangedWarning> para mostrar la diferencia). <Countdown> renderiza un countdown MM:SS hasta expiresAt. Ambos no renderizan nada cuando no hay reservación activa, así que puedes dejarlos montados en el mismo árbol.

Nota El countdown se detiene en 00:00. Un checkout ya en vuelo puede completarse un poco después, pero la UI no debe depender de eso. Después de expirar, pide al usuario iniciar de nuevo.

<CheckoutForm returnUrl=... />

Separa según la configuración de Ticketing del evento: Ticketing deshabilitado muestra un estado no disponible, Ticketing gratuito muestra email + submit, y Ticketing de pago monta Stripe Elements. El SDK trata el evento como gratuito cuando ticketingConfig.enabled === true y ticketingConfig.stripe === null; es de pago cuando stripe está presente. Checkout gratuito devuelve éxito de inmediato y dispara on.paymentSucceeded. Checkout de pago ejecuta tres pasos:

  1. useCheckout().prepare({ email }) llama el endpoint de checkout para crear una orden y un client secret de PaymentIntent.
  2. useCheckoutPayment().capture() recolecta un PaymentMethod mediante Stripe Elements.
  3. useCheckout().confirm({ returnUrl }) cobra la tarjeta. Redirects de 3DS / wallet entregan el control a returnUrl; confirmaciones en página llaman on.paymentSucceeded directamente.

returnUrl es requerido para eventos de pago: el SDK lanza durante render si lo olvidas en un evento de pago. Los eventos gratuitos lo ignoran.

Tip returnUrl también acepta una función (response) => string. Úsala para codificar orderId y orderStatusToken en la URL para que la página después del redirect recupere estado sin leer el store persistido. Es útil para páginas de retorno cross-domain, confirmaciones en nueva pestaña o rutas SSR. Consulta <CheckoutForm> - returnUrl.

Advertencia orderStatusToken es una credencial bearer. Cuando lo transportas en la URL de retorno, la página de retorno debe quitarlo de la URL en el primer render para mantenerlo fuera del historial del navegador, el header Referer en clicks posteriores y logs de acceso del servidor. Consulta Proteger el token de estado de la orden para el patrón recomendado.

Nota clientSecret nunca se persiste. Si el usuario recarga a mitad de checkout, el SDK vuelve a crear el PaymentIntent desde el token de reservación en vez de rehidratar un secret obsoleto que Stripe pudo invalidar del lado servidor.

useOrderStatus()

Consulta GET /events/{eventId}/ticketing/orders/{orderId}/status hasta que la orden llega a un estado terminal (completed, failed o refunded). Por defecto toma eventId, orderId y orderStatusToken desde el estado del SDK; estos se pueblan automáticamente cuando <CheckoutForm> confirma. El hook se detiene automáticamente en estado terminal; el flag isTerminal indica cuando puedes navegar fuera.

Para eventos de pago con 3DS, el polling cubre la ventana entre que stripe.confirmPayment devuelve y la orden llega a su estado terminal. La confirmación y la finalización de la orden son asíncronas, así que espera siempre el estado; no asumas.

Verificar

Copia el ejemplo en tu app y recorre el flujo completo con un evento de pago:

  1. La página carga. <EventHeader> pinta los metadatos del evento. El panel de red del navegador muestra GET /api/v1/events/<id>/config, GET /api/v1/events/<id>/ticketing/config y GET /api/v1/events/<id>/ticketing/availability, todos 200.
  2. Selecciona al menos una cantidad de boleto en <AvailabilityList>. El <ReserveButton> se habilita cuando el captcha queda listo.
  3. Haz click en Verify en el widget de captcha. El widget cambia a "Verified".
  4. Haz click en el botón de reservar. El panel de red muestra POST /api/v1/events/<id>/ticketing/reservations con respuesta 200. <ReservationSummary> y <Countdown> aparecen.
  5. Ingresa un email en <CheckoutForm> y, para eventos de pago, los datos de tarjeta en <PaymentElement>. Usa la tarjeta de prueba de Stripe 4242 4242 4242 4242 con cualquier expiracion futura y cualquier CVC.
  6. Envia. El panel de red muestra POST /api/v1/events/<id>/ticketing/checkout con respuesta 200. El confirmPayment de Stripe resuelve OK.
  7. El callback paymentSucceeded se dispara con el orderId.
  8. <OrderStatusBanner> aparece y consulta GET /api/v1/events/<id>/ticketing/orders/<orderId>/status cada 2 segundos. Cambia a completed en pocos segundos y deja de consultar.

Si algún paso falla, consulta Manejo de errores para los códigos que expone el SDK y cómo manejar cada uno.

Siguientes pasos

Ya tienes un embed funcionando. Desde aqui:

Desviaciones comunes

Quieres...Usa...
Renderizar items personalizados en <AvailabilityList>Render prop con function-children; consulta <AvailabilityList>.
Reemplazar el estilo default del botón de reservarSlot asChild en <ReserveButton>: conserva el click handler y cambia el elemento. Consulta la referencia.
Construir checkout sin <CheckoutForm>useCheckout: mismo flujo de datos, sin UI.
Mostrar eventos gratuitos distinto a eventos de pagouseTicketingConfig().data discrimina: enabled === true && stripe === null es gratis; stripe !== null es de pago. Separa antes de montar <CheckoutForm>.
Manejar la reservación desde un botón personalizadouseReservation: mismo flujo que <ReserveButton>, sin UI. Devuelve { reservation, estimate, breakdown, token, createReservation, ... }.
Mostrar los segundos restantes en un widget propio<Countdown> acepta function children: <Countdown>{({ secondsRemaining }) => ...}</Countdown>.