1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsで高速eコマースアプリを構築する | エピソード5: Stripeを使った決済機能の統合

Posted at

こんにちは!前回のエピソードでは、Zustandを使ってカート機能を実装し、商品の追加や数量変更を可能にしました。今回は、Stripeを統合して安全でスムーズな決済機能を追加します。チェックアウトページを構築し、フォームバリデーションや決済処理の実装を通じて、ユーザーが簡単に購入を完了できるようにします。

このエピソードのゴール

  • Stripe CheckoutをNext.jsに統合。
  • チェックアウトページを構築し、フォームバリデーションを実装。
  • カートデータを基に決済セッションを作成。
  • Tailwind CSSでレスポンシブなチェックアウトUIをデザイン。

必要なもの

  • 前回のプロジェクト(next-ecommerce)がセットアップ済み。
  • Stripeアカウントと公開鍵/秘密鍵(Stripeダッシュボードから取得)。
  • @stripe/stripe-jsstripeパッケージ。
  • 基本的なTypeScript、React、Next.jsの知識。

ステップ1: Stripeのセットアップ

Stripeをプロジェクトに統合するために、必要なパッケージをインストールします:

npm install @stripe/stripe-js stripe

Stripeの秘密鍵を安全に管理するため、.env.localに以下を追加:

STRIPE_SECRET_KEY=あなたのStripe秘密鍵
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=あなたのStripe公開鍵

注意: 秘密鍵はサーバーサイドでのみ使用し、公開鍵はクライアントサイドで使用します。


ステップ2: Stripe APIクライアントの作成

サーバーサイドでStripeと通信するためのクライアントを作成します。src/lib/stripe.tsファイルを作成し、以下のコードを追加:

import Stripe from 'stripe';

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-09-30.acacia',
});

このコードは、Stripeの秘密鍵を使ってAPIクライアントを初期化します。


ステップ3: チェックアウトページの構築

チェックアウトページを作成し、カートデータを表示して決済に進むフォームを構築します。src/app/checkout/page.tsxファイルを作成し、以下のコードを追加:

import { useCartStore } from '@/lib/cartStore';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function CheckoutPage() {
  const { items, clearCart } = useCartStore();
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const router = useRouter();

  const totalPrice = items.reduce((sum, item) => sum + item.price * item.quantity, 0);

  const handleCheckout = async () => {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ items }),
      });

      const { url } = await response.json();
      if (url) {
        clearCart();
        router.push(url);
      } else {
        setError('決済セッションの作成に失敗しました。');
      }
    } catch (err) {
      setError('エラーが発生しました。もう一度お試しください。');
    } finally {
      setLoading(false);
    }
  };

  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">チェックアウト</h1>
      {items.length === 0 ? (
        <p className="text-gray-600">カートに商品がありません。</p>
      ) : (
        <div className="grid md:grid-cols-2 gap-8">
          {/* カート内容 */}
          <div>
            <h2 className="text-xl font-semibold mb-4">注文内容</h2>
            {items.map((item) => (
              <div key={item.variantId} className="flex justify-between border-b py-2">
                <span>
                  {item.title} x {item.quantity}
                </span>
                <span>
                  {(item.price * item.quantity).toFixed(2)} JPY
                </span>
              </div>
            ))}
            <div className="text-right font-bold mt-4">
              合計: {totalPrice.toFixed(2)} JPY
            </div>
          </div>
          {/* 決済フォーム */}
          <div>
            <h2 className="text-xl font-semibold mb-4">決済情報</h2>
            <div className="space-y-4">
              <div>
                <label className="block text-sm font-medium">メールアドレス</label>
                <input
                  type="email"
                  className="w-full border p-2 rounded"
                  placeholder="example@email.com"
                  required
                />
              </div>
              <div>
                <label className="block text-sm font-medium">配送先住所</label>
                <input
                  type="text"
                  className="w-full border p-2 rounded"
                  placeholder="東京都渋谷区..."
                  required
                />
              </div>
              {error && <p className="text-red-500">{error}</p>}
              <button
                onClick={handleCheckout}
                disabled={loading}
                className="w-full bg-primary text-white px-6 py-3 rounded hover:bg-opacity-90 disabled:opacity-50"
              >
                {loading ? '処理中...' : '決済に進む'}
              </button>
            </div>
          </div>
        </div>
      )}
    </main>
  );
}

このコードは:

  • カート内容と合計金額を表示。
  • メールアドレスと住所の簡易フォームを用意(本番ではバリデーションを強化)。
  • /api/checkoutエンドポイントにリクエストを送信してStripe Checkoutセッションを作成。

ステップ4: Stripe Checkout APIエンドポイントの作成

Stripe Checkoutセッションを作成するAPIルートを追加します。src/app/api/checkout/route.tsファイルを作成し、以下のコードを追加:

import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';

export async function POST(request: Request) {
  try {
    const { items } = await request.json();

    const session = await stripe.checkout.sessions.create({
      payment_method_types: ['card'],
      line_items: items.map((item: any) => ({
        price_data: {
          currency: 'jpy',
          product_data: {
            name: item.title,
            images: [item.image],
          },
          unit_amount: Math.round(item.price * 100), // 円をセントに変換
        },
        quantity: item.quantity,
      })),
      mode: 'payment',
      success_url: `${request.headers.get('origin')}/success`,
      cancel_url: `${request.headers.get('origin')}/cart`,
    });

    return NextResponse.json({ url: session.url });
  } catch (error) {
    console.error('Stripeエラー:', error);
    return NextResponse.json({ error: '決済セッションの作成に失敗しました' }, { status: 500 });
  }
}

このコードは:

  • カートアイテムを基にStripe Checkoutセッションを作成。
  • 成功時またはキャンセル時のリダイレクトURLを指定。
  • セッションのURLをクライアントに返す。

ステップ5: 決済成功ページの作成

決済完了後に表示するページを作成します。src/app/success/page.tsxファイルを作成:

export default function SuccessPage() {
  return (
    <main className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">ご購入ありがとうございます!</h1>
      <p className="text-gray-600">
        ご注文が正常に処理されました。詳細はメールでご確認ください。
      </p>
      <a href="/" className="mt-4 inline-block bg-primary text-white px-6 py-3 rounded hover:bg-opacity-90">
        ホームに戻る
      </a>
    </main>
  );
}

このページは、決済成功後にユーザーに感謝のメッセージを表示します。


ステップ6: 動作確認

  1. .env.localにStripeの公開鍵と秘密鍵を設定。
  2. 開発サーバーを起動(npm run dev)。
  3. カートに商品を追加し、/checkoutにアクセス。
  4. フォームにテスト情報を入力し、「決済に進む」をクリック。
  5. Stripeのテスト決済ページにリダイレクトされ、以下の点を確認:
    • 商品名、数量、金額が正しく表示される。
    • 決済完了後に/successページにリダイレクトされる。
    • カートが空になる(clearCartが機能)。
  6. エラーがあれば、コンソールログやStripeダッシュボードを確認。

テストカード: Stripeのテスト環境では、4242 4242 4242 4242(有効期限: 未来の日付、CVC: 任意の3桁)を使用。


まとめと次のステップ

このエピソードでは、Stripe Checkoutを統合し、チェックアウトページと決済機能を構築しました。フォームバリデーションやエラーハンドリングも追加し、ユーザーが安全に購入を完了できるようにしました。

次回のエピソードでは、パフォーマンス最適化に焦点を当て、Core Web Vitals(LCP、CLSなど)を改善します。Lighthouseを使った分析やReact Server Componentsの活用も紹介しますので、引き続きお楽しみに!


この記事が役に立ったと思ったら、ぜひ「いいね」を押して、ストックしていただければ嬉しいです!次回のエピソードもお楽しみに!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?