> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js 集成

> 使用 Next.js App Router 构建完整的支付流程

<Accordion title="✨ 利用 AI 安装" icon="sparkles">
  将此提示复制到你的 AI 代码编辑器（Cursor、Copilot、Claude Code 等）中，即可自动完成集成：

  ```text theme={"system"}
  使用官方 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
  ```
</Accordion>

***

## 你将构建什么

一个完整的 Next.js 收银台集成，包括：

* 服务端创建收银台会话
* 客户端重定向到托管收银台页面
* Webhook 处理支付确认
* 基于支付状态的受保护路由

***

## 前置条件

* Next.js 13+ 带 App Router
* Waffo Pancake 账户和 API 密钥
* 在 Dashboard 中创建的产品

***

## 项目设置

### 1. 安装依赖

```bash theme={"system"}
npm install @waffo/pancake-ts
```

### 2. 环境变量

```bash theme={"system"}
# .env.local
WAFFO_MERCHANT_ID=your-merchant-id
WAFFO_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIE..."
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

<Tip>
  SDK 支持多种私钥格式：PEM、base64 或原始格式，初始化时会自动标准化。`.env` 文件中的字面量 `\n` 也能正常使用。
</Tip>

***

## 创建收银台 API 路由

使用 SDK 创建一个 API 路由来生成收银台会话：

```typescript theme={"system"}
// 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 });
  }
}
```

<Note>
  SDK 会自动处理请求签名和确定性幂等键，无需手动设置请求头。
</Note>

***

## 定价页面组件

创建带有收银台按钮的定价页面：

```tsx theme={"system"}
// 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 密钥：

```typescript theme={"system"}
// 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 });
  }
}
```

<Note>
  SDK 的 `verifyWebhook()` 使用内嵌公钥进行签名验证，无需设置 `WAFFO_WEBHOOK_SECRET` 环境变量。同时默认包含重放攻击防护。
</Note>

***

## 成功页面

支付成功后显示确认：

```tsx theme={"system"}
// 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 为生产端点

***

## 下一步

<CardGroup cols={2}>
  <Card title="处理 Webhooks" icon="webhook" href="/zh/guides/webhooks">
    深入了解 Webhook 处理
  </Card>

  <Card title="订阅管理" icon="repeat" href="/zh/guides/subscriptions">
    高级订阅功能
  </Card>
</CardGroup>
