Comment construire en React et Next.js un checkout en une page qui résiste aux refus et aux changements de prix
Un formulaire de checkout, c’est facile. Les refus, le 3-D Secure, les prix qui changent avant le paiement et les paiements qui ne répondent jamais, beaucoup moins. Un checkout en une page en React et Next.js.
Preet Suthar · · 6 min de lecture
- React
- Next.js
- E-commerce
- Payments
La plupart des tutoriels de checkout s’arrêtent à un formulaire et à un bouton Payer qui appelle Stripe. Cette partie marche du premier coup. Ce qui fait perdre des commandes, c’est tout ce qu’il y a autour : la carte refusée, la banque qui exige une vérification 3-D Secure, le tarif de livraison qui change avec le code postal, la requête qui ne revient jamais et l’acheteur qui appuie une deuxième fois sur Payer.
Voici comment je structure un checkout en une page en React et Next.js, avec les états qui comptent et un petit bout de code pour chacun. Ça fonctionne avec shadcn/ui ou n’importe quel autre ensemble de composants. La difficulté, c’est le parcours, pas le style.
Une page, trois étapes qui se replient
Un checkout en une page garde tout visible : aucun chargement de page entre les étapes, et le bouton Retour du navigateur ne peut pas jeter la moitié d’un formulaire. Mais un long formulaire unique est un mur. La solution : trois étapes sur la même page, coordonnées, livraison et paiement, dont une seule est ouverte à la fois.
Quand une étape est terminée, elle se replie en une ligne récapitulative, si bien qu’un checkout terminé se lit comme un reçu :
L’état derrière tout ça est réduit. Gardez la liste des étapes terminées et déduisez-en celle qui est ouverte :
Deux détails font que ça sonne juste. Quand quelqu’un appuie sur Modifier, déplacez le focus sur le titre de l’étape qui s’est ouverte, sinon les utilisateurs du clavier et des lecteurs d’écran restent sur un bouton qui n’existe plus. Et quand l’acheteur est connecté, sautez les coordonnées et commencez à la livraison avec son adresse par défaut.
Sur les grands écrans, le récapitulatif de commande se trouve à côté du formulaire. Sur mobile, il se replie dans une barre « Afficher le récapitulatif » au-dessus du formulaire qui montre toujours le total, parce que le total est le seul chiffre que les gens vérifient sans cesse.
Valider au blur, récapituler au paiement
Affichez l’erreur d’un champ quand l’acheteur le quitte, puis mettez-la à jour en direct pendant qu’il tape, pour qu’elle disparaisse dès que c’est corrigé. Jamais pendant la première saisie.
Ne désactivez pas Payer tant que le formulaire n’est pas valide. Un bouton désactivé ne peut dire à personne ce qui manque. Laissez les gens appuyer, puis placez en haut un récapitulatif unique qui liste chaque problème, reliez chaque élément à son champ et déplacez le focus sur le récapitulatif. Le récapitulatif d’erreurs du GOV.UK Design System est une bonne référence pour ce pattern.
Les tarifs et les taxes suivent l’adresse
Les tarifs de livraison et les taxes dépendent de l’adresse, ils ne peuvent donc se charger qu’une fois l’adresse complète. Attendez une courte pause dans la saisie, annulez la requête précédente quand l’adresse change à nouveau, et affichez « Calcul en cours… » dans le récapitulatif au lieu d’un chiffre périmé.
Quand de nouveaux tarifs arrivent, gardez le tarif choisi s’il est toujours proposé. Sinon, choisissez le moins cher et dites-le : « L’option Express n’est pas disponible pour cette adresse. » Changer de mode en silence, c’est la meilleure façon de faire payer aux gens une livraison qu’ils n’ont pas choisie.
Le paiement express d’abord
Apple Pay, Google Pay, Shop Pay et PayPal remplacent tout le formulaire, donc leur place est en haut, au-dessus des coordonnées, avec un séparateur « Ou payer par carte » en dessous. Les mettre à côté du bouton Payer, c’est garantir que l’acheteur a déjà saisi son adresse quand il découvre le moyen le plus rapide. N’affichez que les wallets que le navigateur peut réellement utiliser. L’Express Checkout Element de Stripe et les SDK similaires s’en chargent pour vous.
Payer est une machine à états, pas un booléen
isSubmitting ne peut pas décrire ce qui se passe après Payer. Donnez plutôt un type à la réponse du serveur :
Ensuite, traitez chaque cas délibérément.
Payer ne peut pas s’exécuter deux fois. Un double clic déclenche deux handlers avant que React ne refasse le rendu, donc une vérification disabled={pending} ne suffit pas. Protégez avec une ref, et verrouillez les boutons Modifier pendant qu’une commande est passée pour que le panier ne bouge pas sous le paiement.
Chaque tentative reçoit une clé d’idempotence. Créez-la avec crypto.randomUUID() quand on appuie sur Payer et envoyez-la avec la requête de paiement. Stripe et la plupart des API de paiement en acceptent une, et une requête relancée avec la même clé ne peut pas débiter deux fois.
Quitter en plein paiement demande d’abord confirmation. Ajoutez un listener beforeunload pendant qu’un paiement est en cours et retirez-le quand il est réglé.
Les refus ont besoin de mots, pas de « Une erreur est survenue »
Un refus est l’échec le plus courant du checkout, et celui dont les messages sont les pires. Associez les codes que vous recevez à des phrases qui disent aux gens quoi faire, déplacez le focus sur le message et gardez les données de carte remplies :
requires_action n’est pas une erreur. Cela signifie que la banque veut un 3-D Secure, ce qui est courant en Europe avec l’authentification forte du client. Conservez l’état de traitement, ouvrez la vérification de la banque dans une boîte de dialogue intitulée « Confirmer auprès de votre banque » et reprenez quand elle est résolue.
Quand le prix a changé depuis le panier
Les prix, le stock et les remises peuvent changer entre le panier et le paiement. Envoyez avec la commande le total que l’acheteur a vu, recalculez le prix sur le serveur et refusez de débiter s’ils diffèrent :
Côté client, mettez à jour le récapitulatif, barrez l’ancien total et listez chaque changement en mots simples, par exemple « Ring Vase coûte désormais $74, au lieu de $68. » L’acheteur appuie à nouveau sur Payer avec le nouveau montant. Un article épuisé pendant le checkout fonctionne de la même façon : retirez-le du total, signalez-le et laissez l’acheteur le supprimer ou continuer sans.
Quand le paiement ne répond jamais
Parfois, la requête reste bloquée. Le pire message à afficher après un délai dépassé est « Réessayer », parce que le premier débit est peut-être passé. Après une attente raisonnable, affichez « Vérification du statut de votre paiement… » et interrogez un endpoint de statut avec la même clé d’idempotence jusqu’à savoir si une commande existe. Ne proposez une nouvelle tentative que lorsque le serveur indique que l’ancienne a échoué.
Une checklist
- Les étapes se replient en lignes récapitulatives, et Modifier déplace le focus sur l’étape rouverte.
- Erreurs au blur, un récapitulatif avec liens au clic sur Payer, pas de bouton Payer désactivé.
- Les tarifs et les taxes attendent une adresse complète, annulent les requêtes périmées et n’affichent jamais un total périmé.
- Les wallets express en haut, uniquement ceux que le navigateur prend en charge.
- Une garde par ref, une clé d’idempotence et une invite
beforeunloadautour de Payer. - Un message précis pour chaque code de refus, avec le formulaire toujours rempli.
- Le 3-D Secure comme une étape, pas comme une erreur.
- Les prix modifiés et les articles épuisés bloquent le débit et s’expliquent d’eux-mêmes.
- Un délai dépassé vérifie le statut au lieu de proposer de payer à nouveau.
Le construire avec shadcn/ui
shadcn/ui vous fournit les champs, les groupes de boutons radio et les collapsibles pour tout cela. Le parcours ci-dessus est la partie que vous écrivez vous-même, et c’est l’essentiel du travail.
Je l’ai construit une fois pour toutes sous la forme du bloc Checkout de HextaUI Pro, à partir des composants gratuits de HextaUI. Il prend le panier et trois callbacks, onGetRates, onEstimateTax et onPlaceOrder, et gère les quatre résultats ci-dessus. Si onPlaceOrder ne répond pas en 20 secondes, il interroge onCheckStatus avec la même clé d’idempotence au lieu de proposer de payer à nouveau. Il est fourni avec des exemples pour les PaymentIntents de Stripe avec des server actions, un passage au checkout Shopify et Medusa. Il utilise le bloc Payment pour les wallets, les cartes, les refus et le 3-D Secure, et l’Address Form pour la livraison. Chaque bloc a un aperçu interactif que vous pouvez parcourir avant de décider quoi que ce soit.
Si votre boutique tourne sur Shopify, utilisez le checkout hébergé de Shopify plutôt que de le reconstruire. Un checkout sur mesure a du sens quand vous encaissez vous-même les paiements.
FAQ
Un checkout en une page est-il meilleur qu’un checkout en plusieurs étapes ? Pour la plupart des boutiques, une seule page avec des étapes qui se replient offre le meilleur des deux : tout reste visible, et les gens n’ont qu’une section à la fois devant eux.
Faut-il désactiver le bouton Payer tant que le formulaire n’est pas valide ? Non. Laissez les gens appuyer dessus et affichez un récapitulatif de ce qui manque, avec des liens vers chaque champ.
Comment éviter les doubles débits ? Protégez le handler avec une ref, envoyez une clé d’idempotence à chaque tentative et vérifiez le statut d’un paiement qui a expiré au lieu de laisser les gens payer à nouveau.
Où placer Apple Pay et Google Pay ? En haut du checkout, avant les coordonnées, parce qu’ils remplacent le reste du formulaire.