💲
Stripe x Next.js Page Router: Create checkout session
背景
Next.js 15を使っているがまだ Page Router を使っており、Stripe 公式では App Router (Server Component) でのやり方しか書いてなかったのでメモ。
なお Firestore と Run payment with Stripe extension を使っている場合は、専用の Web SDK からセッションを作成しないと正常に機能しないので、その場合はこちら。
また今回は顧客データのIDを自社システムのユーザーIDと紐づける必要があるため自前でAPIを実装しているが、決済ページに飛ばして成功したら返ってくるだけでいいなら Payment Links を使って作成したURLをボタンのリンク先に指定するだけでOK。
package.json
生成AI開発の v0 x shadcn/ui を使っている関係で、React19など最新バージョンのライブラリばかりを入れている。
"dependencies": {
"@stripe/stripe-js": "^7.2.0",
"@tailwindcss/postcss": "^4.1.4",
"firebase": "^10.7.2",
"firebase-admin": "^13.2.0",
"next": "^15.3.1",
"next-themes": "^0.4.6",
"postcss": "^8.5.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"stripe": "^18.0.0",
}
api/checkout_sessions/index.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
apiVersion: '', // Stripe ダッシュボードで設定している API バージョン
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { priceId } = req.body;
// Create Checkout Session
const session = await stripe.checkout.sessions.create({
line_items: [ // 数量が1個なら `priceId: priceId,` だけでいいはず
{
price: priceId,
quantity: 1,
},
],
mode: 'subscription',
success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`, // CHECKOUT_SESSION_ID は Stripe がリダイレクト時に付与してくれる
cancel_url: `${req.headers.origin}/pricing`,
automatic_tax: { enabled: true }, // 任意。Stripe Tax 税金の自動徴収を設定しているかどうかで挙動が変わりそう
});
return res.status(200).json({ sessionId: session.id });
} catch (error) {
console.error('Error creating checkout session:', error);
return res.status(500).json({ error: 'Error creating checkout session' });
}
}
pages/pricing/index.tsx
import React, { useState } from 'react';
import { useRouter } from 'next/router';
const PricingPage = () => {
const router = useRouter();
const [loading, setLoading] = useState<boolean>(false);
const handleSubscription = async (priceId: string) => {
setLoading(true);
try {
const response = await fetch('/api/checkout_sessions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
priceId,
}),
});
const { sessionId } = await response.json();
// Redirect to Stripe Checkout
const stripe = await getStripe();
stripe?.redirectToCheckout({ sessionId });
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
};
return (
<>
<Button
onClick={() => handleSubscription('your_product_price_id')}
disabled={loading}
>
{loading ? '処理中...' : '登録する'}
</Button>
</>
)
};
// TODO: lib/stripe.ts などに移動
const getStripe = async () => {
const { loadStripe } = await import('@stripe/stripe-js');
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
);
return stripePromise;
};
export default PricingPage;
解決しない場合
お困りでしたら X のDM、もしくは Lancers などからご相談ください。
Discussion