skillby ioku24

stripe-integration

Implements Stripe payments correctly the first time. Handles checkout sessions, webhooks, subscriptions, and customer management following Stripe best practices.

Installs: 0
Used in: 1 repos
Updated: 2d ago
$npx ai-builder add skill ioku24/stripe-integration

Installs to .claude/skills/stripe-integration/

# Stripe Integration Skill

Implements Stripe payments following official best practices. Covers checkout, webhooks, subscriptions, and error handling.

## When I Activate

- User mentions "Stripe", "payments", "checkout", "subscription", "billing"
- Building payment flows, upgrading plans, or handling purchases
- Setting up webhooks or handling payment events

---

## Core Principles

1. **Never trust client-side data for amounts** - Always compute prices server-side
2. **Use webhooks for fulfillment** - Don't rely on redirect success alone
3. **Test with test keys first** - Never use live keys in development
4. **Handle all webhook events** - Especially `checkout.session.completed`
5. **Store Stripe customer IDs** - Link users to their Stripe customers

---

## Implementation Checklist

### Environment Variables Required
```env
STRIPE_SECRET_KEY=sk_test_...          # Server-side only
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...  # Client-side OK
STRIPE_WEBHOOK_SECRET=whsec_...        # For webhook verification
```

### Getting STRIPE_WEBHOOK_SECRET
1. Go to https://dashboard.stripe.com/webhooks
2. Click "Add endpoint"
3. Enter your webhook URL: `https://yourdomain.com/api/webhooks/stripe`
4. Select events: `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`
5. Click "Add endpoint"
6. Click "Reveal" under "Signing secret" - this is your `STRIPE_WEBHOOK_SECRET`

For local development:
```bash
# Install Stripe CLI: brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Copy the webhook signing secret it displays
```

---

## Standard Implementation Pattern

### 1. Install Dependencies
```bash
npm install stripe @stripe/stripe-js
```

### 2. Create Stripe Client (Server)
```typescript
// src/lib/stripe.ts
import Stripe from 'stripe'

if (!process.env.STRIPE_SECRET_KEY) {
  throw new Error('STRIPE_SECRET_KEY is not set')
}

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2024-12-18.acacia',
  typescript: true,
})
```

### 3. Create Checkout Session API Route
```typescript
// src/app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import { auth } from '@clerk/nextjs/server'
import { stripe } from '@/lib/stripe'
import { getUserByClerkId } from '@/lib/supabase/queries'

const PRICES = {
  starter: process.env.STRIPE_STARTER_PRICE_ID,
  pro: process.env.STRIPE_PRO_PRICE_ID,
  business: process.env.STRIPE_BUSINESS_PRICE_ID,
}

export async function POST(req: Request) {
  try {
    const { userId } = await auth()
    if (!userId) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }

    const { priceId, planName } = await req.json()

    // Validate price ID exists
    if (!priceId || !Object.values(PRICES).includes(priceId)) {
      return NextResponse.json({ error: 'Invalid price' }, { status: 400 })
    }

    // Get user from database
    const { data: user } = await getUserByClerkId(userId)
    if (!user) {
      return NextResponse.json({ error: 'User not found' }, { status: 404 })
    }

    // Create or retrieve Stripe customer
    let stripeCustomerId = user.stripe_customer_id

    if (!stripeCustomerId) {
      const customer = await stripe.customers.create({
        email: user.email,
        metadata: { userId: user.id, clerkId: userId },
      })
      stripeCustomerId = customer.id
      // TODO: Save stripeCustomerId to user record
    }

    // Create checkout session
    const session = await stripe.checkout.sessions.create({
      customer: stripeCustomerId,
      mode: 'subscription',
      payment_method_types: ['card'],
      line_items: [{ price: priceId, quantity: 1 }],
      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?checkout=success`,
      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?checkout=cancelled`,
      metadata: {
        userId: user.id,
        planName: planName,
      },
      subscription_data: {
        metadata: { userId: user.id, planName: planName },
      },
    })

    return NextResponse.json({ url: session.url })
  } catch (error) {
    console.error('[CHECKOUT] Error:', error)
    return NextResponse.json({ error: 'Checkout failed' }, { status: 500 })
  }
}
```

### 4. Create Webhook Handler (CRITICAL)
```typescript
// src/app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'
import Stripe from 'stripe'

const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!

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

  let event: Stripe.Event

  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
  } catch (err) {
    console.error('[STRIPE_WEBHOOK] Signature verification failed:', err)
    return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
  }

  try {
    switch (event.type) {
      case 'checkout.session.completed': {
        const session = event.data.object as Stripe.Checkout.Session
        await handleCheckoutCompleted(session)
        break
      }
      case 'customer.subscription.updated': {
        const subscription = event.data.object as Stripe.Subscription
        await handleSubscriptionUpdated(subscription)
        break
      }
      case 'customer.subscription.deleted': {
        const subscription = event.data.object as Stripe.Subscription
        await handleSubscriptionDeleted(subscription)
        break
      }
      case 'invoice.payment_failed': {
        const invoice = event.data.object as Stripe.Invoice
        await handlePaymentFailed(invoice)
        break
      }
    }

    return NextResponse.json({ received: true })
  } catch (error) {
    console.error('[STRIPE_WEBHOOK] Handler error:', error)
    return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 })
  }
}

async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
  const userId = session.metadata?.userId
  const planName = session.metadata?.planName

  if (!userId || !planName) {
    console.error('[STRIPE_WEBHOOK] Missing metadata:', session.id)
    return
  }

  // Update user's plan and credits in database
  // This is where you fulfill the purchase!
  console.log(`[STRIPE_WEBHOOK] Upgrading user ${userId} to ${planName}`)

  // TODO: Call your database update function
  // await updateUserPlan(userId, planName)
}

async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
  const userId = subscription.metadata?.userId
  // Handle plan changes, status changes
  console.log(`[STRIPE_WEBHOOK] Subscription updated for user ${userId}`)
}

async function handleSubscriptionDeleted(subscription: Stripe.Subscription) {
  const userId = subscription.metadata?.userId
  // Downgrade user to free plan
  console.log(`[STRIPE_WEBHOOK] Subscription cancelled for user ${userId}`)
  // TODO: await downgradeUserToFree(userId)
}

async function handlePaymentFailed(invoice: Stripe.Invoice) {
  // Notify user of failed payment
  console.log(`[STRIPE_WEBHOOK] Payment failed for invoice ${invoice.id}`)
}
```

### 5. Create Checkout Button Component
```typescript
// src/components/CheckoutButton.tsx
'use client'

import { useState } from 'react'
import { Button } from '@/components/ui/button'

interface CheckoutButtonProps {
  priceId: string
  planName: string
  children: React.ReactNode
}

export function CheckoutButton({ priceId, planName, children }: CheckoutButtonProps) {
  const [loading, setLoading] = useState(false)

  const handleCheckout = async () => {
    setLoading(true)
    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ priceId, planName }),
      })

      const { url, error } = await response.json()

      if (error) {
        alert(error)
        return
      }

      // Redirect to Stripe Checkout
      window.location.href = url
    } catch (error) {
      console.error('Checkout error:', error)
      alert('Failed to start checkout')
    } finally {
      setLoading(false)
    }
  }

  return (
    <Button onClick={handleCheckout} disabled={loading}>
      {loading ? 'Loading...' : children}
    </Button>
  )
}
```

---

## Stripe Dashboard Setup

### Create Products and Prices
1. Go to https://dashboard.stripe.com/products
2. Click "Add product"
3. For each plan (Starter, Pro, Business):
   - Name: "RankEasy Starter" etc.
   - Pricing: Recurring, Monthly
   - Price: $29, $79, $199 etc.
4. Copy the Price ID (starts with `price_`)
5. Add to `.env.local`:
   ```
   STRIPE_STARTER_PRICE_ID=price_...
   STRIPE_PRO_PRICE_ID=price_...
   STRIPE_BUSINESS_PRICE_ID=price_...
   ```

---

## Testing Checklist

### Test Cards
- Success: `4242 4242 4242 4242`
- Declined: `4000 0000 0000 0002`
- Requires auth: `4000 0025 0000 3155`

### Test Flow
1. Click checkout button
2. Should redirect to Stripe Checkout
3. Use test card `4242 4242 4242 4242`
4. Any future expiry, any CVC
5. Complete payment
6. Should redirect back with `?checkout=success`
7. Webhook should fire and update user's plan

### Webhook Testing (Local)
```bash
# Terminal 1: Run your app
npm run dev

# Terminal 2: Forward webhooks
stripe listen --forward-to localhost:3000/api/webhooks/stripe

# Terminal 3: Trigger test event
stripe trigger checkout.session.completed
```

---

## Common Errors

| Error | Cause | Fix |
|-------|-------|-----|
| "No such price" | Invalid price ID | Check STRIPE_*_PRICE_ID env vars |
| "Invalid signature" | Wrong webhook secret | Update STRIPE_WEBHOOK_SECRET |
| Webhook not firing | Endpoint not registered | Add endpoint in Stripe Dashboard |
| Customer not found | No stripe_customer_id | Create customer first |

---

## Security Best Practices

1. **Never expose STRIPE_SECRET_KEY to client** - Only use in server components/API routes
2. **Always verify webhook signatures** - Prevents spoofed events
3. **Use metadata for user linking** - Store userId in checkout session metadata
4. **Validate prices server-side** - Don't trust client-provided amounts
5. **Handle all webhook events** - Don't just rely on success redirect

Quick Install

$npx ai-builder add skill ioku24/stripe-integration

Details

Type
skill
Author
ioku24
Slug
ioku24/stripe-integration
Created
6d ago