logo 公式Webサイト

パワフルな
ブログ

Stripeで決済画面を作成【Express,Next.js,TypeScript】

2024年5月17日

stripeを用いて決済画面を作成する場面がありましたので、備忘録としてまとめます。
後から「時間があれば」画像等を追加して、細かい説明を作成したいと、思っています😅
フロント側も「別途時間ができた際」に掲載しようとは思いますが、基本的には各種必要なものをbodyにセットして、今回作成するエンドポイントにリクエストを送るだけになります(雑)

stripeアカウント作成

下記リンクからアカウントを作成します。
stripeアカウント作成
登録完了後、環境を「テスト環境」に変更し、「商品カタログ」より商品を追加します。

各種モジュールをインストール

npm install stripe 

Stripeオブジェクトと、秘密キーを用意

stripeオブジェクトの定義と、秘密キーを用意し、定義しておきます。

const Stripe = require("stripe");
const stripe = Stripe("秘密キーを記載");

商品リストを取得

stripeの商品一覧は、priceIDというものに紐づいており、stripe.pricesで取得することができます。
また、stripeでは一度公開した商品を削除するのは基本的には無く、アーカイブという形をとります。
なので、該当の商品がアクティブ状態であるというのをフィルタリング(.product.active)する形をとります。

app.get("/prices", async (req, res) => {
  try {
    const prices = await stripe.prices.list({
      expand: ["data.product"],
    });

    // アーカイブされていない価格のみをフィルタリング
    const activePrices = prices.data.filter((price) => price.product.active);

    res.json(activePrices);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

会計処理を実行する

.checkout.sessions.createでチェックアウト(会計)処理のセッションを作成することができるので、セッションを作成。
価格はpriceプロパティで、priceID(stripeから取得した商品)をセット、個数はリクエスト時に受け取る想定のquantityをquantityプロパティにセット。
modeプロパティは、セッションのモードがstripeには複数あるので、その中で今回はpaymentを選択。
success_urlプロパティは購入完了後の遷移先、cancel_urlは購入キャンセル(購入ボタンから戻るボタンを押した際)時の遷移先を設定できます。
client_reference_idプロパティはstripeの決済を誰が行ったのかを把握するために固有のIDを付与します。
automatic_taxプロパティは税金の自動計算を設定するもので、今回はtrueにしています。

app.post("/create-checkout-session", async (req, res) => {
  const { priceId, userId, quantity } = req.body;

  const session = await stripe.checkout.sessions.create({
    payment_method_types: ["card"],
    line_items: [
      {
        price: priceId,
        quantity: quantity,
      },
    ],
    mode: "payment",
    success_url: `http://localhost:3000/user/userTest?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `http://localhost:3000/user/userTest`,
    client_reference_id: userId,
    automatic_tax: { enabled: true },
  });

  res.json({ url: session.url });
});

webHookを使用して、決済したユーザーの情報を任意のテーブル>カラムに追加

今回は、決済した情報をあらかじめ作成したuserテーブルのpointカラムに追加していきたい形となりますのでstripeが提供してくれているwebhookを使います。

webHookの作成

stripe側の管理画面から、「開発者」ボタンを押下し、「webhook」タブを選択し、「エンドポイントを追加」ボタンを押下します。
ローカルでエンドポイントを作成する場合は、CLIでテストすることができるのですが、今回はトンネリングツールのngrokを扱って、ローカルホストのサーバを外部公開します。ngrokのインストールについては下記を参照しました。
執筆者の方、本当にありがとうございます。
https://qiita.com/mininobu/items/b45dbc70faedf30f484e
実行した後に
Forwarding http://XXXXXXXX.ngrok.io -> localhost:8080
という画面が表示されるので、このhttp://XXXXXXXX.ngrok.ioをwebhookとしてstripe側に登録します。
今回はhttp://XXXXXXXX.ngrok.io/webhookとして登録します。
今回扱うイベントは会計処理の完了なので、イベントはCheckout>checkout.session.completedを選択します。
選択後、イベント追加を完了し、これでstripe側のwebhookエンドポイントの設定は完了となります。
このstripeの画面は、後ほどイベントのシークレットキーを取得する際に必要となります。

Express側でwebHookのエンドポイントを作成

続いて、Express側でエンドポイントを作成します。
データベースはmySqlでdbとしてインクルードしている形で、corsの設定も完了している前提となります。
まずは、先ほどのstripe側のエンドポイント画面で、「署名シークレット」を選択し、シークレットキーを取得し、そのキーをendpointSecret(名称は任意)という変数に割り当てます。

const endpointSecret = "秘密だよ"; // ここに取得したWebhook Secretを設定

続いて、エンドポイントを定義していきます。
基本的には下記のドキュメントに従った形で作成します。
https://docs.stripe.com/webhooks#verify-official-libraries
日本語でわかりやすく解説してくれるstripeさん、、、ありがとうございます🙇‍♂️

// Webhookエンドポイントの設定
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["stripe-signature"];
  let event;

  try {
    event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
    console.log("Webhook event constructed:", event); // デバッグログ
  } catch (err) {
    console.error("Webhook Error:", err.message); // エラーログ
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Checkoutセッション完了イベントを処理
  if (event.type === "checkout.session.completed") {
    const session = event.data.object;
    const userId = session.client_reference_id; // クライアントIDを取得
    const amount = session.amount_total; // 購入金額を取得(円)

    console.log(`Processing payment for user ${userId} with amount ${amount}`); // デバッグログ

    // ユーザーの現在のポイントを取得
    db.query("SELECT point FROM user WHERE id = ?", [userId], (err, results) => {
      if (err) {
        console.error("Error fetching user points:", err); // エラーログ
        return res.status(500).send("Error fetching user points");
      }

      const currentPoints = results[0].point;
      const newPoints = currentPoints + amount;

      // ユーザーのポイントを更新
      db.query("UPDATE user SET point = ? WHERE id = ?", [newPoints, userId], (err) => {
        if (err) {
          console.error("Error updating user points:", err); // エラーログ
          return res.status(500).send("Error updating user points");
        }
        console.log(`User ${userId} points updated to ${newPoints}`); // デバッグログ
      });
    });
  }

  res.status(200).end();
});

今回は、userというテーブルのpointというカラムを更新する想定として作成しています。
ここで注意なのですが、app.use(express.json());の下に、今回のエンドポイントを入れてしまうと、シグネチャチェックに使うパラメータにフィルタがかかるのが原因となりエラーが発生します。
ですので、エンドポイントはapp.use(express.json());の上に記載する形がベストプラクティス(使ってみたかったw)となります。
参考文献は下記になります。
執筆者の方、本当にありがとうございます、このエラー解消で一気に進みました。

https://zenn.dev/lambta/articles/5e004344bb8ec0

こんな感じで、一通りのアイテム取得処理->アイテム購入処理->webhook処理が完了となりました。