ZeroToShip

Docs/Tutorial

// tutorial

Ricevere pagamenti (Stripe)

Stripe è già cablato: bottone di checkout lato client, sessione creata lato server, webhook che aggiorna il database quando arriva il pagamento. Tu devi solo definire i piani.

1. Definisci un piano in config.ts

I piani vivono in config.ts sotto stripe.plans. Ogni piano ha un priceId (creato su Stripe), un nome, un prezzo e una lista di feature:

config.ts
ts
stripe: {
  plans: [
    {
      priceId: process.env.NODE_ENV === "development"
        ? "price_xxxTest"
        : "price_xxxProd",
      name: "Starter",
      description: "Tutto quello che ti serve per partire",
      price: 199,
      priceAnchor: 249,
      features: [
        { name: "Repo completo" },
        { name: "1 anno di aggiornamenti" },
      ],
    },
  ],
},

Vai su dashboard.stripe.com/products, crea un prodotto, copia il price_... e incollalo qui. Un priceId per la modalità test, uno per la produzione.

2. Il bottone Checkout

<ButtonCheckout /> è già usato dentro <Pricing />. Ricapitolando: il click chiama l'API /api/stripe/create-checkout, riceve un URL di Stripe e ci redirige l'utente.

components/ButtonCheckout.tsx
tsx
const { url } = await apiClient.post("/stripe/create-checkout", {
  priceId,
  successUrl: window.location.href,
  cancelUrl: window.location.href,
  mode, // "payment" o "subscription"
});

window.location.href = url;

3. La sessione lato server

Dentro app/api/stripe/create-checkout/route.ts recuperiamo l'utente Supabase, leggiamo il suo profilo (per precompilare email e customer_id) e chiamiamo createCheckout da libs/stripe.ts:

libs/stripe.ts
ts
const stripeSession = await stripe.checkout.sessions.create({
  mode,
  line_items: [{ price: priceId, quantity: 1 }],
  client_reference_id: user?.id,
  success_url: successUrl,
  cancel_url: cancelUrl,
  allow_promotion_codes: true,
});

return stripeSession.url;

Nota il client_reference_id: passiamo lì l'ID Supabase dell'utente, così quando il webhook riceve l'evento sappiamo esattamente a chi dare accesso.

4. Il webhook

Quando Stripe conferma il pagamento manda un POST a /api/webhook/stripe. Sull'evento checkout.session.completed aggiorniamo il profilo nell'utente Supabase:

app/api/webhook/stripe/route.ts
ts
case "checkout.session.completed": {
  const session = await findCheckoutSession(stripeObject.id);
  const customerId = session?.customer;
  const priceId = session?.line_items?.data[0]?.price.id;
  const userId = stripeObject.client_reference_id;

  await supabase
    .from("profiles")
    .update({
      customer_id: customerId,
      price_id: priceId,
      has_access: true,
    })
    .eq("id", userId);

  break;
}

Gestiamo anche customer.subscription.deleted, invoice.paid e invoice.payment_failed — per gli abbonamenti ricorrenti. Dai un'occhiata al file, è tutto commentato.

5. Testare il webhook in locale

Installa la Stripe CLI, poi apri un secondo terminale mentre npm run dev gira:

terminal
bash
stripe login
stripe listen --forward-to localhost:3000/api/webhook/stripe

La CLI ti stampa un whsec_...: mettilo in .env.local come STRIPE_WEBHOOK_SECRET. Adesso ogni pagamento test triggera il webhook sul tuo localhost.