Aller au contenu principalAller au pied de page
NeuraAPI
NeuraAPI
Navigation
Facturation 10 mai 2024 15 min de lecture

Intégrer Stripe Billing dans votre SaaS Next.js

Partager :

Stripe est la solution idéale pour la facturation de votre SaaS. Voici comment l'intégrer.

Prérequis

  • Compte Stripe
  • Projet Next.js
  • Base de données
  • NeuraAPI (optionnel)

Étape 1 : Setup Stripe

Installez le package Stripe et configurez vos clés API.

npm install stripe @stripe/stripe-js

Créez vos produits et prix dans le dashboard Stripe. .env.local:

STRIPE_SECRET_KEY=sk_test_xxxxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
NEXT_PUBLIC_APP_URL=http://localhost:3000

Étape 2 : Produits et prix

Configurez les plans d'abonnement avec les prix.

  • Starter — 19€/mois, 1 000 Crédits
  • Pro — 49€/mois, 5 000 Crédits
  • Entreprise — 99€/mois, 20 000 Crédits

Définissez les fonctionnalités de chaque plan. price_id Configurez les cycles de facturation.

Étape 3 : Webhooks

Les webhooks synchronisent les données entre Stripe et votre base.

// app/api/stripe/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export const PLANS = {
  starter: {
    name: 'Starter',
    priceId: 'price_starter_monthly',
    credits: 1000,
  },
  pro: {
    name: 'Pro',
    priceId: 'price_pro_monthly',
    credits: 5000,
  },
  enterprise: {
    name: 'Entreprise',
    priceId: 'price_enterprise_monthly',
    credits: 20000,
  },
} as const

export async function POST(req: NextRequest) {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json(
      { error: 'Non autorisé' },
      { status: 401 }
    )
  }

  const { plan } = await req.json()
  const selectedPlan = PLANS[plan as keyof typeof PLANS]

  if (!selectedPlan) {
    return NextResponse.json(
      { error: 'Plan invalide' },
      { status: 400 }
    )
  }

  const checkoutSession = await stripe.checkout.sessions.create({
    mode: 'subscription',
    payment_method_types: ['card'],
    customer_email: session.user.email!,
    line_items: [
      {
        price: selectedPlan.priceId,
        quantity: 1,
      },
    ],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
    metadata: {
      userId: session.user.id,
      plan: plan,
    },
  })

  return NextResponse.json({ url: checkoutSession.url })
}

Configurez les événements à écouter. metadata Implémentez les handlers pour chaque événement.

Étape 4 : Checkout

Le Checkout Stripe gère le paiement de manière sécurisée.

// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server'
import Stripe from 'stripe'
import { prisma } from '@/lib/db'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(req: NextRequest) {
  const body = await req.text()
  const sig = req.headers.get('stripe-signature')!

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(
      body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    )
  } catch (err) {
    console.error('Webhook signature verification failed:', err)
    return NextResponse.json(
      { error: 'Signature invalide' },
      { status: 400 }
    )
  }

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object as Stripe.Checkout.Session
      const userId = session.metadata?.userId
      const plan = session.metadata?.plan

      if (userId && plan) {
        await prisma.user.update({
          where: { id: userId },
          data: {
            plan,
            stripeCustomerId: session.customer as string,
          },
        })

        await prisma.subscription.create({
          data: {
            userId,
            stripeSubscriptionId: session.subscription as string,
            status: 'active',
            plan,
            currentPeriodEnd: new Date(
              (session.subscription as { current_period_end: number }).current_period_end * 1000
            ),
          },
        })
      }
      break
    }

    case 'customer.subscription.updated': {
      const subscription = event.data.object as Stripe.Subscription
      const status = subscription.status

      await prisma.subscription.updateMany({
        where: {
          stripeSubscriptionId: subscription.id,
        },
        data: {
          status,
          currentPeriodEnd: new Date(
            subscription.current_period_end * 1000
          ),
        },
      })
      break
    }

    case 'customer.subscription.deleted': {
      const subscription = event.data.object as Stripe.Subscription

      await prisma.subscription.updateMany({
        where: {
          stripeSubscriptionId: subscription.id,
        },
        data: { status: 'canceled' },
      })

      // Downgrade l'utilisateur au plan gratuit
      const sub = await prisma.subscription.findUnique({
        where: { stripeSubscriptionId: subscription.id },
      })
      if (sub) {
        await prisma.user.update({
          where: { id: sub.userId },
          data: { plan: 'free' },
        })
      }
      break
    }

    case 'invoice.payment_failed': {
      const invoice = event.data.object as Stripe.Invoice
      // Envoyer un email d'alerte à l'utilisateur
      console.error('Paiement échoué pour:', invoice.customer)
      break
    }
  }

  return NextResponse.json({ received: true })
}

Redirigez vers la page de checkout Stripe.

Étape 5 : Portal client

Le Portal client permet aux utilisateurs de gérer leur abonnement.

# Installer Stripe CLI
npm i -g stripe

# Se connecter à votre compte Stripe
stripe login

# Forwarder les webhooks vers votre serveur local
stripe listen --forward-to localhost:3000/api/stripe/webhook

# Dans un autre terminal, déclencher des événements de test
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted

Les utilisateurs peuvent changer de plan, ajouter une carte ou annuler. webhook signing secret Gérez les annulations et les remboursements. STRIPE_WEBHOOK_SECRET Testez le flux complet en mode test.

Étape 6 : Gestion des abonnements

Gérez les abonnements actifs, en pause et annulés.

// app/api/stripe/portal/route.ts
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import Stripe from 'stripe'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST() {
  const session = await auth()
  if (!session?.user) {
    return NextResponse.json(
      { error: 'Non autorisé' },
      { status: 401 }
    )
  }

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: session.user.stripeCustomerId!,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  })

  return NextResponse.json({ url: portalSession.url })
}

Synchronisez les données avec votre base.

Étape 7 : Production

Passez en mode production et testez avec de vrais paiements.

// components/SubscriptionStatus.tsx
'use client'

import { useState } from 'react'

interface Props {
  plan: string
  status: string
  currentPeriodEnd: string
}

export function SubscriptionStatus({ plan, status, currentPeriodEnd }: Props) {
  const [loading, setLoading] = useState(false)

  const handleManage = async () => {
    setLoading(true)
    const res = await fetch('/api/stripe/portal', { method: 'POST' })
    const data = await res.json()
    if (data.url) {
      window.location.href = data.url
    }
    setLoading(false)
  }

  return (
    <div className="rounded-xl border border-indigo-800/50 bg-indigo-900/30 p-6">
      <h3 className="text-lg font-semibold text-white">Abonnement</h3>
      <div className="mt-4 space-y-2">
        <p className="text-indigo-200">
          Plan : <span className="font-semibold text-white capitalize">{plan}</span>
        </p>
        <p className="text-indigo-200">
          Statut : <span className={status === 'active' ? 'text-green-400' : 'text-red-400'}>{status === 'active' ? 'Actif' : 'Inactif'}</span>
        </p>
        <p className="text-sm text-indigo-400">
          Renouvellement le {new Date(currentPeriodEnd).toLocaleDateString('fr-FR')}
        </p>
      </div>
      <button
        onClick={handleManage}
        disabled={loading}
        className="mt-6 rounded-lg border border-indigo-500 px-4 py-2 text-sm font-semibold text-indigo-200 hover:bg-indigo-900/50 transition-all disabled:opacity-50"
      >
        {loading ? 'Chargement...' : 'Gérer l'abonnement'}
      </button>
    </div>
  )
}

Bonnes pratiques

Configurez les webhooks correctement

Les webhooks sont essentiels pour synchroniser les données entre Stripe et votre base. constructEvent() Écoutez les événements importants : checkout.session.completed, customer.subscription.updated.

Utilisez le mode test

Testez tous les flux de paiement avant de passer en production. updateMany Stripe fournit des numéros de carte de test pour valider votre intégration.

Gérez les erreurs de paiement

Prévoyez des messages d'erreur clairs pour vos utilisateurs.

Documentez votre intégration

Créez une documentation interne pour faciliter la maintenance.

Récapitulatif

1Étape 1Installation et configuration
2Étape 2Produits et prix
3Étape 3Webhooks
4Étape 4Checkout
5Étape 5Portal client
6Étape 6Gestion abonnements
7Étape 7Production

Configurer Stripe

Commencez à facturer vos utilisateurs dès maintenant. NeuraSaaS Créez votre compte et configurez Stripe en quelques minutes.

Voir les tarifs