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.

4 min read
Déverrouillez votre potentiel

avec React Router 7

Vous en avez marre de...

❌ perdre du temps à chercher des informations éparpillées
❌ ne pas avoir de retour sur votre progression
Assistant IA spécialisé

Posez vos questions 24/7 à notre IA experte en React Router 7

Quiz interactifs

Validez vos acquis avec des quiz personnalisés et un feedback instantané

9 modules
72 leçons
Accès à vie
299.49
-35%

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 :

  1. rediriger l’utilisateur vers un Checkout Stripe prêt à encaisser ;
  2. enregistrer l’ID de session dans la commande ;
  3. mettre à jour le statut lors du webhook checkout.session.completed.

1 Préparer la couche serveur

1.1 SDK Stripe et client singleton

server/stripe.server.ts
1
import { Stripe } from "stripe";
2
3
if (!process.env.STRIPE_SECRET_KEY) {
4
throw new Error("STRIPE_SECRET_KEY environment variable is required");
5
}
6
7
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
8
apiVersion: "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

server/stripe.server.ts {97-138}
1
export async function createStripeCheckoutSession({
2
order,
3
user,
4
guestEmail,
5
successUrl,
6
cancelUrl,
7
}: {
8
order: Awaited<ReturnType<typeof createOrder>>;
9
user?: Awaited<ReturnType<typeof getOptionalUser>>;
10
guestEmail?: string;
11
successUrl: string;
12
cancelUrl: string;
13
}) {
14
// ① transformer chaque OrderItem en line_item Stripe
15
const lineItems = await Promise.all(
16
order.items.map(async (it) => {
17
const product = await prisma.product.findUnique({
18
where: { id: it.productId! },
19
select: { stripePriceId: true },
20
});
21
if (!product?.stripePriceId) {
22
throw new Error(`Product ${it.productName} missing stripePriceId`);
23
}
24
return { price: product.stripePriceId, quantity: it.quantity };
25
}),
26
);
27
28
// ② créer la session
29
const session = await stripe.checkout.sessions.create({
30
payment_method_types: ["card"],
31
line_items: lineItems,
32
mode: "payment",
33
success_url: successUrl,
34
cancel_url: cancelUrl,
35
billing_address_collection: "required",
36
shipping_address_collection: {
37
allowed_countries: ["FR", "BE", "LU", "CH", "IT", "ES", "DE", "NL"],
38
},
39
customer: user?.stripeCustomerId ?? undefined,
40
customer_creation: user?.stripeCustomerId ? undefined : "always",
41
customer_email: user?.stripeCustomerId ? undefined : user?.email || guestEmail,
42
metadata: {
43
orderId: order.id,
44
userId: user?.id ?? "",
45
isGuest: user ? "false" : "true",
46
},
47
});
48
49
// ③ sauvegarder l’ID de session
50
await prisma.order.update({
51
where: { id: order.id },
52
data: {
53
stripeCheckoutSession: session.id,
54
paymentStatus: "PENDING",
55
},
56
});
57
58
return session;
59
}

Explications rapides

  1. On contrôle que chaque produit possède bien un stripePriceId.
  2. La clé customer_creation: "always" force Stripe à créer un compte client si l’utilisateur n’existe pas.
  3. Les métadonnées serviront au webhook pour pointer vers la bonne commande.

2 Déclencher le checkout depuis la route panier

app/routes/_public+/cart.tsx {120-145}
1
const session = await createStripeCheckoutSession({
2
order,
3
user,
4
guestEmail: submission.value.email,
5
successUrl: `${request.url.replace(/\/cart.*/,"")}/orders/${order.id}?success=true`,
6
cancelUrl: `${request.url.replace(/\/cart.*/,"")}/cart`,
7
});
8
return 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

routes/api.stripe.webhooks.ts
1
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET;
2
3
case "checkout.session.completed":
4
await handleCheckoutSessionCompleted(event.data.object);
5
break;

3.1 Mettre à jour la commande

routes/api.stripe.webhooks.ts {63-84}
1
async function handleCheckoutSessionCompleted(
2
session: Stripe.Checkout.Session,
3
) {
4
const { orderId, userId, isGuest } = session.metadata || {};
5
if (!orderId) return;
6
7
await prisma.order.update({
8
where: { id: orderId },
9
data: {
10
orderStatus: "PAID",
11
paymentStatus: "PAID",
12
stripePaymentIntentId: session.payment_intent as string,
13
billingAddress: session.customer_details
14
? {
15
name: session.customer_details.name,
16
email: session.customer_details.email,
17
address: session.customer_details.address,
18
}
19
: null,
20
},
21
});
22
23
if (session.customer) {
24
await linkStripeCustomer({
25
stripeCustomerId: session.customer as string,
26
userId: isGuest === "false" ? userId : undefined,
27
email: session.customer_details?.email ?? undefined,
28
orderId,
29
});
30
}
31
}
  • Order PAID + addresses sauvegardées.
  • linkStripeCustomer rattache le customer à l’utilisateur si nécessaire.

4 Mise à jour Prisma : stripeCustomerId

prisma/schema.prisma {38-41}
1
model User {
2
3
stripeCustomerId String? @unique
4
}

Contrôle de cohérence : un seul compte = un seul customer Stripe.


5 Environnement & CLI Stripe

1
STRIPE_SECRET_KEY=sk_test_…
2
STRIPE_WEBHOOK_SECRET=whsec_xxx

En local :

1
stripe 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

app/routes/_public+/orders.$orderId.tsx {23-33}
1
useEffect(() => {
2
if (searchParams.get("success") === "true") {
3
clearCart({ 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

  1. Dashboard > Payments : la transaction est enregistrée.
  2. Dashboard > Customers : un client est créé à la première commande, lié aux suivantes.
  3. Les produits et prix sont bien archivés / créés grâce aux helpers syncProductWithStripe() et syncStripePrice().

8 Checklist finale

  • Variables d’environnement STRIPE_SECRET_KEY et STRIPE_WEBHOOK_SECRET ajoutées.
  • Route api.stripe.webhooks.ts déployée (HTTPS obligatoire en prod).
  • createStripeCheckoutSession appelé lors du clic Commander maintenant.
  • Webhook marque la commande PAID et lie le customer.
  • Panier vidé et page /orders/:id affiche PAID.

Prochaine étape : envoyer l’e-mail de confirmation via Resend dès réception du webhook !

Premium
Quiz interactif
Testez vos connaissances et validez votre compréhension du module avec notre quiz interactif.
1

Comprendre les concepts fondamentaux

Quelle est la principale différence entre les composants client et serveur dans React ?

Les composants client s'exécutent uniquement dans le navigateur
Les composants serveur peuvent utiliser useState
Les composants client sont plus rapides
Il n'y a aucune différence significative
2

Optimisation des performances

Quelle technique est recommandée pour éviter les rendus inutiles dans React ?

Utiliser React.memo pour les composants fonctionnels
Ajouter plus d'états locaux
Éviter d'utiliser les props
Toujours utiliser les class components
3

Architecture des données

Quel hook permet de gérer les effets de bord dans un composant React ?

useEffect
useState
useMemo
useContext
4

Gestion des erreurs

Comment implémenter la gestion des erreurs pour les requêtes API dans React ?

Utiliser try/catch avec async/await
Ignorer les erreurs
Toujours afficher un message d'erreur
Rediriger l'utilisateur
5

Déploiement et CI/CD

Quelle est la meilleure pratique pour déployer une application React en production ?

Utiliser un service CI/CD comme GitHub Actions
Copier les fichiers manuellement via FTP
Envoyer le code source complet
Ne jamais mettre à jour l'application

Débloquez ce quiz et tous les autres contenus premium en achetant ce cours