Pular para o conteúdo
🎯 Aula 08

💳 Stripe Gateway: Do Zero ao Webhook

Integre o gateway de pagamento mais confiável do mundo. Aprenda a processar pagamentos, gerenciar assinaturas e implementar webhooks para sincronizar dados automaticamente.

45 min
Nível: Intermediário

🎯 Por que isso é importante

Stripe processa mais de $640 bilhões anualmente e é usado por 85% das Fortune 500. Dominar esta integração significa criar sistemas de pagamento seguros, escaláveis e confiáveis que geram receita real para negócios.

🧠 Conceitos Fundamentais

Payment Intent

Objeto que representa a intenção de coletar pagamento do cliente. Controla todo o fluxo de autorização e captura do pagamento.

Webhooks

Notificações HTTP que o Stripe envia para sua aplicação quando eventos importantes acontecem (pagamento aprovado, falha, etc.).

Customer Portal

Interface pronta do Stripe onde clientes gerenciam assinaturas, métodos de pagamento e faturas sem código adicional.

PCI Compliance

Stripe Elements garante conformidade PCI automaticamente, protegendo dados sensíveis sem complexidade adicional.

🛠️ Implementação Completa

SetupVariáveis
setup

Configuração Inicial

1. Instalação das Dependências

terminal
# Instalar Stripe SDK oficial
npm install stripe @stripe/stripe-js 

2. Conta Stripe Dashboard

• Crie conta gratuita em stripe.com
• Acesse o Dashboard → Developers → API Keys
• Copie as chaves: Publishable Key e Secret Key
• Configure webhook endpoint para receber eventos

env

Variáveis de Ambiente

.env.local
# .env.local
# 🔑 Stripe Keys (obrigatórias)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...

# 🔐 Webhook Secret (para validar eventos)  
STRIPE_WEBHOOK_SECRET=whsec_...

# 🌐 URL base da aplicação
NEXT_PUBLIC_APP_URL=http://localhost:3000

# 📊 Stripe Products (IDs dos produtos criados no Dashboard)
STRIPE_PRICE_ID_BASIC=price_...
STRIPE_PRICE_ID_PRO=price_...

⚠️ Segurança Crítica

NUNCA exponha STRIPE_SECRET_KEY no cliente. Use apenas em API Routes server-side. A chave publishable é segura para uso público.

💰 Exemplo Prático: Tela de Pricing

Componente de Pricing Completo

Vamos criar uma tela de pricing profissional que se conecta diretamente ao Stripe. Este exemplo mostra como implementar planos de assinatura com checkout automático.

app/pricing/page.tsx
'use client';

import { Button } from '@/components/ui/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Check } from 'lucide-react';
import { features } from 'process';
import { useState } from 'react';

const plans = [
  {
    name: 'Básico',
    price: '$ 29,90',
    priceId: 'STRIPE_PRICE_ID_BASIC',
    description: 'Para começar',
    features: [
      'Acesso a plataforma',
      'Suporte básico',
      'Atualizações regulares',
    ],
  },
  {
    name: 'Pro',
    price: '$ 39,90',
    priceId: 'STRIPE_PRICE_ID_PRO',
    description:
      'Para usuários avançados',
    features: [
      'Acesso a plataforma',
      'Suporte básico',
      'Atualizações regulares',
    ],
    popular: true,
  },
];

export default function PricingPage() {
  const [loading, setLoading] =
    useState<string | null>(null);
  const handleSubscribe = async (
    priceId: string,
  ) => {
    setLoading(priceId);
    try {
      const response = await fetch(
        '/api/create-checkout-session',
        {
          method: 'POST',
          headers: {
            'Content-Type':
              'application/json',
          },
          body: JSON.stringify({
            priceId,
          }),
        },
      );

      const data =
        await response.json();
      if (data.url) {
        window.location.href = data.url;
      } else {
        console.error(
          'Erro ao criar sessão:',
          data.error,
        );
        alert(
          'Erro ao processar pagamento. Tente novamente.',
        );
      }
    } catch (error) {
      console.error(
        'Error during subscription:',
        error,
      );
      setLoading(null);
    }
  };
  return (
    <div className="min-h-screen bg-background">
      <div className="container mx-auto px-4 py-16">
        <div className="text-center mb-12">
          <h1 className="text-5xl font-bold mb-4">
            Escolha seu plano
          </h1>
          <p className="text-xl text-muted-foreground">
            Compare os recursos e
            escolha o plano que melhor
            atende às suas necessidades.
          </p>
        </div>
        <div className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto">
          {plans.map((plan) => (
            <Card
              key={plan.name}
              className={`relative ${
                plan.popular
                  ? 'border-2 border-primary shadow-lg'
                  : ''
              }`}
            >
              {plan.popular && (
                <div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
                  <span className="bg-primary text-foreground px-4 py-1 rounded-full text-sm font-medium">
                    Mais popular
                  </span>
                </div>
              )}
              <CardHeader className="text-center">
                <CardTitle className="text-2xl">
                  {plan.name}
                </CardTitle>
                <CardDescription>
                  {plan.description}
                </CardDescription>
                <div className="mt-4">
                  <span className="text-4xl font-bold text-muted-foreground">
                    {plan.price}
                  </span>
                  <span className="text-muted-foreground">
                    /mês
                  </span>
                </div>
              </CardHeader>
              <CardContent>
                <ul className="space-y-3">
                  {plan.features.map(
                    (feature) => (
                      <li
                        key={feature}
                        className="flex items-center"
                      >
                        <Check className="h-5 w-5 text-green-500 mr-3" />
                        <span className="text-muted-foreground">
                          {feature}
                        </span>
                      </li>
                    ),
                  )}
                </ul>
              </CardContent>
              <CardFooter>
                <Button
                  onClick={() => {
                    handleSubscribe(
                      plan.priceId,
                    );
                  }}
                  disabled={
                    loading ===
                    plan.priceId
                  }
                  className={`w-full cursor-pointer`}
                >
                  {loading ===
                  plan.priceId
                    ? 'Processando...'
                    : `Assinar ${plan.name}`}
                </Button>
              </CardFooter>
            </Card>
          ))}
        </div>
      </div>
    </div>
  );
}

🔌 API Route: Checkout Session

Criando Sessão de Checkout

Esta API route cria uma sessão segura de checkout no Stripe e redireciona o usuário para a página de pagamento oficial.

app/api/create-checkout-session/route.ts
// app/api/create-checkout-session/route.ts
import { NextResponse } from 'next/server';
import {
  auth,
  currentUser,
} from '@clerk/nextjs/server';
import Stripe from 'stripe';

const stripeSecretKey =
  process.env.STRIPE_SECRET_KEY;
if (
  !stripeSecretKey ||
  stripeSecretKey.length < 25
) {
  console.error(
    '⚠️  STRIPE_SECRET_KEY ausente ou inválida',
  );
  throw new Error(
    'Stripe não configurado. Defina STRIPE_SECRET_KEY nas variáveis de ambiente.',
  );
}

const stripe = new Stripe(
  stripeSecretKey,
  { apiVersion: '2025-07-30.basil' },
);

export async function POST(
  request: Request,
) {
  try {
    const { userId } = await auth();
    const user = await currentUser();
    if (!userId || !user) {
      return new NextResponse(
        'Unauthorized',
        { status: 401 },
      );
    }
    const { quantity = 1, priceId } =
      await request.json();
    if (!priceId) {
      return new NextResponse(
        'Price ID is required',
        { status: 400 },
      );
    }
    const session =
      await stripe.checkout.sessions.create(
        {
          line_items: [
            {
              price:
                process.env[priceId],
              quantity,
            },
          ],
          mode: 'subscription',
          success_url: `${request.headers.get(
            'origin',
          )}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
          cancel_url: `${request.headers.get(
            'origin',
          )}/pricing`,
          client_reference_id: userId,
          customer_email:
            user.primaryEmailAddress
              ?.emailAddress,
          metadata: {
            clerkUserId: userId,
            userEmail:
              user.primaryEmailAddress
                ?.emailAddress || '',
          },
        },
      );
    return NextResponse.json({
      url: session.url,
    });
  } catch (error) {
    console.error(
      'Erro ao obter usuário autenticado:',
      error,
    );
    return new NextResponse(
      'Internal error',
      { status: 500 },
    );
  }
}

🗄️ Modelo do Banco de Dados

Schema Prisma para Assinaturas

Primeiro, vamos definir o modelo de dados para armazenar as informações das assinaturas do Stripe. Este modelo será usado para sincronizar os dados entre o Stripe e nosso banco de dados.

prisma/schema.prisma
// prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Subscription {
  id                    String   @id @default(cuid())
  clerkUserId           String   @unique
  stripeCustomerId      String   @unique
  stripeSubscriptionId  String   @unique
  status                String   // active, canceled, past_due, etc.
  priceId               String
  cancelAtPeriodEnd     Boolean  @default(false)
  createdAt             DateTime @default(now())
  updatedAt             DateTime @updatedAt

  @@map("subscriptions")
}

📋 Campos do Modelo:

  • clerkUserId - ID único do usuário no Clerk
  • stripeCustomerId - ID do cliente no Stripe
  • stripeSubscriptionId - ID da assinatura no Stripe
  • status - Status da assinatura (active, canceled, etc.)
  • priceId - ID do preço/plano no Stripe
  • cancelAtPeriodEnd - Se deve cancelar no fim do período

⚡ Comandos Prisma:

npx prisma migrate dev --name add-subscription
npx prisma generate

🔧 Serviços de Assinatura

Gerenciamento de Assinaturas

Primeiro, vamos criar os serviços para gerenciar assinaturas no banco de dados. Estas funções serão utilizadas pelos webhooks para sincronizar dados.

lib/services/subscription.ts
// lib/services/subscription.ts
// lib/services/subscription.ts
import { prisma } from '@/lib/prisma';
import { Subscription as PrismaSubscription } from '@prisma/client';
import Stripe from 'stripe';

export interface CreateSubscriptionData {
  clerkUserId: string;
  stripeCustomerId: string;
  stripeSubscriptionId: string;
  status: string;
  priceId: string;
  cancelAtPeriodEnd?: boolean;
}

export async function createSubscription(
  data: CreateSubscriptionData,
): Promise<PrismaSubscription> {
  return await prisma.subscription.create(
    {
      data,
    },
  );
}

export async function updateSubscriptionByStripeId(
  stripeSubscriptionId: string,
  data: Partial<CreateSubscriptionData>,
): Promise<PrismaSubscription | null> {
  return await prisma.subscription.update(
    {
      where: {
        stripeSubscriptionId,
      },
      data,
    },
  );
}

export async function getSubscriptionByClerkId(
  clerkUserId: string,
): Promise<PrismaSubscription | null> {
  return await prisma.subscription.findUnique(
    {
      where: {
        clerkUserId,
      },
    },
  );
}

export async function getSubscriptionByStripeId(
  stripeSubscriptionId: string,
): Promise<PrismaSubscription | null> {
  return await prisma.subscription.findUnique(
    {
      where: {
        stripeSubscriptionId,
      },
    },
  );
}

export async function cancelSubscription(
  stripeSubscriptionId: string,
): Promise<PrismaSubscription | null> {
  try {
    const stripe = new Stripe(
      process.env.STRIPE_SECRET_KEY!,
      {
        apiVersion: '2025-07-30.basil',
      },
    );
    await stripe.subscriptions.cancel(
      stripeSubscriptionId,
    );
  } catch (error) {
    console.error(
      'Erro ao cancelar assinatura:',
      error,
    );
    throw error;
  }
  return await prisma.subscription.update(
    {
      where: {
        stripeSubscriptionId,
      },
      data: {
        status: 'canceled',
        cancelAtPeriodEnd: true,
      },
    },
  );
}

export async function createOrUpdateSubscriptionFromStripe(
  stripeSubscription: Stripe.Subscription,
  clerkUserId?: string,
): Promise<PrismaSubscription> {
  // Extract only the essential fields from Stripe.Subscription
  const subscriptionData = {
    stripeCustomerId:
      stripeSubscription.customer as string,
    stripeSubscriptionId:
      stripeSubscription.id,
    status: stripeSubscription.status,
    priceId:
      stripeSubscription.items.data[0]
        ?.price.id || '',
    cancelAtPeriodEnd:
      stripeSubscription.cancel_at_period_end,
  };

  // Try to find existing subscription
  const existingSubscription =
    await getSubscriptionByStripeId(
      stripeSubscription.id,
    );

  if (existingSubscription) {
    // Update existing subscription
    const updatedSubscription =
      await updateSubscriptionByStripeId(
        stripeSubscription.id,
        subscriptionData,
      );
    if (!updatedSubscription) {
      throw new Error(
        'Falha ao atualizar assinatura existente',
      );
    }
    return updatedSubscription;
  } else {
    // Create new subscription - clerkUserId is required for new subscriptions
    if (!clerkUserId) {
      throw new Error(
        'clerkUserId é obrigatório para criar nova assinatura',
      );
    }

    return await createSubscription({
      clerkUserId,
      ...subscriptionData,
    });
  }
}

📡 API para Consultar Assinatura

Endpoint para Assinatura Atual

Esta API permite que o frontend consulte o status da assinatura do usuário logado, verificando se possui uma assinatura ativa e seus detalhes.

app/api/subscriptions/current/route.ts
// app/api/subscriptions/current/route.ts
 import { NextResponse } from "next/server";
 import { auth } from "@clerk/nextjs/server";
 import { getSubscriptionByClerkId } from "@/lib/services/subscription";
 
 export async function GET() {
   try {
     const { userId } = await auth();
 
     if (!userId) {
       return new NextResponse("Unauthorized", { status: 401 });
     }
 
     const subscription = await getSubscriptionByClerkId(userId);
 
     if (!subscription) {
       return NextResponse.json({
         subscription: null,
         hasActiveSubscription: false,
       });
     }
 
     // Check if subscription is active
     const isActive = subscription.status === "active";
 
     return NextResponse.json({
       subscription: {
         id: subscription.id,
         status: subscription.status,
         priceId: subscription.priceId,
         cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
         createdAt: subscription.createdAt,
         updatedAt: subscription.updatedAt,
         customerId: subscription.stripeCustomerId,
       },
       hasActiveSubscription: isActive,
     });
   } catch (error) {
     console.error("Error fetching subscription:", error);
     return new NextResponse("Internal error", { status: 500 });
   }
 }

✅ Funcionalidades:

  • • Autenticação via Clerk
  • • Busca assinatura por ID do usuário
  • • Verifica se a assinatura está ativa
  • • Retorna dados sanitizados da assinatura
  • • Tratamento de erros completo

📋 Exemplo de Uso no Frontend:

const response = await fetch('/api/subscriptions/current'); const { subscription, hasActiveSubscription } = await response.json();

🏠 Tela de Dashboard

Dashboard do Usuário

Implementação completa de uma tela de dashboard que consome a API de assinatura e exibe informações detalhadas sobre o status da assinatura do usuário.

app/dashboard/page.tsx
'use client';

import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
  Settings,
  CheckCircle,
  Clock,
  AlertCircle,
} from 'lucide-react';
import { useSearchParams } from 'next/navigation';
import {
  Suspense,
  useEffect,
  useState,
} from 'react';

interface SubscriptionData {
  subscription: {
    id: string;
    status: string;
    priceId: string;
    cancelAtPeriodEnd: boolean;
    createdAt: string;
    updatedAt: string;
    customerId: string;
  } | null;
  hasActiveSubscription: boolean;
}

function DashboardContent() {
  const searchParams =
    useSearchParams();
  const sessionId = searchParams.get(
    'session_id',
  );
  const [
    subscriptionData,
    setSubscriptionData,
  ] = useState<SubscriptionData | null>(
    null,
  );
  const [loading, setLoading] =
    useState(true);
  const [error, setError] = useState<
    string | null
  >(null);

  useEffect(() => {
    const fetchSubscription =
      async () => {
        try {
          const response = await fetch(
            '/api/subscriptions/current',
          );
          if (!response.ok) {
            throw new Error(
              'Failed to fetch subscription',
            );
          }
          const data =
            await response.json();
          setSubscriptionData(data);
        } catch (err) {
          setError(
            'Erro ao carregar dados da assinatura',
          );
          console.error(
            'Error fetching subscription:',
            err,
          );
        } finally {
          setLoading(false);
        }
      };

    fetchSubscription();
  }, []);

  const getSubscriptionStatus = () => {
    if (!subscriptionData?.subscription)
      return {
        text: 'Sem assinatura',
        variant: 'secondary' as const,
        icon: AlertCircle,
      };

    const {
      status,
      cancelAtPeriodEnd,
    } = subscriptionData.subscription;

    if (
      status === 'active' &&
      !cancelAtPeriodEnd
    ) {
      return {
        text: 'Plano Pro',
        variant: 'default' as const,
        icon: CheckCircle,
      };
    } else if (
      status === 'active' &&
      cancelAtPeriodEnd
    ) {
      return {
        text: 'Cancelando',
        variant: 'destructive' as const,
        icon: Clock,
      };
    } else if (status === 'past_due') {
      return {
        text: 'Pagamento pendente',
        variant: 'destructive' as const,
        icon: AlertCircle,
      };
    } else {
      return {
        text: 'Inativo',
        variant: 'secondary' as const,
        icon: AlertCircle,
      };
    }
  };

  const formatDate = (
    dateString: string,
  ) => {
    return new Date(
      dateString,
    ).toLocaleDateString('pt-BR');
  };

  const statusInfo =
    getSubscriptionStatus();
  const StatusIcon = statusInfo.icon;

  if (loading) {
    return (
      <div className="min-h-screen bg-bg-2 flex items-center justify-center">
        <div className="text-center">
          <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-border mx-auto mb-4"></div>
          <p>Carregando...</p>
        </div>
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-bg-2">
      <div className="container mx-auto px-4 py-8">
        {/* Success Message */}
        {sessionId && (
          <div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg flex items-center">
            <CheckCircle className="h-5 w-5 text-green-500 mr-3" />
            <div>
              <p className="text-green-800 font-medium">
                Assinatura ativada com
                sucesso!
              </p>
              <p className="text-green-600 text-sm">
                Bem-vindo ao MinimalSaaS
              </p>
            </div>
          </div>
        )}

        {/* Welcome Section */}
        <div className="mb-8">
          <h2 className="text-3xl font-bold mb-2">
            Bem-vindo de volta!
          </h2>
          <p className="text-fg-dim">
            Aqui está o resumo da sua
            conta
          </p>
        </div>

        {/* Subscription Details */}
        {subscriptionData?.subscription && (
          <div className="bg-white rounded-lg shadow p-6 mb-8">
            <h3 className="text-xl font-semibold mb-4">
              Detalhes da Assinatura
            </h3>
            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
              <div>
                <p className="text-sm text-fg-dim">
                  Status
                </p>
                <p className="font-medium capitalize">
                  {
                    subscriptionData
                      .subscription
                      .status
                  }
                </p>
              </div>
              <div>
                <p className="text-sm text-fg-dim">
                  Cancelamento
                  automático
                </p>
                <p className="font-medium">
                  {subscriptionData
                    .subscription
                    .cancelAtPeriodEnd
                    ? 'Sim'
                    : 'Não'}
                </p>
              </div>
              <div>
                <p className="text-sm text-fg-dim">
                  Criada em
                </p>
                <p className="font-medium">
                  {formatDate(
                    subscriptionData
                      .subscription
                      .createdAt,
                  )}
                </p>
              </div>
              <div>
                <p className="text-sm text-fg-dim">
                  Última atualização
                </p>
                <p className="font-medium">
                  {formatDate(
                    subscriptionData
                      .subscription
                      .updatedAt,
                  )}
                </p>
              </div>
              <CustomerPortalButton
                customerId={
                  subscriptionData
                    ?.subscription
                    ?.customerId
                }
              />
            </div>
          </div>
        )}

        {/* No Subscription Message */}
        {!subscriptionData?.subscription &&
          !loading && (
            <div className="bg-white rounded-lg shadow p-6 text-center">
              <AlertCircle className="h-12 w-12 text-fg-muted mx-auto mb-4" />
              <h3 className="text-lg font-semibold mb-2">
                Nenhuma assinatura ativa
              </h3>
              <p className="text-fg-dim mb-4">
                Você não possui uma
                assinatura ativa no
                momento.
              </p>
              <Button asChild>
                <a href="/pricing">
                  Ver Planos
                </a>
              </Button>
            </div>
          )}

        {/* Error Message */}
        {error && (
          <div className="bg-red-50 border border-red-200 rounded-lg p-4">
            <p className="text-red-800">
              {error}
            </p>
          </div>
        )}
      </div>
    </div>
  );
}

export default function DashboardPage() {
  return (
    <Suspense
      fallback={
        <div>Carregando...</div>
      }
    >
      <DashboardContent />
    </Suspense>
  );
}
function CustomerPortalButton({
  customerId,
}: {
  customerId: string;
}) {
  const [loading, setLoading] =
    useState(false);

  const handlePortalAccess =
    async () => {
      setLoading(true);

      try {
        const response = await fetch(
          '/api/stripe/create-portal-session',
          {
            method: 'POST',
            headers: {
              'Content-Type':
                'application/json',
            },
            body: JSON.stringify({
              customerId,
            }),
          },
        );

        const { url } =
          await response.json();
        window.location.href = url;
      } catch (error) {
        console.error('Erro:', error);
      } finally {
        setLoading(false);
      }
    };

  return (
    <Button
      onClick={handlePortalAccess}
      disabled={loading}
      variant="outline"
      className="border-border"
    >
      {loading
        ? 'Carregando...'
        : '⚙️ Gerenciar Assinatura'}
    </Button>
  );
}

🎯 Funcionalidades do Dashboard:

  • Estado de Loading: Spinner durante carregamento dos dados
  • Mensagem de Sucesso: Exibe confirmação após checkout bem-sucedido
  • Status da Assinatura: Badges dinâmicos baseados no status
  • Detalhes Completos: Informações formatadas da assinatura
  • Portal do Cliente: Botão para gerenciar assinatura no Stripe
  • Estado Vazio: Mensagem quando não há assinatura ativa
  • Tratamento de Erros: Feedback visual para problemas de API

🔄 Fluxo de Estados:

1. Loading: Exibe spinner enquanto busca dados da API

2. Success: Mostra dados da assinatura se encontrada

3. Empty: Exibe call-to-action para criar assinatura

4. Error: Mostra mensagem de erro em caso de falha

📱 Recursos de UX:

  • • Design responsivo com grid adaptativo
  • • Ícones contextuais para cada status
  • • Formatação de datas em português
  • • Suspense boundary para carregamento
  • • URL params para feedback de checkout
  • 🔔 Webhook: Sincronização Automática

    Processamento de Eventos

    Webhooks garantem que sua aplicação seja notificada instantaneamente sobre mudanças no status de pagamentos, assinaturas e outros eventos importantes.

    app/api/webhooks/stripe/route.ts
    // app/api/webhooks/stripe/route.ts 
    import {
      type NextRequest,
      NextResponse,
    } from 'next/server';
    import Stripe from 'stripe';
    import {
      createOrUpdateSubscriptionFromStripe,
      cancelSubscription,
    } from '@/lib/services/subscription';
    
    const stripe = new Stripe(
      process.env.STRIPE_SECRET_KEY!,
      {
        apiVersion: '2025-07-30.basil',
      },
    );
    
    const webhookSecret =
      process.env.STRIPE_WEBHOOK_SECRET!;
    
    export async function POST(
      request: NextRequest,
    ) {
      try {
        const body = await request.text();
        const signature =
          request.headers.get(
            'stripe-signature',
          )!;
    
        let event: Stripe.Event;
    
        try {
          event =
            stripe.webhooks.constructEvent(
              body,
              signature,
              webhookSecret,
            );
        } catch (err) {
          console.error(
            'Webhook signature verification failed:',
            err,
          );
          return NextResponse.json(
            { error: 'Invalid signature' },
            { status: 400 },
          );
        }
        // Handle the event
        switch (event.type) {
          case 'checkout.session.completed':
            const session = event.data
              .object as Stripe.Checkout.Session;
            console.log(
              'Payment successful:',
              session.id,
            );
    
            try {
              const clerkUserId =
                session.client_reference_id;
    
              if (!clerkUserId) {
                console.error(
                  'No clerk user ID found in session',
                );
                break;
              }
    
              // Get the subscription from Stripe
              if (session.subscription) {
                const subscription =
                  await stripe.subscriptions.retrieve(
                    session.subscription as string,
                  );
    
                await createOrUpdateSubscriptionFromStripe(
                  subscription,
                  clerkUserId,
                );
                console.log(
                  'Subscription saved to database:',
                  subscription.id,
                );
              }
            } catch (error) {
              console.error(
                'Error handling checkout session:',
                error,
              );
            }
            break;
    
          case 'customer.subscription.created':
            const subscription = event.data
              .object as Stripe.Subscription;
            console.log(
              'Subscription created:',
              subscription.id,
            );
    
            try {
              // Try to get clerkUserId from customer metadata
              const customer =
                await stripe.customers.retrieve(
                  subscription.customer as string,
                );
              const clerkUserId = (
                customer as Stripe.Customer
              ).metadata?.clerkUserId;
    
              await createOrUpdateSubscriptionFromStripe(
                subscription,
                clerkUserId,
              );
              console.log(
                'New subscription saved to database:',
                subscription.id,
              );
            } catch (error) {
              console.error(
                'Error handling subscription creation:',
                error,
              );
            }
            break;
    
          case 'customer.subscription.updated':
            const updatedSubscription =
              event.data
                .object as Stripe.Subscription;
            console.log(
              'Subscription updated:',
              updatedSubscription.id,
            );
            const subscriptionUpdated = {
              ...updatedSubscription,
              status:
                updatedSubscription?.cancellation_details
                  ? 'canceled'
                  : updatedSubscription.status,
            };
            try {
              await createOrUpdateSubscriptionFromStripe(
                subscriptionUpdated,
              );
              console.log(
                'Subscription updated in database:',
                subscriptionUpdated.id,
              );
            } catch (error) {
              console.error(
                'Error handling subscription update:',
                error,
              );
            }
            break;
    
          case 'customer.subscription.deleted':
            const deletedSubscription =
              event.data
                .object as Stripe.Subscription;
            console.log(
              'Subscription cancelled:',
              deletedSubscription.id,
            );
    
            try {
              await cancelSubscription(
                deletedSubscription.id,
              );
              console.log(
                'Subscription cancelled in database:',
                deletedSubscription.id,
              );
            } catch (error) {
              console.error(
                'Error handling subscription cancellation:',
                error,
              );
            }
            break;
    
          case 'invoice.payment_succeeded':
            const invoice = event.data
              .object as Stripe.Invoice;
            console.log(
              'Payment succeeded:',
              invoice.id,
            );
    
            try {
              // Get subscription ID from the invoice line items
              const subscriptionId =
                invoice.lines?.data?.[0]
                  ?.subscription;
    
              if (
                subscriptionId &&
                typeof subscriptionId ===
                  'string'
              ) {
                const subscription =
                  await stripe.subscriptions.retrieve(
                    subscriptionId,
                  );
                await createOrUpdateSubscriptionFromStripe(
                  subscription,
                );
                console.log(
                  'Subscription updated after successful payment:',
                  subscription.id,
                );
              }
            } catch (error) {
              console.error(
                'Error handling successful payment:',
                error,
              );
            }
            break;
    
          case 'invoice.payment_failed':
            const failedInvoice = event.data
              .object as Stripe.Invoice;
            console.log(
              'Payment failed:',
              failedInvoice.id,
            );
    
            try {
              // Get subscription ID from the invoice line items
              const subscriptionId =
                failedInvoice.lines
                  ?.data?.[0]?.subscription;
    
              if (
                subscriptionId &&
                typeof subscriptionId ===
                  'string'
              ) {
                const subscription =
                  await stripe.subscriptions.retrieve(
                    subscriptionId,
                  );
                await createOrUpdateSubscriptionFromStripe(
                  subscription,
                );
                console.log(
                  'Subscription updated after failed payment:',
                  subscription.id,
                );
              }
            } catch (error) {
              console.error(
                'Error handling failed payment:',
                error,
              );
            }
            break;
    
          default:
            console.log(
              `Unhandled event type: ${event.type}`,
            );
        }
    
        return NextResponse.json({
          received: true,
        });
      } catch (error) {
        console.error(
          'Webhook error:',
          error,
        );
        return NextResponse.json(
          {
            error: 'Webhook handler failed',
          },
          { status: 500 },
        );
      }
    }

    👤 Customer Portal: Autoatendimento

    Portal do Cliente

    O Customer Portal do Stripe oferece uma interface completa onde clientes podem gerenciar assinaturas, métodos de pagamento e faturas sem código adicional.

    app/api/create-portal-session/route.ts
    // app/api/create-portal-session/route.ts
    // app/api/create-portal-session/route.ts
    import {
      NextRequest,
      NextResponse,
    } from 'next/server';
    import Stripe from 'stripe';
    
    const stripe = new Stripe(
      process.env.STRIPE_SECRET_KEY!,
      {
        apiVersion: '2025-07-30.basil',
      },
    );
    
    export async function POST(
      request: NextRequest,
    ) {
      try {
        const { customerId } =
          await request.json();
    
        if (!customerId) {
          return NextResponse.json(
            {
              error:
                'Customer ID é obrigatório',
            },
            { status: 400 },
          );
        }
    
        // Criar sessão do portal do cliente
        const portalSession =
          await stripe.billingPortal.sessions.create(
            {
              customer: customerId,
              return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
            },
          );
    
        return NextResponse.json({
          url: portalSession.url,
        });
      } catch (error) {
        console.error(
          'Erro ao criar portal session:',
          error,
        );
        return NextResponse.json(
          {
            error:
              'Erro interno do servidor',
          },
          { status: 500 },
        );
      }
    }

    ✅ Vantagens desta Implementação

    Segurança Máxima

    • PCI Compliance automático - Stripe cuida da certificação
    • Webhook verification - Eventos validados criptograficamente
    • 3D Secure nativo - Proteção contra fraudes

    Performance

    • API ultra-rápida - 99.99% uptime garantido
    • Checkout otimizado - Conversão 30% maior
    • Retry automático - Falhas temporárias tratadas

    Escala Global

    • 135+ moedas - Vendas internacionais
    • Métodos locais - PIX, boleto, cartões
    • Multi-idioma - Experiência localizada

    Automação Total

    • Webhooks confiáveis - Sincronização automática
    • Customer Portal - Autoatendimento completo
    • Retry inteligente - Recuperação de falhas

    🧪 Testando a Implementação

    Validação Completa

    Antes de ir para produção, é essencial testar todos os fluxos de pagamento usando as ferramentas de teste do Stripe.

    🔧 Cartões de Teste

    test-cards.txt
    // Cartões para testes
    // ✅ Sucesso
    4242 4242 4242 4242
    
    // ❌ Falha genérica
    4000 0000 0000 0002
    
    // 🔐 Requer 3D Secure
    4000 0025 0000 3155
    
    // 💳 Cartão expirado
    4000 0000 0000 0069
    
    // 🚫 Fundos insuficientes
    4000 0000 0000 9995

    📋 Checklist de Testes

    • • ✅ Checkout com cartão válido
    • • ❌ Checkout com cartão inválido
    • • 🔄 Webhook recebido e processado
    • • 👤 Customer Portal funcionando
    • • 📧 Emails de confirmação enviados
    • • 🗄️ Dados salvos no banco
    • • 🔐 Permissões atualizadas

    🚀 Próximos Passos

    Recursos Avançados

    • Stripe Connect - Marketplace com múltiplos vendedores
    • Recurring Billing - Assinaturas com múltiplos planos
    • Payment Links - Links de pagamento sem código
    • Terminal API - Pagamentos presenciais

    Integrações Complementares

    • Stripe Radar - Machine learning anti-fraude
    • Stripe Tax - Cálculo automático de impostos
    • Stripe Issuing - Cartões corporativos
    • Stripe Treasury - Contas bancárias programáveis

    🎯 Resumo da Aula

    Você aprendeu a implementar um sistema completo de pagamentos com Stripe: tela de pricing profissional, checkout seguro, webhooks para sincronização, customer portal para autoatendimento e sistema de logs para monitoramento. Esta base sólida permite escalar para milhões de transações com segurança.