stripe
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStripe Integration Helper
Stripe集成助手
Assist with Stripe payment gateway integration for SaaS applications.
为SaaS应用提供Stripe支付网关集成支持。
Quick Reference
快速参考
Installation
安装
bash
bun add stripe @stripe/stripe-jsbash
bun add stripe @stripe/stripe-jsEnvironment Variables
环境变量
bash
undefinedbash
undefinedServer-side (secret)
服务端(密钥)
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
Client-side (publishable)
客户端(可发布密钥)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
App URL for callbacks
回调用的应用URL
NEXT_PUBLIC_APP_URL="https://your-app.com"
undefinedNEXT_PUBLIC_APP_URL="https://your-app.com"
undefinedSDK Initialization
SDK初始化
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!
);服务端:
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,
});客户端:
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
1. 创建结账会话
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 });
}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, // 可选:预填邮箱
});
return NextResponse.json({ url: session.url });
}2. Create Customer Portal Session
2. 创建客户门户会话
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 });
}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 });
}
// 从数据库获取Stripe客户ID
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
3. Webhook处理器
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 });
}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签名验证失败");
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
Webhook事件
| Event | When to Handle |
|---|---|
| User completes checkout |
| New subscription starts |
| Plan change, renewal |
| Subscription cancelled |
| Successful payment |
| Failed payment attempt |
| Customer info changed |
| 事件 | 处理时机 |
|---|---|
| 用户完成结账时 |
| 新订阅启动时 |
| 套餐变更、续订时 |
| 订阅取消时 |
| 支付成功时 |
| 支付尝试失败时 |
| 客户信息变更时 |
Subscription Status Values
订阅状态值
| Status | Description |
|---|---|
| Subscription is current |
| Payment failed, retrying |
| Subscription ended |
| All retry attempts failed |
| In trial period |
| First payment pending |
| 状态 | 描述 |
|---|---|
| 订阅当前有效 |
| 支付失败,正在重试 |
| 订阅已终止 |
| 所有重试尝试均失败 |
| 处于试用阶段 |
| 首次支付待处理 |
Database Schema
数据库表结构
Users Table (add Stripe fields)
用户表(新增Stripe相关字段)
sql
stripeCustomerId TEXT UNIQUE
stripeSubscriptionId TEXT
stripePriceId TEXT
stripeCurrentPeriodEnd TIMESTAMPsql
stripeCustomerId TEXT UNIQUE
stripeSubscriptionId TEXT
stripePriceId TEXT
stripeCurrentPeriodEnd TIMESTAMPSubscription 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));
}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;
}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;
// 检查订阅是否仍有效
const now = new Date();
return user.stripeCurrentPeriodEnd > now;
}Testing
测试
Test Card Numbers
测试卡号
| Card | Scenario |
|---|---|
| Successful payment |
| Card declined |
| Requires 3D Secure |
| Insufficient funds |
| 卡号 | 场景 |
|---|---|
| 支付成功 |
| 卡片被拒绝 |
| 需要3D安全验证 |
| 余额不足 |
Stripe CLI for Local Webhooks
使用Stripe CLI测试本地Webhook
bash
undefinedbash
undefinedInstall Stripe CLI
安装Stripe CLI
brew install stripe/stripe-cli/stripe
brew install stripe/stripe-cli/stripe
Login
登录
stripe login
stripe login
Forward webhooks to local
将Webhook转发到本地
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Trigger test events
触发测试事件
stripe trigger checkout.session.completed
undefinedstripe trigger checkout.session.completed
undefinedPricing 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}
/>
))}typescript
// 从Stripe获取价格
const prices = await stripe.prices.list({
active: true,
expand: ["data.product"],
});
// 在组件中展示
{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
安全最佳实践
- Never expose secret key - Use only server-side
STRIPE_SECRET_KEY - Verify webhook signatures - Always use
stripe.webhooks.constructEvent - Idempotency - Store event IDs to prevent duplicate processing
- Raw body for webhooks - Don't parse JSON before verification
- Use metadata - Store userId in checkout session metadata
- 切勿暴露密钥 - 仅在服务端使用
STRIPE_SECRET_KEY - 验证Webhook签名 - 始终使用
stripe.webhooks.constructEvent - 幂等性 - 存储事件ID以避免重复处理
- Webhook使用原始请求体 - 验证前不要解析JSON
- 使用元数据 - 在结账会话元数据中存储userId
Common Issues
常见问题
| Issue | Solution |
|---|---|
| Webhook signature invalid | Use raw body, not parsed JSON |
| Customer not found | Create customer before checkout |
| Subscription not syncing | Check webhook event registration |
| Test cards failing | Ensure using test mode keys |
| Portal not loading | Verify customer has active subscription |
| 问题 | 解决方案 |
|---|---|
| Webhook签名无效 | 使用原始请求体,而非解析后的JSON |
| 找不到客户 | 在结账前创建客户 |
| 订阅未同步 | 检查Webhook事件是否已注册 |
| 测试卡片支付失败 | 确保使用测试模式密钥 |
| 客户门户无法加载 | 验证客户是否有有效订阅 |
Useful Commands
常用命令
bash
undefinedbash
undefinedList products
列出产品
stripe products list
stripe products list
List prices
列出价格
stripe prices list
stripe prices list
Get subscription
获取订阅信息
stripe subscriptions retrieve sub_xxx
stripe subscriptions retrieve sub_xxx
Cancel subscription
取消订阅
stripe subscriptions cancel sub_xxx
undefinedstripe subscriptions cancel sub_xxx
undefined