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.
avec React Router 7
Posez vos questions 24/7 à notre IA experte en React Router 7
Validez vos acquis avec des quiz personnalisés et un feedback instantané
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 :
checkout.session.completed
.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 ?
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
stripePriceId
.customer_creation: "always"
force Stripe à créer un compte client si l’utilisateur n’existe pas.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.?success=true
; elle servira à vider le panier dans la page /orders/:id
.1const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;2…3case "checkout.session.completed":4await handleCheckoutSessionCompleted(event.data.object);5break;
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}
PAID
+ addresses sauvegardées.linkStripeCustomer
rattache le customer à l’utilisateur si nécessaire.stripeCustomerId
1model User {2…3stripeCustomerId String? @unique4}
Contrôle de cohérence : un seul compte = un seul customer 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
.
1useEffect(() => {2if (searchParams.get("success") === "true") {3clearCart({ disableAuthenticatedClearCart: true });4}5}, [searchParams, clearCart]);
disableAuthenticatedClearCart
).Si l’utilisateur possède déjà stripeCustomerId
, la session récupère automatiquement son adresse et son email : expérience fluide.
syncProductWithStripe()
et syncStripePrice()
.STRIPE_SECRET_KEY
et STRIPE_WEBHOOK_SECRET
ajoutées.api.stripe.webhooks.ts
déployée (HTTPS obligatoire en prod).createStripeCheckoutSession
appelé lors du clic Commander maintenant.PAID
et lie le customer./orders/:id
affiche PAID.Prochaine étape : envoyer l’e-mail de confirmation via Resend dès réception du webhook !
Quelle est la principale différence entre les composants client et serveur dans React ?
Quelle technique est recommandée pour éviter les rendus inutiles dans React ?
Quel hook permet de gérer les effets de bord dans un composant React ?
Comment implémenter la gestion des erreurs pour les requêtes API dans React ?
Quelle est la meilleure pratique pour déployer une application React en production ?