AgentSkillsCN

stripe

协助在 Next.js 项目中完成 Stripe 支付集成。适用于实现结账流程、订阅功能、Webhook、客户门户,或排查支付问题时使用。涵盖 Stripe SDK 的使用、Webhook 验证以及订阅管理。

SKILL.md
--- frontmatter
name: stripe
description: "Help with Stripe payment integration in Next.js projects. Use when implementing checkout flows, subscriptions, webhooks, customer portal, or debugging payment issues. Covers Stripe SDK usage, webhook verification, and subscription management."
allowed-tools: Read, Glob, Grep, Write, Edit, Bash, WebSearch

Stripe Integration Helper

Assist with Stripe payment gateway integration for SaaS applications.

Quick Reference

Installation

bash
bun add stripe @stripe/stripe-js

Environment Variables

bash
# Server-side (secret)
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."

# Client-side (publishable)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."

# App URL for callbacks
NEXT_PUBLIC_APP_URL="https://your-app.com"

SDK Initialization

Server-side:

typescript
// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-01-27.acacia",
  typescript: true,
});

Client-side:

typescript
// lib/stripe-client.ts
import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

Common Tasks

1. Create Checkout Session

typescript
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { stripe } from "@/lib/stripe";

export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { priceId } = await request.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    metadata: { userId },
    customer_email: user.email, // Optional: pre-fill email
  });

  return NextResponse.json({ url: session.url });
}

2. Create Customer Portal Session

typescript
// app/api/billing/portal/route.ts
export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Get Stripe customer ID from your database
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user?.stripeCustomerId) {
    return NextResponse.json({ error: "No subscription" }, { status: 400 });
  }

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

  return NextResponse.json({ url: session.url });
}

3. Webhook Handler

typescript
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  let event: Stripe.Event;

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

  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleCheckoutComplete(session);
      break;
    }
    case "customer.subscription.updated": {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionUpdate(subscription);
      break;
    }
    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCancelled(subscription);
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }
  }

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

Webhook Events

EventWhen to Handle
checkout.session.completedUser completes checkout
customer.subscription.createdNew subscription starts
customer.subscription.updatedPlan change, renewal
customer.subscription.deletedSubscription cancelled
invoice.payment_succeededSuccessful payment
invoice.payment_failedFailed payment attempt
customer.updatedCustomer info changed

Subscription Status Values

StatusDescription
activeSubscription is current
past_duePayment failed, retrying
canceledSubscription ended
unpaidAll retry attempts failed
trialingIn trial period
incompleteFirst payment pending

Database Schema

Users Table (add Stripe fields)

sql
stripeCustomerId    TEXT UNIQUE
stripeSubscriptionId TEXT
stripePriceId       TEXT
stripeCurrentPeriodEnd TIMESTAMP

Subscription Sync Pattern

typescript
async function syncSubscription(
  userId: string,
  subscription: Stripe.Subscription
) {
  await db
    .update(users)
    .set({
      stripeSubscriptionId: subscription.id,
      stripePriceId: subscription.items.data[0].price.id,
      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
    })
    .where(eq(users.id, userId));
}

Feature Gating

typescript
async function checkFeatureAccess(userId: string): Promise<boolean> {
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user?.stripeSubscriptionId) return false;

  // Check if subscription is still valid
  const now = new Date();
  return user.stripeCurrentPeriodEnd > now;
}

Testing

Test Card Numbers

CardScenario
4242424242424242Successful payment
4000000000000002Card declined
4000002500003155Requires 3D Secure
4000000000009995Insufficient funds

Stripe CLI for Local Webhooks

bash
# Install Stripe CLI
brew install stripe/stripe-cli/stripe

# Login
stripe login

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

# Trigger test events
stripe trigger checkout.session.completed

Pricing Page Pattern

typescript
// Get prices from Stripe
const prices = await stripe.prices.list({
  active: true,
  expand: ["data.product"],
});

// Display in component
{prices.data.map((price) => (
  <PriceCard
    key={price.id}
    name={(price.product as Stripe.Product).name}
    price={price.unit_amount! / 100}
    interval={price.recurring?.interval}
    priceId={price.id}
  />
))}

Security Best Practices

  1. Never expose secret key - Use STRIPE_SECRET_KEY only server-side
  2. Verify webhook signatures - Always use stripe.webhooks.constructEvent
  3. Idempotency - Store event IDs to prevent duplicate processing
  4. Raw body for webhooks - Don't parse JSON before verification
  5. Use metadata - Store userId in checkout session metadata

Common Issues

IssueSolution
Webhook signature invalidUse raw body, not parsed JSON
Customer not foundCreate customer before checkout
Subscription not syncingCheck webhook event registration
Test cards failingEnsure using test mode keys
Portal not loadingVerify customer has active subscription

Useful Commands

bash
# List products
stripe products list

# List prices
stripe prices list

# Get subscription
stripe subscriptions retrieve sub_xxx

# Cancel subscription
stripe subscriptions cancel sub_xxx