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

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

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

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

**`npm start`で開発用ローカルサーバーを起動します。**
![image-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2001657/2c5ee255-9c4d-5ae1-3777-9f1e5156d05e.jpeg)

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

## サーバーサイドの処理
sever.jsを作成し、expressでローカルサーバーを立ち上げるためのコードを記述します。
```javascript
const express  = require(express);
const app = express();
const PORT = 3000;

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

package.jsonファイルの"scripts"セクションに、Expressサーバーを起動するための新しいスクリプトを追加します。
```javascript
"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の公式ドキュメントからコピペ出来ます。各セクションの説明も丁寧にされていてとても便利です。

https://docs.stripe.com/billing/quickstart?client=react

```javascript
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2001657/dccaf4fb-6c01-92b4-cea1-406472eb2d4a.jpeg)

CSS実装後　↓
![image-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2001657/4820519f-9f50-1cf6-7739-eb26be48517b.jpeg)

## Stripeで商品を追加する
Stripeの公式サイトの「商品カタログ」から商品を追加します。
![image-4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2001657/03bfc9b5-cc78-4c87-eb00-d0bdee4f8a5c.png)

## サーバー処理を記述
以下のコードの説明も公式ドキュメントに詳細に書かれています。

```javascript
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で作成した商品の情報を取得します。
```javascript
 const prices = await stripe.prices.list({
    lookup_keys: [req.body.lookup_key],
    expand: ['data.product'],
  });
```
console.logすると以下のように表示されます。
```javascript
{
  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を指定
```javascript
success_url: `${YOUR_DOMAIN}/?success=true&session_id{CHECKOUT_SESSION_ID}`,
cancel_url: `${YOUR_DOMAIN}?canceled=true`,
```

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