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:
- Mostrar el nombre, fechas, ubicación y organizador del evento.
- Mostrar tipos de boleto disponibles con selección de cantidad (precios por wave colapsados a una fila por tipo: "From $X").
- Apartar inventario con una reservación emitida por el servidor (protegida por captcha).
- Mostrar line items de la reservación y un countdown MM:SS hasta que expire.
- Confirmar checkout: los eventos gratuitos terminan de inmediato; los eventos
de pago montan Stripe Elements y confirman un
PaymentIntent. - Consultar el estado de la orden hasta que llegue a un estado terminal
(
completed,failedorefunded).
Integración total: un archivo, unas 50 líneas.
Prerrequisitos
- Instalación completa:
@feelrift/reactinstalado, peers presentes y ubicación por framework elegida. - Un
eventIdpara un evento con al menos una wave configurada y un tipo de boleto. El dashboard crea estos recursos. - Para eventos de pago:
@stripe/stripe-jsy@stripe/react-stripe-jsinstalados (consulta Instalación -> Eventos de pago). - Un
returnUrlexpuesto 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
disclaimero globalmente con<RiftProvider strings={...}>. Pasadisclaimer={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:
useCheckout().prepare({ email })llama el endpoint de checkout para crear una orden y un client secret de PaymentIntent.useCheckoutPayment().capture()recolecta un PaymentMethod mediante Stripe Elements.useCheckout().confirm({ returnUrl })cobra la tarjeta. Redirects de 3DS / wallet entregan el control areturnUrl; confirmaciones en página llamanon.paymentSucceededdirectamente.
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
returnUrltambién acepta una función(response) => string. Úsala para codificarorderIdyorderStatusTokenen 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
orderStatusTokenes 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 headerRefereren clicks posteriores y logs de acceso del servidor. Consulta Proteger el token de estado de la orden para el patrón recomendado.
Nota
clientSecretnunca 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:
- La página carga.
<EventHeader>pinta los metadatos del evento. El panel de red del navegador muestraGET /api/v1/events/<id>/config,GET /api/v1/events/<id>/ticketing/configyGET /api/v1/events/<id>/ticketing/availability, todos200. - Selecciona al menos una cantidad de boleto en
<AvailabilityList>. El<ReserveButton>se habilita cuando el captcha queda listo. - Haz click en Verify en el widget de captcha. El widget cambia a "Verified".
- Haz click en el botón de reservar. El panel de red muestra
POST /api/v1/events/<id>/ticketing/reservationscon respuesta200.<ReservationSummary>y<Countdown>aparecen. - Ingresa un email en
<CheckoutForm>y, para eventos de pago, los datos de tarjeta en<PaymentElement>. Usa la tarjeta de prueba de Stripe4242 4242 4242 4242con cualquier expiracion futura y cualquier CVC. - Envia. El panel de red muestra
POST /api/v1/events/<id>/ticketing/checkoutcon respuesta200. ElconfirmPaymentde Stripe resuelve OK. - El callback
paymentSucceededse dispara con elorderId. <OrderStatusBanner>aparece y consultaGET /api/v1/events/<id>/ticketing/orders/<orderId>/statuscada 2 segundos. Cambia acompleteden 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:
- Manejo de errores - cada código de error del servidor y cómo manejarlo.
- Renderizado del lado del servidor - las garantias que trae el SDK y lo que los consumidores necesitan hacer para conservarlas.
- Referencia de componentes - cada prop, callback y ejemplo.
Desviaciones comunes
| Quieres... | Usa... |
|---|---|
Renderizar items personalizados en <AvailabilityList> | Render prop con function-children; consulta <AvailabilityList>. |
| Reemplazar el estilo default del botón de reservar | Slot 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 pago | useTicketingConfig().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 personalizado | useReservation: 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>. |