Next.js 14 で Stripe サブスクリプションを実装する

こんばんは。

今日は「Next.js 14 で Stripe サブスクリプションを実装する方法」について説明します。

Next.js 14 で Stripe サブスクリプションを実装する方法について、フロントエンドとバックエンドの両方で必要な手順とコード例を以下に示します。フロントエンドでは、カード番号、CVC、有効期限、および顧客名を含む支払いフォームを作成し、バックエンドでは Next.js API を使用して Stripe サブスクリプションを管理します。

# 必要なパッケージのインストールと環境変数の設定

## パッケージのインストール

まず、Stripe の公式ライブラリと必要なパッケージをインストールします。

```bash
npm install @stripe/stripe-js @stripe/react-stripe-js stripe
```

- `@stripe/stripe-js` と `@stripe/react-stripe-js`: フロントエンドで Stripe Elements を使用するためのライブラリ。
- `stripe`: バックエンドで Stripe API を操作するためのライブラリ。

## 環境変数の設定

プロジェクトのルートに .env.local ファイルを作成し、以下の環境変数を設定します。

```env
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=your_publishable_key_here
STRIPE_SECRET_KEY=your_secret_key_here
```

- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`: フロントエンドで使用する公開可能キー。
- `STRIPE_SECRET_KEY`: バックエンドで使用する秘密キー。

# 実装

## フロントエンドの実装

フロントエンドでは、Stripe Elements を使用して安全な支払いフォームを作成します。

### Stripe プロバイダーの設定

_app.js または _app.tsx ファイルで Elements プロバイダーを設定します。

```javascript
// pages/_app.js
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';
import '../styles/globals.css';

const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);

function MyApp({ Component, pageProps }) {
  return (
    <Elements stripe={stripePromise}>
      <Component {...pageProps} />
    </Elements>
  );
}

export default MyApp;
```

### 支払いフォームの作成

`pages/subscribe.js` ファイルを作成し、支払いフォームを実装します。

```javascript
// pages/subscribe.js
import { useState } from 'react';
import {
  CardCvcElement,
  CardExpiryElement,
  CardNumberElement,
  useElements,
  useStripe,
} from '@stripe/react-stripe-js'

const Subscribe = () => {
  const stripe = useStripe();
  const elements = useElements();
  
  const [customerName, setCustomerName] = useState('');
  const [email, setEmail] = useState('');
  const [subscriptionStatus, setSubscriptionStatus] = useState(null);
  const [error, setError] = useState(null);

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!stripe || !elements) {
      return;
    }

    const cardNumberElement = elements.getElement(CardNumberElement)
    const cardExpiryElement = elements.getElement(CardExpiryElement)
    const cardCvcElement = elements.getElement(CardCvcElement)

    if (!cardNumberElement || !cardExpiryElement || !cardCvcElement) {
      console.error('Stripe Elements not found')
      return
    }

    try {
      const { error, paymentMethod } = await stripe.createPaymentMethod({
        type: 'card',
        card: cardNumberElement,
        billing_details: {
          name: cardholderName, // Pass the cardholder name here
        },
      })

      if (error) return

      // 顧客情報と支払い情報をバックエンドに送信
      const response = await fetch('/api/subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: customerName,
          email: email,
          paymentMethodId: paymentMethod.id,
        }),
      });
      
      const result = await response.json();

      if (result.error) {
        setError(result.error);
      } else {
        setSubscriptionStatus(result.status);
      }
    } catch (e) {
      console.error(e)
    }
  };

  return (
    <div>
      <h1>サブスクリプション登録</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label>名前</label>
          <input
            type="text"
            value={customerName}
            onChange={(e) => setCustomerName(e.target.value)}
            required
          />
        </div>
        <div>
          <label>メールアドレス</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
          />
        </div>
        <div>
          <label>カード情報</label>
          <CardNumberElement />
        </div>
        <div>
          <label>有効期限</label>
          <CardExpiryElement />
        </div>
        <div>
          <label>セキュリティコード</label>
          <CardCvcElement />
        </div>
        <button type="submit" disabled={!stripe}>
          購入
        </button>
      </form>
      {subscriptionStatus && <p>サブスクリプションが正常に作成されました！</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}
    </div>
  );
};

export default Subscribe;
```

フォーム送信時にバックエンド API /api/subscribe に顧客情報と支払い情報を送信します。

## バックエンドの実装

Next.js の API Routes を使用して、サブスクリプションの作成を処理します。

### Stripe インスタンスの設定

`lib/stripe.js` ファイルを作成し、Stripe インスタンスを設定します。

```javascript
// lib/stripe.js
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
  apiVersion: '2022-11-15',
});

export default stripe;
```

### サブスクリプション API の作成

pages/api/subscribe.js ファイルを作成し、サブスクリプションの作成を処理します。

```javascript
// pages/api/subscribe.js
import stripe from '../../lib/stripe';

export default async function handler(req, res) {
  if (req.method === 'POST') {
    const { name, email, paymentMethodId } = req.body;

    try {
      // 顧客の作成
      const customer = await stripe.customers.create({
        name: name,
        email: email,
        payment_method: paymentMethodId,
        invoice_settings: {
          default_payment_method: paymentMethodId,
        },
      });

      // サブスクリプションの作成
      const subscription = await stripe.subscriptions.create({
        customer: customer.id,
        items: [{ price: PRICE_ID }], // 事前にStripeダッシュボードで作成した価格IDに置き換えてください
        expand: ['latest_invoice.payment_intent'],
      });

      res.status(200).json({ status: subscription.status });
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  } else {
    res.setHeader('Allow', 'POST');
    res.status(405).end('Method Not Allowed');
  }
}
```

- サブスクリプションの作成: 作成した顧客に対してサブスクリプションを作成します。PRICE_ID(price_XXXXXXXXXX) は事前に Stripe ダッシュボードで設定した価格 ID に置き換えてください。
- エラーハンドリング: エラーが発生した場合、クライアントにエラーメッセージを返します。

以上が Next.js 14 で Stripe サブスクリプションを実装する基本的な手順とコード例です。詳細なカスタマイズや高度な機能については、Stripe の公式ドキュメントを参照してください。

# 追加の考慮事項

- セキュリティ: 支払い情報は Stripe Elements を使用して安全に処理されますが、常に最新のセキュリティベストプラクティスに従ってください。
- エラーハンドリング: フロントエンドとバックエンドの両方で適切なエラーハンドリングを実装し、ユーザーにわかりやすいフィードバックを提供してください。
- サブスクリプション管理: ユーザーがサブスクリプションを管理（キャンセル、更新など）できるように、追加の機能を実装することを検討してください。

今日は以上です。

ありがとうございました。
よろしくお願いいたします。