Saltar al contenido

3D Secure se lleva al cliente fuera de su página

La autenticación manda el navegador al banco y de vuelta, y todo lo que React guardaba desaparece. Cómo dejar el estado del pago donde un redirect no lo destruya, y qué mostrar mientras tanto.

5 min de lectura

Un cliente pulsa pagar. Su banco decide que ese pago necesita autenticación. El navegador abandona su sitio hacia una página que usted no ha visto nunca, el cliente la aprueba con una app o un código, y el navegador vuelve.

En ese momento, todo el estado de React que su checkout guardaba ha desaparecido. El árbol de componentes se desmontó, los hooks se reiniciaron, la variable que decía de qué pedido se trataba no existe. No es un fallo y no hay nada que arreglar en su código: un redirect es una navegación, y las navegaciones destruyen el estado del cliente.

El checkout hay que construirlo de forma que perderlo no cueste nada.

Qué tiene que existir antes de que el cliente se vaya

Antes de que el navegador vaya a ningún sitio, el servidor ya tiene que guardar todo lo necesario para entender la vuelta. En la práctica eso significa una fila de pedido, en estado pendiente, con un identificador propio.

Ese identificador va a dos sitios. Va a los metadatos del pago en el proveedor, para que un evento que llegue más tarde encuentre el pedido sin tabla de búsqueda. Y va en la return_url como segmento de ruta:

return_url: `https://example.com/orders/${order.id}/confirm`

Una ruta y no un parámetro de consulta, porque una ruta es una página que usted controla y un parámetro es algo que el cliente puede cambiar. Esa URL la va a visitar un navegador después de dar un rodeo por un tercero, y todo lo que lleve debería ser una referencia a algo que el servidor pueda verificar en lugar de una afirmación que tenga que creer.

Nada sobre el importe, el estado o el cliente va en esa URL. Nombra un pedido; qué significa ese pedido lo decide el servidor.

La página de vuelta lee la base de datos

La ruta de confirmación es un componente de servidor: carga el pedido por el identificador de la ruta, comprueba que pertenece a quien pregunta, y muestra lo que dice la fila.

export default async function Confirm({ params }) {
  const { id } = await params
  const session = await auth()
  const order = await db.order.findUnique({ where: { id } })
 
  if (!order || order.userId !== session?.user?.id) notFound()
 
  if (order.status === 'paid')   return <Receipt order={order} />
  if (order.status === 'failed') return <Retry order={order} />
  return <Pending order={order} />
}

La comprobación de propiedad no es opcional. Los identificadores de pedido acaban en el historial del navegador, en enlaces compartidos, en tickets de soporte, y un identificador por sí solo no es autorización para ver un recibo con la dirección de alguien.

Fíjese en lo que la página no mira: los parámetros que el proveedor añadió al volver. Algunos llevan un estado y sirven para elegir qué mensaje enseñar primero, pero vinieron a través de un navegador y no son lo que decide si el pedido está pagado.

El tercer estado es el que importa

La mayoría de equipos construye dos desenlaces, éxito y fallo, y descubre el tercero en producción. El tercero es que el pago es real, el cliente está aquí, y su base de datos todavía no sabe la respuesta porque el webhook no ha llegado.

Normalmente dura unos segundos. A veces dura minutos, y es para ese caso para el que hay que diseñar la pantalla.

Consulte la fila del pedido durante un rato corto: un componente de cliente que vuelve a mirar un par de veces a lo largo de diez o quince segundos cubre casi todos los casos reales. Después pare. Una página que gira eternamente le enseña al cliente que algo se rompió, y lo siguiente que hace es pagar otra vez, lo que convierte un webhook lento en un cargo duplicado y una devolución.

Así que la pantalla de espera, cuando la consulta se rinde, debe decir con claridad que el pago se recibió y la confirmación se está cerrando, mostrar la referencia del pedido y prometer un correo. Ese texto hace trabajo de verdad: es la diferencia entre un cliente que espera y uno que paga dos veces.

El único estado que sobrevive es el que está en la URL

Detrás de esto hay un principio más amplio que va mucho más allá de los pagos, y este es su ejemplo más afilado.

Todo lo que la aplicación necesite después de una navegación que no controla tiene que vivir donde la navegación no llega: la ruta de la URL, una cookie, o la base de datos. El estado de React no cuenta. sessionStorage no cuenta, porque el cliente puede volver en otra pestaña, en otro dispositivo, desde un enlace de correo. Un componente que guarda la respuesta no cuenta, porque no estará montado.

En la práctica esto empuja el estado del checkout al servidor, que es donde App Router lo quiere de todas formas. La pregunta del caché tiene entonces una respuesta firme: estas páginas no se cachean nunca. Una confirmación de pedido es por cliente y cambia bajo sus pies; leer cookies() en la ruta la hace dinámica, y aquí eso es el comportamiento que quiere, no un coste.

El resto del checkout - quién calcula el precio, quién puede crear el pedido, dónde aterriza el webhook - es el mismo reparto de confianza, y el redirect es sencillamente el momento en que se pone a prueba.

Nuestros proyectos de comercio tratan esta pantalla como parte del trabajo de pagos y no del trabajo de diseño, porque es lo último que ve un cliente antes de decidir si el dinero salió.

Preguntas relacionadas

¿Pasa en todos los pagos?
No, y justo por eso se pasa por alto. La autenticación la exige el banco del cliente según el importe, la tarjeta, el país y su propia evaluación de riesgo, así que un desarrollador probando con una tarjeta en un país puede construir el checkout entero sin ver nunca el redirect. Después aparece en producción en una parte considerable de los pagos europeos.
¿Podemos guardar el carrito en localStorage y restaurarlo después?
Para el contenido del carrito es una comodidad razonable. Para cualquier cosa que decida el resultado no, porque vive en un solo navegador y no está cuando el mismo cliente abre el enlace de confirmación en su portátil, y porque algo que el cliente puede editar no es prueba de lo que pagó.
¿Qué debe mostrar la página de vuelta si el pago sigue procesándose?
La verdad, y una salida. Una consulta breve sobre la fila del pedido cubre el caso habitual en que el webhook llega en unos segundos; después la página debe dejar de girar y decir que la confirmación está en camino, con la referencia del pedido y un correo. Un indicador que gira indefinidamente es como un cliente decide que el pago falló y paga otra vez.
¿Esto es exclusivo de Stripe?
En absoluto. Cualquier método basado en redirección tiene la misma forma: iDEAL, Bancontact, la mayoría de métodos por transferencia, los esquemas locales del Golfo y de Turquía, y 3D Secure en tarjetas en cualquier sitio. Si el flujo manda el navegador a algún lado y espera que vuelva, todo esto aplica.

Volver a todos los artículos