Gestion des paiements avec Stripe Checkout et React Router 7
Apprends à créer une session Stripe Checkout, stocker l’ID, gérer le webhook checkout.session.completed et marquer ta commande payée dans React Router 7.
Intégrer Stripe Checkout : de la commande au paiement
Pourquoi cette étape ?
Dans la leçon précédente nous avons créé la table Order ; elle s’enregistre bien en base mais aucun paiement réel n’est déclenché.
Objectif d’aujourd’hui :
- rediriger l’utilisateur vers un Checkout Stripe prêt à encaisser ;
- enregistrer l’ID de session dans la commande ;
- mettre à jour le statut lors du webhook
checkout.session.completed.
1 Préparer la couche serveur
1.1 SDK Stripe et client singleton
1import { Stripe } from "stripe";23if (!process.env.STRIPE_SECRET_KEY) {4throw new Error("STRIPE_SECRET_KEY environment variable is required");5}67export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {8apiVersion: "2025-07-30.basil",9});
Pourquoi ?
- Une seule instance pour tout le projet (évite de recréer la connexion).
- L’API versionnée garantit qu’une mise à jour Stripe ne casse pas ton code.
1.2 Créer une session Checkout
1export async function createStripeCheckoutSession({2order,3user,4guestEmail,5successUrl,6cancelUrl,7}: {8order: Awaited<ReturnType<typeof createOrder>>;9user?: Awaited<ReturnType<typeof getOptionalUser>>;10guestEmail?: string;11successUrl: string;12cancelUrl: string;13}) {14// ① transformer chaque OrderItem en line_item Stripe15const lineItems = await Promise.all(16order.items.map(async (it) => {17const product = await prisma.product.findUnique({18where: { id: it.productId! },19select: { stripePriceId: true },20});21if (!product?.stripePriceId) {22throw new Error(`Product ${it.productName} missing stripePriceId`);23}24return { price: product.stripePriceId, quantity: it.quantity };25}),26);2728// ② créer la session29const session = await stripe.checkout.sessions.create({30payment_method_types: ["card"],31line_items: lineItems,32mode: "payment",33success_url: successUrl,34cancel_url: cancelUrl,35billing_address_collection: "required",36shipping_address_collection: {37allowed_countries: ["FR", "BE", "LU", "CH", "IT", "ES", "DE", "NL"],38},39customer: user?.stripeCustomerId ?? undefined,40customer_creation: user?.stripeCustomerId ? undefined : "always",41customer_email: user?.stripeCustomerId ? undefined : user?.email || guestEmail,42metadata: {43orderId: order.id,44userId: user?.id ?? "",45isGuest: user ? "false" : "true",46},47});4849// ③ sauvegarder l’ID de session50await prisma.order.update({51where: { id: order.id },52data: {53stripeCheckoutSession: session.id,54paymentStatus: "PENDING",55},56});5758return session;59}
Explications rapides
- On contrôle que chaque produit possède bien un
stripePriceId. - La clé
customer_creation: "always"force Stripe à créer un compte client si l’utilisateur n’existe pas. - Les métadonnées serviront au webhook pour pointer vers la bonne commande.
2 Déclencher le checkout depuis la route panier
1const session = await createStripeCheckoutSession({2order,3user,4guestEmail: submission.value.email,5successUrl: `${request.url.replace(/\/cart.*/,"")}/orders/${order.id}?success=true`,6cancelUrl: `${request.url.replace(/\/cart.*/,"")}/cart`,7});8return redirect(session.url!, { status: 303 });
redirect()303 pour respecter la spec HTTP après un POST.- On passe l’URL de succès contenant
?success=true; elle servira à vider le panier dans la page/orders/:id.
3 Écouter le webhook Stripe
1const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;2…3case "checkout.session.completed":4await handleCheckoutSessionCompleted(event.data.object);5break;
3.1 Mettre à jour la commande
1async function handleCheckoutSessionCompleted(2session: Stripe.Checkout.Session,3) {4const { orderId, userId, isGuest } = session.metadata || {};5if (!orderId) return;67await prisma.order.update({8where: { id: orderId },9data: {10orderStatus: "PAID",11paymentStatus: "PAID",12stripePaymentIntentId: session.payment_intent as string,13billingAddress: session.customer_details14? {15name: session.customer_details.name,16email: session.customer_details.email,17address: session.customer_details.address,18}19: null,20},21});2223if (session.customer) {24await linkStripeCustomer({25stripeCustomerId: session.customer as string,26userId: isGuest === "false" ? userId : undefined,27email: session.customer_details?.email ?? undefined,28orderId,29});30}31}
- Order
PAID+ addresses sauvegardées. linkStripeCustomerrattache le customer à l’utilisateur si nécessaire.
4 Mise à jour Prisma : stripeCustomerId
1model User {2…3stripeCustomerId String? @unique4}
Contrôle de cohérence : un seul compte = un seul customer Stripe.
5 Environnement & CLI Stripe
1STRIPE_SECRET_KEY=sk_test_…2STRIPE_WEBHOOK_SECRET=whsec_xxx
En local :
1stripe listen --forward-to localhost:5173/api/stripe/webhooks
Puis copie le secret dans .env.
Le tunnel Stripe ↔ localhost relaiera checkout.session.completed.
6 Comportement côté client
6.1 Vider le panier après paiement
1useEffect(() => {2if (searchParams.get("success") === "true") {3clearCart({ disableAuthenticatedClearCart: true });4}5}, [searchParams, clearCart]);
- On évite de supprimer le panier côté serveur si l’utilisateur était invité (
disableAuthenticatedClearCart).
6.2 Pré-remplir le checkout
Si l’utilisateur possède déjà stripeCustomerId, la session récupère automatiquement son adresse et son email : expérience fluide.
7 Ce que tu peux vérifier dans Stripe
- Dashboard > Payments : la transaction est enregistrée.
- Dashboard > Customers : un client est créé à la première commande, lié aux suivantes.
- Les produits et prix sont bien archivés / créés grâce aux helpers
syncProductWithStripe()etsyncStripePrice().
8 Checklist finale
- Variables d’environnement
STRIPE_SECRET_KEYetSTRIPE_WEBHOOK_SECRETajoutées. - Route
api.stripe.webhooks.tsdéployée (HTTPS obligatoire en prod). -
createStripeCheckoutSessionappelé lors du clic Commander maintenant. - Webhook marque la commande
PAIDet lie le customer. - Panier vidé et page
/orders/:idaffiche PAID.
Prochaine étape : envoyer l’e-mail de confirmation via Resend dès réception du webhook !
Comprendre les concepts fondamentaux
Quelle est la principale différence entre les composants client et serveur dans React ?
Optimisation des performances
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Architecture des données
Quel hook permet de gérer les effets de bord dans un composant React ?