✨ 利用 AI 安装
✨ 利用 AI 安装
将此提示复制到你的 AI 代码编辑器(Cursor、Copilot、Claude Code 等)中,即可自动完成集成:
使用官方 TypeScript SDK 将 Waffo Pancake 支付集成到我的 Next.js 应用中。
npm install @waffo/pancake-ts
要求:
1. 创建 /app/api/checkout/route.ts — 使用 WaffoPancake 客户端调用 client.checkout.createSession()
2. 创建 /app/api/webhooks/waffo/route.ts — 使用 SDK 的 verifyWebhook() 验证 x-waffo-signature
3. 添加环境变量:WAFFO_MERCHANT_ID, WAFFO_PRIVATE_KEY, NEXT_PUBLIC_APP_URL
完整 API 参考请阅读 https://waffo.mintlify.app/llms-full.txt
你将构建什么
一个完整的 Next.js 结账集成,包括:- 服务端创建结账会话
- 客户端重定向到托管结账页面
- Webhook 处理支付确认
- 基于支付状态的受保护路由
前置条件
- Next.js 13+ 带 App Router
- Waffo Pancake 账户和 API 密钥
- 在 Dashboard 中创建的产品
项目设置
1. 安装依赖
npm install @waffo/pancake-ts
2. 环境变量
# .env.local
WAFFO_MERCHANT_ID=your-merchant-id
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIE..."
NEXT_PUBLIC_APP_URL=http://localhost:3000
SDK 支持多种私钥格式:PEM、base64 或原始格式,初始化时会自动标准化。
.env 文件中的字面量 \n 也能正常使用。创建结账 API 路由
使用 SDK 创建一个 API 路由来生成结账会话:// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { WaffoPancake, CheckoutSessionProductType, WaffoPancakeError } from "@waffo/pancake-ts";
const client = new WaffoPancake({
merchantId: process.env.WAFFO_MERCHANT_ID!,
privateKey: process.env.WAFFO_PRIVATE_KEY!,
});
export async function POST(req: NextRequest) {
try {
const { productId, email, metadata } = await req.json();
const session = await client.checkout.createSession({
storeId: "store_xxx",
productId,
productType: CheckoutSessionProductType.Onetime,
currency: "USD",
buyerEmail: email || undefined,
metadata,
successUrl: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
});
return NextResponse.json({ checkoutUrl: session.checkoutUrl });
} catch (error) {
if (error instanceof WaffoPancakeError) {
return NextResponse.json({ error: error.errors[0]?.message }, { status: error.status });
}
return NextResponse.json({ error: "Failed to create checkout" }, { status: 500 });
}
}
SDK 会自动处理请求签名和确定性幂等键,无需手动设置请求头。
定价页面组件
创建带有结账按钮的定价页面:// app/pricing/page.tsx
'use client';
import { useState } from 'react';
const plans = [
{
name: '入门版',
price: '¥69',
period: '月',
productId: 'prod_starter',
features: ['5 个项目', '10GB 存储', '邮件支持'],
},
{
name: '专业版',
price: '¥199',
period: '月',
productId: 'prod_pro',
features: ['无限项目', '100GB 存储', '优先支持', 'API 访问'],
popular: true,
},
{
name: '企业版',
price: '¥699',
period: '月',
productId: 'prod_enterprise',
features: ['专业版所有功能', '自定义集成', '专属支持', 'SLA'],
},
];
export default function PricingPage() {
const [loading, setLoading] = useState<string | null>(null);
async function handleCheckout(productId: string) {
setLoading(productId);
try {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId }),
});
const { checkoutUrl, error } = await response.json();
if (error) {
alert(error);
return;
}
// 重定向到 Waffo Pancake 结账页面
window.location.href = checkoutUrl;
} catch (error) {
alert('出错了');
} finally {
setLoading(null);
}
}
return (
<div className="py-12">
<h1 className="text-4xl font-bold text-center mb-12">
选择你的计划
</h1>
<div className="grid md:grid-cols-3 gap-8 max-w-5xl mx-auto px-4">
{plans.map((plan) => (
<div
key={plan.name}
className={`border rounded-lg p-6 ${
plan.popular ? 'border-green-500 ring-2 ring-green-500' : ''
}`}
>
{plan.popular && (
<span className="bg-green-500 text-white text-sm px-3 py-1 rounded-full">
最受欢迎
</span>
)}
<h2 className="text-2xl font-bold mt-4">{plan.name}</h2>
<p className="text-4xl font-bold mt-2">
{plan.price}
<span className="text-lg font-normal">/{plan.period}</span>
</p>
<ul className="mt-6 space-y-3">
{plan.features.map((feature) => (
<li key={feature} className="flex items-center">
<CheckIcon className="w-5 h-5 text-green-500 mr-2" />
{feature}
</li>
))}
</ul>
<button
onClick={() => handleCheckout(plan.productId)}
disabled={loading !== null}
className="w-full mt-8 py-3 px-4 bg-black text-white rounded-lg hover:bg-gray-800 disabled:opacity-50"
>
{loading === plan.productId ? '加载中...' : '立即开始'}
</button>
</div>
))}
</div>
</div>
);
}
function CheckIcon({ className }: { className: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
);
}
Webhook 处理器
使用 SDK 内置的verifyWebhook() 处理支付确认 — 它内嵌了测试和生产环境的公钥,无需管理 Webhook 密钥:
// app/api/webhooks/waffo/route.ts
import { NextResponse } from "next/server";
import { verifyWebhook, WebhookEventType } from "@waffo/pancake-ts";
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("x-waffo-signature");
try {
const event = verifyWebhook(body, signature);
// 立即响应,异步处理业务逻辑
switch (event.eventType) {
case WebhookEventType.OrderCompleted:
console.log(`Order completed: ${event.data.orderId}`);
// 更新数据库、授予访问权限等
break;
case WebhookEventType.SubscriptionActivated:
console.log(`Subscription activated: ${event.data.buyerEmail}`);
break;
case WebhookEventType.SubscriptionCanceling:
console.log(`Subscription canceling: ${event.data.orderId}`);
break;
case WebhookEventType.SubscriptionCanceled:
console.log(`Subscription canceled: ${event.data.orderId}`);
break;
case WebhookEventType.RefundSucceeded:
console.log(`Refund succeeded: ${event.data.amount} ${event.data.currency}`);
break;
}
return NextResponse.json({ received: true });
} catch {
return new Response("Invalid signature", { status: 401 });
}
}
SDK 的
verifyWebhook() 使用内嵌公钥进行签名验证,无需设置 WAFFO_WEBHOOK_SECRET 环境变量。同时默认包含重放攻击防护。成功页面
支付成功后显示确认:// app/success/page.tsx
import { Suspense } from 'react';
export default function SuccessPage() {
return (
<Suspense fallback={<div>加载中...</div>}>
<SuccessContent />
</Suspense>
);
}
async function SuccessContent() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-2xl font-bold mb-2">支付成功!</h1>
<p className="text-gray-600 mb-6">
感谢你的购买。你现在可以访问所有功能了。
</p>
<a
href="/dashboard"
className="inline-block bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800"
>
进入控制台
</a>
</div>
</div>
);
}
完整文件结构
app/
├── api/
│ ├── checkout/
│ │ └── route.ts # 创建结账会话
│ └── webhooks/
│ └── waffo/
│ └── route.ts # 处理 Webhooks
├── pricing/
│ └── page.tsx # 定价页面
├── success/
│ └── page.tsx # 成功页面
├── dashboard/
│ └── page.tsx # 受保护的仪表盘
└── layout.tsx
middleware.ts # 路由保护
.env.local # 环境变量
测试清单
上线前:- 使用测试卡
4576 7500 0000 0110测试结账流程 - 验证 Webhooks 已收到(检查 Dashboard 日志)
- 使用
4576 7500 0000 0220测试拒绝支付 - 确认成功页面正确显示
- 切换到正式 API 密钥
- 更新 Webhook URL 为生产端点
下一步
处理 Webhooks
深入了解 Webhook 处理
订阅管理
高级订阅功能