ScaleRocket/Web

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 Portal

Toute 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 Portal

Toute 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énementAction
checkout.session.completedCréer l'abonnement, accorder les crédits
customer.subscription.updatedMettre à jour le plan, le statut, la période
customer.subscription.deletedMarquer comme annulé, révoquer l'accès
invoice.payment_failedDéfinir past_due, notifier l'utilisateur
invoice.payment_succeededRé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-webhook
stripe listen --forward-to https://YOUR_DEV_DEPLOYMENT.convex.site/stripe-webhook

Fini ? Marquez cette page comme terminée.

On this page