Cómo construir un checkout de una página en React y Next.js que resista rechazos y cambios de precio
Un checkout es fácil. Rechazos, 3-D Secure, precios que cambian antes de pagar y pagos que nunca responden, no. Un checkout de una página en React y Next.js.
Preet Suthar · · 6 min de lectura
- React
- Next.js
- E-commerce
- Payments
La mayoría de los tutoriales de checkout terminan en un formulario y un botón Pagar que llama a Stripe. Esa parte funciona al primer intento. Lo que hace perder pedidos es todo lo que la rodea: la tarjeta rechazada, el banco que pide una verificación 3-D Secure, la tarifa de envío que cambia con el código postal, la petición que nunca vuelve y el cliente que pulsa Pagar por segunda vez.
Así es como estructuro un checkout de una página en React y Next.js, con los estados que importan y un pequeño fragmento de código para cada uno. Funciona con shadcn/ui o con cualquier otro conjunto de componentes. Lo difícil es el flujo, no el estilo.
Una página, tres pasos que se pliegan
Un checkout de una página lo mantiene todo a la vista: no hay cargas de página entre pasos y el botón Atrás del navegador no puede tirar medio formulario. Pero un formulario largo es un muro. La solución son tres pasos en la misma página, contacto, envío y pago, con solo uno abierto a la vez.
Cuando un paso está hecho, se pliega en una fila de resumen, así que un checkout terminado se lee como un recibo:
El estado que hay detrás es pequeño. Guarda la lista de pasos terminados y deriva el que está abierto:
Dos detalles hacen que se sienta bien. Cuando alguien pulsa Cambiar, mueve el foco al encabezado del paso que se abrió, o los usuarios de teclado y de lectores de pantalla se quedan en un botón que ya no existe. Y cuando el cliente ha iniciado sesión, sáltate el contacto y empieza en el envío con su dirección predeterminada.
En pantallas anchas, el resumen del pedido va junto al formulario. En móviles se pliega en una barra "Mostrar resumen del pedido" encima del formulario que sigue mostrando el total, porque el total es el número que la gente no para de consultar.
Valida al salir del campo, resume al pulsar Pagar
Muestra el error de un campo cuando el cliente sale de él y luego actualízalo en vivo mientras escribe, para que desaparezca en cuanto esté corregido. Nunca mientras aún lo está escribiendo por primera vez.
No desactives Pagar hasta que el formulario sea válido. Un botón desactivado no puede decirle a nadie qué falta. Deja que lo pulsen y luego pon un único resumen arriba que liste todos los problemas, enlaza cada elemento a su campo y mueve el foco al resumen. El resumen de errores del GOV.UK Design System es una buena referencia para este patrón.
Las tarifas y los impuestos siguen a la dirección
Las tarifas de envío y los impuestos dependen de la dirección, así que solo pueden cargarse cuando la dirección está completa. Espera a una breve pausa al escribir, cancela la petición anterior cuando la dirección vuelva a cambiar y muestra "Calculando…" en el resumen en lugar de un número desactualizado.
Cuando lleguen tarifas nuevas, conserva la elegida si sigue disponible. Si no, elige la más barata y dilo: "El envío exprés no está disponible para esta dirección." Cambiar el método en silencio es la forma de que la gente acabe pagando un envío que no eligió.
El pago exprés va primero
Apple Pay, Google Pay, Shop Pay y PayPal sustituyen al formulario entero, así que van arriba, encima del contacto, con un separador "O paga con tarjeta" debajo. Si los pones junto al botón Pagar, el cliente ya habrá escrito su dirección cuando vea la vía rápida. Muestra solo los wallets que el navegador puede usar de verdad. El Express Checkout Element de Stripe y SDK similares lo resuelven por ti.
Pagar es una máquina de estados, no un booleano
isSubmitting no puede describir lo que pasa después de Pagar. Dale un tipo a la respuesta del servidor:
Luego gestiona cada caso a propósito.
Pagar no puede ejecutarse dos veces. Un doble clic dispara dos handlers antes de que React vuelva a renderizar, así que una comprobación disabled={pending} no basta. Protégelo con una ref y bloquea los botones Cambiar mientras se realiza el pedido, para que el carrito no cambie bajo el pago.
Cada intento lleva una clave de idempotencia. Créala con crypto.randomUUID() al pulsar Pagar y envíala con la petición de pago. Stripe y la mayoría de las API de pago la aceptan, y una petición reintentada con la misma clave no puede cobrar dos veces.
Salir a mitad del pago pide confirmación. Añade un listener de beforeunload mientras se procesa un pago y quítalo cuando termine.
Los rechazos necesitan palabras, no "Algo salió mal"
Un rechazo es el fallo más común del checkout y el que tiene los peores textos. Traduce los códigos que recibes a frases que digan a la gente qué hacer, mueve el foco al mensaje y mantén rellenos los datos de la tarjeta:
requires_action no es un error. Significa que el banco quiere 3-D Secure, algo rutinario en Europa con la autenticación reforzada de clientes. Mantén el estado de procesamiento, abre el desafío del banco en un diálogo titulado "Confirma con tu banco" y continúa cuando se resuelva.
Cuando el precio cambió desde el carrito
Los precios, el stock y los descuentos pueden cambiar entre el carrito y Pagar. Envía con el pedido el total que vio el cliente, recalcula el precio en el servidor y niégate a cobrar si no coinciden:
En el cliente, actualiza el resumen, tacha el total anterior y enumera cada cambio con palabras sencillas, como "Ring vase ahora cuesta $74, antes $68." El cliente vuelve a pulsar Pagar con el nuevo importe. Un artículo que se agotó durante el checkout funciona igual: sácalo del total, márcalo y deja que lo quiten o continúen sin él.
Cuando el pago nunca responde
A veces la petición se queda colgada. Lo peor que puedes mostrar tras un timeout es "Reintentar", porque puede que el primer cobro se haya realizado. Tras una espera razonable, di "Comprobando el estado de tu pago…" y consulta un endpoint de estado con la misma clave de idempotencia hasta saber si existe un pedido. Ofrece un nuevo intento solo cuando el servidor diga que el anterior falló.
Una checklist
- Los pasos se pliegan en filas de resumen, y Cambiar mueve el foco al paso que se reabre.
- Errores al salir del campo, un resumen enlazado al pulsar Pagar y ningún botón Pagar desactivado.
- Las tarifas y los impuestos esperan a una dirección completa, cancelan peticiones obsoletas y nunca muestran un total desactualizado.
- Wallets exprés arriba, solo los que admite el navegador.
- Una protección con ref, una clave de idempotencia y un aviso de
beforeunloadalrededor de Pagar. - Un texto específico por código de rechazo, con el formulario aún relleno.
- 3-D Secure como un paso, no como un error.
- Los precios cambiados y los artículos agotados detienen el cobro y se explican.
- Un timeout comprueba el estado en lugar de ofrecer pagar de nuevo.
Construirlo con shadcn/ui
shadcn/ui te da los inputs, los grupos de radio y los collapsibles para todo esto. El flujo de arriba es la parte que escribes tú, y es la mayor parte del trabajo.
Lo construí una vez como el bloque Checkout de HextaUI Pro, hecho con los componentes gratuitos de HextaUI. Recibe el carrito y tres callbacks, onGetRates, onEstimateTax y onPlaceOrder, y gestiona los cuatro resultados anteriores. Si onPlaceOrder no responde en 20 segundos, consulta onCheckStatus con la misma clave de idempotencia en lugar de ofrecer pagar de nuevo. Incluye ejemplos para PaymentIntents de Stripe con server actions, un traspaso al checkout de Shopify y Medusa. Usa el bloque Payment para wallets, tarjetas, rechazos y 3-D Secure, y el Address Form para el envío. Cada bloque tiene una vista previa en vivo que puedes probar antes de decidir nada.
Si tu tienda funciona con Shopify, usa el checkout alojado de Shopify en lugar de reconstruirlo. Un checkout propio tiene sentido cuando cobras tú mismo.
Preguntas frecuentes
¿Es mejor un checkout de una página que uno de varios pasos? Para la mayoría de las tiendas, una sola página con pasos que se pliegan ofrece lo mejor de ambos: todo queda a la vista y la gente solo se enfrenta a una sección cada vez.
¿Debo desactivar el botón Pagar hasta que el formulario sea válido? No. Deja que lo pulsen y muestra un resumen de lo que falta, con enlaces a cada campo.
¿Cómo evito los cobros dobles? Protege el handler con una ref, envía una clave de idempotencia con cada intento y comprueba el estado de un pago que agotó el tiempo de espera en lugar de dejar que la gente vuelva a pagar.
¿Dónde deben ir Apple Pay y Google Pay? Arriba del checkout, antes de los datos de contacto, porque sustituyen al resto del formulario.