Stripe API with Next.js

Next.js, Stripe
2021-04-25

StripeのAPIをNext.jsで動かしてみた備忘録です。

準備

StripeのBusinessアカウントを取得。

https://dashboard.stripe.com/register

Next.jsをインストールし、Stripeのライブラリをインストールする。

# node.js用ライブラリ
npm install --save stripe
# react及びフロントエンド側ライブラリ
npm install --save @stripe/react-stripe-js @stripe/stripe-js

カスタムの支払いフローを実装する

Stripe決済の基本となる支払い(決済)を実装する。今回、Stripeが用意している構築済みの Checkout ページは利用せず、自前(カスタム可能な)のコンポーネントで画面を作ります。

下準備として、 .env.localファイルに以下ようにStripeのダッシュボード画面に表示されているAPIキーを登録します。公開されているAPIキーはフロントエンド側からも呼び出せる定数として、接頭辞に NEXT_PUBLIC_ を付けています。

NEXT_PUBLIC_STRIPE_API_KEY=pk_test_...
STRIPE_SECRET_API_KEY=sk_test...

PaymentIntent APIを作成する

PaymentIntent は、顧客の支払いライフサイクルを追跡し、支払いの試行失敗があった場合にはその記録を残して顧客への請求に重複が発生しないようにします。PaymentIntent の client secret をレスポンスで返して、クライアントで支払いを完了します。

実際の決済の前処理として、PaymentIntentを作成します。

Next.js のAPI Routesを利用し、チェックアウト用のAPI pages/api/checkout.ts を作成し、以下の内容を登録します。今回は純粋に price という金額(JPY)を受け取りそれを処理します。実際には購入アイテムのデータを受け取り、DB等に問い合わせ正確な金額を取得するなどの処理が必要になります。

// pages/api/checkout.ts
import type { NextApiRequest, NextApiResponse } from "next";
import Stripe from 'stripe';

const stripe_api_key = process.env.STRIPE_SECRET_API_KEY;
const stripe = new Stripe(stripe_api_key, {
  apiVersion: "2020-08-27"
});

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const { price } = req.body;
  // 注文アイテムの個数と、通貨を指定し、PaymentIntent を作成
  const paymentIntent = await stripe.paymentIntents.create({
    amount: price,
    currency: "jpy"
  });
  res.send({
    clientSecret: paymentIntent.client_secret
  });
};

フロントエンド(React.js)の実装

決済のページは以下のようになります。

// pages/checkout.tsx
import { NextPage } from 'next';
import React from 'react';

import { Elements as StripeElements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';

import CheckoutForm from '../components/CheckoutForm';

interface CheckoutProps {}

// loadScriptでstripeのスクリプトを呼び出す
const promise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_API_KEY);

/**
 * Checkout
 */
const Checkout: NextPage<CheckoutProps> = () => (
  <section>
    <div className="mt-10 mx-auto w-96">
      {/* striptのプロパティにloadStripを渡す。こうすることで子のコンポーネントでstripeのサービスが利用できるのようになる */}
      <StripeElements stripe={promise}>
        <CheckoutForm />
      </StripeElements>
    </div>
  </section>
);

export default Checkout;

loadStripeでブラウザで動作するStripeのjsファイルを読み込みます。(処理としては<head>タグ内に <script src="https://js.stripe.com/v3"></script> が読み込まれてる)

CheckoutFormにてカード番号の入力と決済の処理を行います。

import * as React from 'react';

import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
import { StripeCardElementChangeEvent } from '@stripe/stripe-js';

interface CheckoutFormProps {}

const cardOptions = {
  hidePostalCode: true,
  style: {
    base: {
      color: "#1a1a1a",
      fontFamily: "Arial, sans-serif",
      fontSmoothing: "antialiased",
      lineHeight: "1.4",
      fontSize: "16px",
      "::placeholder": {
        color: "#999"
      }
    }
  }
};

/**
 * CheckoutForm
 */
const CheckoutForm: React.FC<CheckoutFormProps> = () => {
  const price = 1000;
  const [succeeded, setSucceeded] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [processing, setProcessing] = React.useState(false);
  const [disabled, setDisabled] = React.useState(true);
  const [clientSecret, setClientSecret] = React.useState("");
  // react hookを利用したstripeへのアクセス
  const stripe = useStripe();
  const elements = useElements();

  React.useEffect(() => {
    // ページロード時にPaymentIntentを作成
    window
      .fetch("/api/checkout", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify({ price })
      })
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        setClientSecret(data.clientSecret);
      });
  }, []);

  // 入力時のチェック
  const handleChange = async (event: StripeCardElementChangeEvent) => {
    setDisabled(event.empty);
    setError(event.error ? event.error.message : "");
  };

  const handleSubmit = async (ev: React.FormEvent) => {
    ev.preventDefault();
    setProcessing(true);
    const payload = await stripe.confirmCardPayment(clientSecret, {
      payment_method: {
        card: elements.getElement(CardElement)
      }
    });
    if (payload.error) {
      setError(`Payment failed ${payload.error.message}`);
      setProcessing(false);
    } else {
      setError(null);
      setProcessing(false);
      setSucceeded(true);
    }
  };

  return (
    <form id="payment-form" onSubmit={handleSubmit}>
      <div className="my-2">&yen;{price} の支払い</div>
      {/* 成功時の表示 */}
      {succeeded ? (
        <p>
          <span>支払いが完了しました。</span>
          <a href={`https://dashboard.stripe.com/test/payments`}>
            Stripe dashboard.
          </a>
          <span>で確認しましょう。</span>
        </p>
      ) : (
        <div>
          {/* CartElementはiframeを利用したクレジットカード入力フォームを提供する */}
          <div className="border border-gray-300 p-4 rounded">
            <CardElement onChange={handleChange} options={cardOptions} />
          </div>
          <button
            className="rounded bg-blue-500 text-white px-4 py-2 mt-2"
            disabled={processing || disabled || succeeded}
            id="submit"
          >
            <span>{processing ? "sending..." : "Pay now"}</span>
          </button>
          {/* エラーの表示 */}
          {error && <div role="alert">{error}</div>}
        </div>
      )}
    </form>
  );
};
export default CheckoutForm;

ページ読み込み時に POST /api/checkout を呼び出しPaymentIntentを作成します。APIから受け取った固有のclientSecretのキーをstateに設定します。この値をSubmit時にセットすることでセキュリティなどのチェックをStripe側で行うことができるようになります。

Stripe.jsが提供する<CardElement> コンポーネントにカード番号の入力を簡単にできます。iframeで実行されており、Secure3Dにも対応することができます。

Submit時に stripe.confirmCardPayment を呼び出し決済を実行します。エラーが発生しない倍、成功とし表示を切り替えます。エラーの場合はその内容を表示します。

Written by Kyohei Tsukuda who lives and works in Tokyo 🇯🇵 , building useful things 🔧.