1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React ✕ Node.js ✕ Stripeで決済システムを構築

Last updated at Posted at 2024-05-02

Stripeとは

Stripe (ストライプ)は、オンライン決済や取引を簡素化するためのプラットフォームです。主にウェブやモバイルアプリケーションで利用され、クレジットカードやデビットカード、その他の決済手段を受け入れるためのAPIやツールを提供しています。

Stripeは、開発者が独自のウェブサイトやアプリケーションに決済機能を統合するのを容易にします。StripeのAPIを使用することで、カスタムの支払いフォームを作成したり、定期支払いや再発行、返金などの機能を組み込んだりすることができます。

環境設定

npx create-react-app コマンドは、Reactプロジェクトを作成するための公式のツールであり、簡単にReactアプリケーションのテンプレートを作成するために使用されます。

npx create-react-app react-stripe-payment

npm startで開発用ローカルサーバーを起動します。
image-1.jpg

必要なフレームワーク等をインストールします。
今回はStripeの他にExpressを使用します。ExpressはバックエンドのWebアプリケーションを構築するためのフレームワークです。

npm i express stripe

サーバーサイドの処理

sever.jsを作成し、expressでローカルサーバーを立ち上げるためのコードを記述します。

const express  = require(express);
const app = express();
const PORT = 3000;

app.listen(PORT, console.log("サーバーが起動しました!"));

package.jsonファイルの"scripts"セクションに、Expressサーバーを起動するための新しいスクリプトを追加します。

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "node server.js"
  },
npm run server

これにより、server.jsファイル内のExpressアプリケーションが起動し、指定されたポートでリクエストを待ち受けることができます。

サブスクリプションページの構築

以下はStripeの公式ドキュメントからコピペ出来ます。各セクションの説明も丁寧にされていてとても便利です。

import React, { useState, useEffect } from 'react';
import './App.css';

const ProductDisplay = () => (
  <section>
    <div className="product">
      <Logo />
      <div className="description">
        <h3>スタータープラン</h3>
        <h5>2,000 / </h5>
      </div>
    </div>
    <form action="/create-checkout-session" method="POST">
      {/* Add a hidden field with the lookup_key of your Price */}
      <input type="hidden" name="lookup_key" value="{{PRICE_LOOKUP_KEY}}" />
      <button id="checkout-and-portal-button" type="submit">
        お申し込みはこちら
      </button>
    </form>
  </section>
);

const SuccessDisplay = ({ sessionId }) => {
  return (
    <section>
      <div className="product Box-root">
        <Logo />
        <div className="description Box-root">
          <h3>Subscription to starter plan successful!</h3>
        </div>
      </div>
      <form action="/create-portal-session" method="POST">
        <input
          type="hidden"
          id="session-id"
          name="session_id"
          value={sessionId}
        />
        <button id="checkout-and-portal-button" type="submit">
          Manage your billing information
        </button>
      </form>
    </section>
  );
};

const Message = ({ message }) => (
  <section>
    <p>{message}</p>
  </section>
);

export default function App() {
  let [message, setMessage] = useState('');
  let [success, setSuccess] = useState(false);
  let [sessionId, setSessionId] = useState('');

  useEffect(() => {
    // Check to see if this is a redirect back from Checkout
    const query = new URLSearchParams(window.location.search);

    if (query.get('success')) {
      setSuccess(true);
      setSessionId(query.get('session_id'));
    }

    if (query.get('canceled')) {
      setSuccess(false);
      setMessage(
        "Order canceled -- continue to shop around and checkout when you're ready."
      );
    }
  }, [sessionId]);

  if (!success && message === '') {
    return <ProductDisplay />;
  } else if (success && sessionId !== '') {
    return <SuccessDisplay sessionId={sessionId} />;
  } else {
    return <Message message={message} />;
  }
}

const Logo = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    xmlnsXlink="http://www.w3.org/1999/xlink"
    width="14px"
    height="16px"
    viewBox="0 0 14 16"
    version="1.1"
   >
    <defs />
    <g id="Flow" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
      <g
        id="0-Default"
        transform="translate(-121.000000, -40.000000)"
        fill="#E184DF">
      <path
        d="M127,50 L126,50 C123.238576,50 121,47.7614237 121,45 C121,42.2385763 123.238576,40 126,40 L135,40 L135,56 L133,56 L133,42 L129,42 L129,56 L127,56 L127,50 Z M127,48 L127,42 L126,42 C124.343146,42 123,43.3431458 123,45 C123,46.6568542 124.343146,48 126,48 L127,48 Z" 
        id="Pilcrow"
       />
      </g>
     </g>
    </svg>
);

ローカルサーバーを確認するとこのように表示されています。後は、適宜CSSを実装すればそれっぽくなります。
image-2.jpg

CSS実装後 ↓
image-3.jpg

Stripeで商品を追加する

Stripeの公式サイトの「商品カタログ」から商品を追加します。
image-4.png

サーバー処理を記述

以下のコードの説明も公式ドキュメントに詳細に書かれています。

const stripe = require('stripe')('sk_test_51OwJ4iKhKNkDMmE5r76y79tcC6ZRlb3xiWVBK25mC67XaRieiskJaZb0gdpqOm2xSj7FzeTGlMTt1VJxZrLX9cAS00RE3jMvzE');
const express = require("express");
const app = express();
app.use(express.static("public"));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

const YOUR_DOMAIN = "http://localhost:3000";

app.post("/create-checkout-session", async (req, res) => {
  try {
    const prices = await stripe.prices.list({});
    const session = await stripe.checkout.sessions.create({
      billing_address_collection: "auto",
      line_items: [
        {
          price: prices.data[0].id,
          quantity: 1,
        },
      ],
      mode: "subscription",
      success_url: `${YOUR_DOMAIN}/?success=true&session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${YOUR_DOMAIN}?canceled=true`,
    });

    res.redirect(303, session.url);
  } catch (err) {
    console.log(err);
  }
});

app.post("/create-portal-session", async (req, res) => {
  const { session_id } = req.body;
  const checkoutSession = await stripe.checkout.sessions.retrieve(session_id);
  const returnUrl = YOUR_DOMAIN;

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: checkoutSession.customer,
    return_url: returnUrl,
  });

  console.log(portalSession.url);
  res.redirect(303, portalSession.url);
});

app.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  (request, response) => {
    const event = request.body;

    const endpointSecret = "whsec_12345";
    if (endpointSecret) {
      const signature = request.headers["stripe-signature"];
      try {
        event = stripe.webhooks.constructEvent(
          request.body,
          signature,
          endpointSecret
        );
      } catch (err) {
        console.log(`⚠️  Webhook signature verification failed.`, err.message);
        return response.sendStatus(400);
      }
    }
    let subscription;
    let status;
    switch (event.type) {
      case "customer.subscription.trial_will_end":
        subscription = event.data.object;
        status = subscription.status;
        console.log(`Subscription status is ${status}.`);
        break;
      case "customer.subscription.deleted":
        subscription = event.data.object;
        status = subscription.status;
        console.log(`Subscription status is ${status}.`);
        break;
      case "customer.subscription.created":
        subscription = event.data.object;
        status = subscription.status;
        console.log(`Subscription status is ${status}.`);
        break;
      case "customer.subscription.updated":
        subscription = event.data.object;
        status = subscription.status;
        console.log(`Subscription status is ${status}.`);
        break;
      default:
        console.log(`Unhandled event type ${event.type}.`);
    }
    response.send();
  }
);

app.listen(3000, () => console.log("Running on port 3000"));

価格の取得

Stripeで作成した商品の情報を取得します。

 const prices = await stripe.prices.list({
    lookup_keys: [req.body.lookup_key],
    expand: ['data.product'],
  });

console.logすると以下のように表示されます。

{
  object: 'list',
  data: [
    {
      id: 'price_1OwJtbKhKNkDMmE5f54dntNO',
      object: 'price',
      active: true,
      billing_scheme: 'per_unit',
      created: 1710920951,
      currency: 'jpy',
      custom_unit_amount: null,
      livemode: false,
      lookup_key: null,
      metadata: {},
      nickname: null,
      product: 'prod_PlrcniV5S70YeZ',
      recurring: [Object],
      tax_behavior: 'unspecified',
      tiers_mode: null,
      transform_quantity: null,
      type: 'recurring',
      unit_amount: 2000,
      unit_amount_decimal: '2000'
    }
  ],
  has_more: false,
  url: '/v1/prices'
}

成功時 / キャンセル時のURLを指定

success_url: `${YOUR_DOMAIN}/?success=true&session_id{CHECKOUT_SESSION_ID}`,
cancel_url: `${YOUR_DOMAIN}?canceled=true`,

上記のコード例では、ReactとNode.jsを使用してStripeを統合し、安全性と利便性を兼ね備えた決済処理システムを実装しました。Stripeを活用することで、安全な決済処理を簡単かつ迅速に実現できます。
このような実装は、開発者が安全で信頼性の高い決済処理を提供するための基盤となります。StripeのAPIを使用することで、支払いセッションの作成やポータルセッションの管理など、決済処理に関連する機能を簡単に実装することができます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?