stripeを用いて決済画面を作成する場面がありましたので、備忘録としてまとめます。
後から「時間があれば」画像等を追加して、細かい説明を作成したいと、思っています😅
フロント側も「別途時間ができた際」に掲載しようとは思いますが、基本的には各種必要なものをbodyにセットして、今回作成するエンドポイントにリクエストを送るだけになります(雑)
下記リンクからアカウントを作成します。
stripeアカウント作成
登録完了後、環境を「テスト環境」に変更し、「商品カタログ」より商品を追加します。
npm install 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 });
});
今回は、決済した情報をあらかじめ作成したuserテーブルのpointカラムに追加していきたい形となりますのでstripeが提供してくれている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側でエンドポイントを作成します。
データベースは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)となります。
参考文献は下記になります。
執筆者の方、本当にありがとうございます、このエラー解消で一気に進みました。
こんな感じで、一通りのアイテム取得処理->アイテム購入処理->webhook処理が完了となりました。