Paiements
Intégration Stripe avec checkout, portail de facturation, webhooks et gestion des abonnements.
Vue d'ensemble
ScaleRocket intègre Stripe pour les paiements. Le flux utilise Stripe Checkout pour les achats, le Stripe Billing Portal pour la gestion, et une fonction côté serveur pour le traitement des webhooks.
Architecture
User -> Checkout Button -> Edge Function (stripe-checkout) -> Stripe Checkout
Stripe -> Webhook -> Edge Function (stripe-webhook) -> Update Database
User -> Manage Button -> Edge Function (stripe-portal) -> Stripe Billing PortalToute la logique Stripe s'exécute côté serveur dans les Edge Functions. Le client ne touche jamais aux clés Stripe.
User -> Checkout Button -> Convex Action (stripe-checkout) -> Stripe Checkout
Stripe -> Webhook -> Convex HTTP Endpoint (stripe-webhook) -> Update Database
User -> Manage Button -> Convex Action (stripe-portal) -> Stripe Billing PortalToute la logique Stripe s'exécute côté serveur dans les fonctions Convex. Le client ne touche jamais aux clés Stripe.
Flux de paiement
1. L'utilisateur clique sur un plan tarifaire
L'application appelle la Edge Function stripe-checkout :
const { data } = await supabase.functions.invoke("stripe-checkout", {
body: {
priceId: "price_xxx",
mode: "subscription", // or "payment" for one-time
},
});
// Redirect to Stripe Checkout
window.location.href = data.url;L'application appelle l'action Convex stripeCheckout :
import { useAction } from "convex/react";
import { api } from "../../convex/_generated/api";
const createCheckout = useAction(api.stripe.createCheckoutSession);
const { url } = await createCheckout({
priceId: "price_xxx",
mode: "subscription",
});
window.location.href = url;2. La fonction serveur crée une session Checkout
// supabase/functions/stripe-checkout/index.ts
const session = await stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: priceId, quantity: 1 }],
mode: "subscription",
success_url: `${SITE_URL}/dashboard?success=true`,
cancel_url: `${SITE_URL}/pricing`,
metadata: {
user_id: user.id,
},
});
return new Response(JSON.stringify({ url: session.url }));// convex/stripe.ts
export const createCheckoutSession = action({
args: { priceId: v.string(), mode: v.string() },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new Error("Unauthorized");
const session = await stripe.checkout.sessions.create({
customer: customerId,
line_items: [{ price: args.priceId, quantity: 1 }],
mode: "subscription",
success_url: `${SITE_URL}/dashboard?success=true`,
cancel_url: `${SITE_URL}/pricing`,
metadata: { user_id: userId },
});
return { url: session.url };
},
});3. Après le paiement, Stripe envoie un webhook
Le gestionnaire de webhook traite l'événement et met à jour la base de données.
Portail de facturation
Permettez aux utilisateurs de gérer leur abonnement (annuler, mettre à jour la carte, consulter les factures) :
const { data } = await supabase.functions.invoke("stripe-portal", {
body: {
returnUrl: `${window.location.origin}/dashboard/settings`,
},
});
window.location.href = data.url;const createPortal = useAction(api.stripe.createPortalSession);
const { url } = await createPortal({
returnUrl: `${window.location.origin}/dashboard/settings`,
});
window.location.href = url;Gestion des webhooks
La Edge Function stripe-webhook dans supabase/functions/stripe-webhook/index.ts gère tous les événements Stripe :
// Verify the webhook signature
const signature = req.headers.get("stripe-signature");
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
switch (event.type) {
case "checkout.session.completed":
// Create or update subscription record
await handleCheckoutCompleted(event.data.object);
break;
case "customer.subscription.updated":
// Update subscription status, period end
await handleSubscriptionUpdated(event.data.object);
break;
case "customer.subscription.deleted":
// Mark subscription as canceled
await handleSubscriptionDeleted(event.data.object);
break;
case "invoice.payment_failed":
// Update status to past_due, send email
await handlePaymentFailed(event.data.object);
break;
}URL du webhook : https://YOUR_PROJECT_REF.supabase.co/functions/v1/stripe-webhook
L'endpoint HTTP Convex dans convex/http.ts gère tous les événements Stripe :
// convex/http.ts
import { httpRouter } from "convex/server";
const http = httpRouter();
http.route({
path: "/stripe-webhook",
method: "POST",
handler: httpAction(async (ctx, req) => {
const signature = req.headers.get("stripe-signature");
const body = await req.text();
const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
switch (event.type) {
case "checkout.session.completed":
await ctx.runMutation(internal.stripe.handleCheckoutCompleted, { ... });
break;
case "customer.subscription.updated":
await ctx.runMutation(internal.stripe.handleSubscriptionUpdated, { ... });
break;
// ... other events
}
return new Response("ok", { status: 200 });
}),
});URL du webhook : https://YOUR_DEPLOYMENT.convex.site/stripe-webhook
Événements gérés
| Événement | Action |
|---|---|
checkout.session.completed | Créer l'abonnement, accorder les crédits |
customer.subscription.updated | Mettre à jour le plan, le statut, la période |
customer.subscription.deleted | Marquer comme annulé, révoquer l'accès |
invoice.payment_failed | Définir past_due, notifier l'utilisateur |
invoice.payment_succeeded | Réinitialiser les crédits, confirmer le renouvellement |
Cycle de vie de l'abonnement
Free User -> Checkout -> Active Subscriber
Active -> Cancel -> Canceled (access until period end)
Active -> Payment Failed -> Past Due -> Retry -> Active / Canceled
Active -> Upgrade/Downgrade -> Updated (via Billing Portal)Vérifiez le statut de l'abonnement dans l'application :
const { data: subscription } = await supabase
.from("subscriptions")
.select("*")
.eq("user_id", user.id)
.single();
const isActive = subscription?.status === "active";
const isPro = subscription?.plan_id === "pro";Configuration des prix
Tous les plans sont définis dans packages/config/src/pricing.ts :
export const plans = [
{
id: "starter",
name: "Starter",
description: "For individuals getting started",
price: { monthly: 9, yearly: 90 },
stripePriceId: {
monthly: "price_starter_monthly",
yearly: "price_starter_yearly",
},
credits: 100,
features: ["Feature A", "Feature B"],
},
{
id: "pro",
name: "Pro",
description: "For growing teams",
price: { monthly: 29, yearly: 290 },
stripePriceId: {
monthly: "price_pro_monthly",
yearly: "price_pro_yearly",
},
credits: 1000,
features: ["Everything in Starter", "Feature C", "Feature D"],
},
];Remplacez les valeurs stripePriceId par vos véritables identifiants de prix Stripe depuis le tableau de bord Stripe.
Tester les paiements
Utilisez le mode test de Stripe avec les numéros de carte de test :
- Succès :
4242 4242 4242 4242 - Refus :
4000 0000 0000 0002 - Authentification requise :
4000 0025 0000 3155
Transférez les webhooks en local :
stripe listen --forward-to http://localhost:54321/functions/v1/stripe-webhookstripe listen --forward-to https://YOUR_DEV_DEPLOYMENT.convex.site/stripe-webhookFini ? Marquez cette page comme terminée.