今回は現在取り組んでいるプロジェクトのOAuth2サーバーに決済機能を導入するお話です。

もともと決済手段に関してはStripeを利用することが決まっていたのですが、まさか私自身決済機能をサポートするとは思っていませんでした。 お金が関わってくるものですし、甘い実装が原因で不正流出事件が起きかねないので責任も重大です。 Stripeには幸いStripe Checkoutと呼ばれる決済フローがあらかじめ用意されているので、決済機能を簡単に導入する方法がありました。

ドキュメントは英語でつらい

残念ながら現時点ではStripeのドキュメントは日本語に対応していません。 といっても、もともとのドキュメントはとても見やすいし、各言語の実装例のリポジトリも存在するので実装するだけならそこまで難しくありません。 一度限りの決済の流れとしては:

  1. クライアント側でユーザーが購入するボタンを押す
  2. サーバー側にPOSTリクエストを送信する
  3. サーバー側でStripeに商品の情報を送信する(Sessionの作成)
  4. Stripeがリダイレクト用のURLを発行するので、サーバー側がSessionのIDを返す
  5. クライアント側はStripeにリダイレクトをさせる
  6. ユーザーはカード情報を入力するか、戻るボタンを押す
  7. Stripeがユーザーの行動に応じて指定されたURLへリダイレクトさせる
  8. クライアント側で決済完了かキャンセルのページを表示させる

少々長いですが、これが大まかなStripe Checkoutでの流れです。 ページはあらかじめStripeがデザインしたものを使うので、カード番号のバリデーション等は自分で実装する必要はありません。

サーバー側の実装メモ

import Stripe from "stripe"

const stripe = new Stripe(STRIPE_SECRET_KEY, {
  apiVersion: "2020-08-27", // <== APIのバージョンを指定する
  typescript: false, // <== TypeScriptも指定できる
})

app.post("/create-checkout-session", async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    payment_method_types: ["card"],
    locale: 'ja', // <== 日本語
    customer_email: 'customer@example.com', // <== メールを指定できる
    line_items: [
      {
        price_data: {
          currency: "jpy", // <== 日本円に変更
          product_data: {
            name: "T-shirt", // <== 商品名
            images: [
              "https://example.com/photo.jpg" // <== 商品画像
            ]
          },
          unit_amount: 2000, // <== 税込価格
        },
        quantity: 1, // <== 個数
      },
    ],
    mode: "payment",
    success_url: "https://example.com/success", // <== 決済完了時URL
    cancel_url: "https://example.com/cancel", // <== キャンセル時URL
  });

  res.json({ id: session.id }); // Stripeへのリダイレクトに使う
});

基本的には上記のような設定でサーバー側は完了です。 あらかじめCustomerProductを指定することも可能なのですが、とりあえず動かしてみたいのに最低限の記述だけで動いてくれるのはありがたいです。

カード番号について

カード番号 説明
4242 4242 4242 4242 Visaのダミーカード
4000 0000 0000 9995 決済に必ず失敗するカード

Stripeのアカウントを取得すると、金融機関の口座情報を登録しない場合はテストモードとして動作します。 かといって、カードの番号はなんでも良いわけではありません。 カード会社ごとに番号が用意されていますが、テストであればこの2種類で十分でしょう。 CVCとカードの有効期限も入力する必要がありますが、これらは任意の番号です。

Webhookについて

app.use(
  express.json({
    // Webhookの処理に必要
    verify: function (req, res, buf) {
      if (req.url === '/webhook') {
        req.rawBody = buf.toString();
      }
    },
  })
);

app.post('/webhook', (request, response) => {
  try {
    const event = stripe.webhooks.constructEvent(
      req.rawBody,
      req.headers['stripe-signature'],
      process.env.STRIPE_WEBHOOK_SECRET
    );

    if (event.type === 'checkout.session.completed') {
      console.log(`🔔  決済が完了しました。`);
      const data = event.data;
    }

    res.sendStatus(200);
  } catch (err) {
    console.log(`⚠️  Webhookの署名が認証に失敗しました。`);
    return res.sendStatus(400);
  }
});

Stripe側でユーザーが決済に成功したかどうかはサーバー側ではわからないので、Webhookのエンドポイントも追加します。 リダイレクトに使用したsession.idevent.dataに含まれているのでこの値をもとに決済情報を更新します。

ngrokのエラーについて

https://stackoverflow.com/questions/45425721/invalid-host-header-when-ngrok-tries-to-connect-to-react-dev-server

Invalid Host Header

Webhookの確認にngrokを利用していたのですがエラーが出ました。 Nginxなどでも見るエラーですが、コマンドの起動時に--host-header=rewriteをつければよいです。