✨ AI でインストール
✨ AI でインストール
このプロンプトを AI コードエディタ(Cursor、Copilot、Claude Code など)にコピーして、統合を自動的にセットアップ:
Integrate Waffo Pancake payments into my Next.js app using the official TypeScript SDK.
npm install @waffo/pancake-ts
Requirements:
1. Create /app/api/checkout/route.ts — use WaffoPancake client with client.checkout.createSession()
2. Create /app/api/webhooks/waffo/route.ts — use verifyWebhook() from SDK to verify x-waffo-signature
3. Add environment variables: WAFFO_MERCHANT_ID, WAFFO_PRIVATE_KEY, NEXT_PUBLIC_APP_URL
Read https://waffo.mintlify.app/llms-full.txt for full API reference.
構築するもの
Next.js での完全なチェックアウト統合。以下を含みます:- サーバーサイドでのチェックアウトセッション作成
- クライアントサイドからホスト型チェックアウトへのリダイレクト
- 支払い確認のための Webhook 処理
- 支払いステータスに基づく保護されたルート
前提条件
- Next.js 13+(App Router)
- API キーを持つ Waffo Pancake アカウント
- ダッシュボードで作成済みの商品
プロジェクトセットアップ
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 はリクエスト署名を自動で処理するため、手動でヘッダーを設定する必要はありません。冪等性はオプトインです:リトライで 2 件目のレコードを作ってはいけない場合は、書き込み(または server action)の最後の引数に
{ idempotencyKey } を渡してください。渡さなければヘッダーは送信されず、重複排除も行われません。価格ページコンポーネント
チェックアウトボタン付きの価格ページを作成します:// app/pricing/page.tsx
'use client';
import { useState } from 'react';
const plans = [
{
name: 'Starter',
price: '$9',
period: 'month',
productId: 'prod_starter',
features: ['5 projects', '10GB storage', 'Email support'],
},
{
name: 'Pro',
price: '$29',
period: 'month',
productId: 'prod_pro',
features: ['Unlimited projects', '100GB storage', 'Priority support', 'API access'],
popular: true,
},
{
name: 'Enterprise',
price: '$99',
period: 'month',
productId: 'prod_enterprise',
features: ['Everything in Pro', 'Custom integrations', 'Dedicated support', '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('Something went wrong');
} finally {
setLoading(null);
}
}
return (
<div className="py-12">
<h1 className="text-4xl font-bold text-center mb-12">
Choose Your Plan
</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">
Most Popular
</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 ? 'Loading...' : 'Get Started'}
</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>Loading...</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">Payment Successful!</h1>
<p className="text-gray-600 mb-6">
Thank you for your purchase. You now have access to all features.
</p>
<a
href="/ja/dashboard"
className="inline-block bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800"
>
Go to Dashboard
</a>
</div>
</div>
);
}
保護されたルートのミドルウェア
サブスクリプションステータスに基づいてルートを保護します:// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// ユーザーセッションを取得(認証ロジックを実装してください)
const session = request.cookies.get('session');
// 保護されたルート
const protectedPaths = ['/dashboard', '/settings', '/projects'];
const isProtectedPath = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
);
if (isProtectedPath && !session) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/settings/:path*', '/projects/:path*'],
};
サーバーコンポーネント:サブスクリプションの確認
// app/dashboard/page.tsx
import { redirect } from 'next/navigation';
import { getServerSession } from 'your-auth-library';
export default async function DashboardPage() {
const session = await getServerSession();
if (!session) {
redirect('/login');
}
// データベースからユーザーのサブスクリプションを取得
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: {
plan: true,
subscriptionActive: true,
subscriptionEndsAt: true,
},
});
if (!user?.subscriptionActive) {
redirect('/pricing');
}
return (
<div>
<h1>Welcome to your Dashboard</h1>
<p>Your current plan: {user.plan}</p>
{/* ダッシュボードのコンテンツ */}
</div>
);
}
カスタマーポータルリンク
ユーザーが自分のサブスクリプションを管理できるようにします:// components/ManageSubscription.tsx
'use client';
export function ManageSubscriptionButton({ email }: { email: string }) {
const portalUrl = `https://checkout.waffo.ai/your-store/portal?email=${encodeURIComponent(email)}`;
return (
<a
href={portalUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
Manage Subscription
</a>
);
}
完全なファイル構成
app/
├── api/
│ ├── checkout/
│ │ └── route.ts # チェックアウトセッション作成
│ └── webhooks/
│ └── waffo/
│ └── route.ts # Webhook 処理
├── pricing/
│ └── page.tsx # 価格ページ
├── success/
│ └── page.tsx # 成功ページ
├── dashboard/
│ └── page.tsx # 保護されたダッシュボード
└── layout.tsx
middleware.ts # ルート保護
.env.local # 環境変数
テストチェックリスト
本番稼働前:- テストカード
4576 7500 0000 0110でチェックアウトフローをテスト - Webhook が受信されることを確認(ダッシュボードのログを確認)
-
4576 7500 0000 0220で拒否された支払いをテスト - 成功ページが正しく表示されることを確認
- 本番 API キーに切り替え
- Webhook URL を本番エンドポイントに更新
次のステップ
Webhook の処理
Webhook 処理の詳細
サブスクリプション管理
高度なサブスクリプション機能