StripeとReactで、銀行振込やコンビニ決済などの複数の決済手段を表示する方法と見た目のカスタマイズ方法

この記事は、[React Advent Calendar 2022](https://qiita.com/advent-calendar/2022/react) 14日目の記事です。

Stripeでは、Checkout / Payment Linksなどのリダイレクト型の決済フォームとは別に、埋め込み型の決済フォーム「Elements」を提供しています。

https://stripe.com/jp/payments/elements

Stripe.jsやiOS/Android SDKを利用してElementsをサイトやアプリに埋め込むことで、提供するサービスの中で決済体験を完結させることができます。

## Elements組み込みの前準備（サーバー側でのPayment Intent作成）

Stripe Elementsでの決済フォーム組み込みには、サーバー側の処理が必要です。

```js
    const stripe = new Stripe(STRIPE_SECRET_KEY, {
        apiVersion: '2022-11-15'
    });
    const paymentIntent = await stripe.paymentIntents.create({
        amount: 200,
        currency: 'jpy',
    })
```

このため、JavaScriptでアプリを構築する場合は、Next.js(React)やNuxt(Vue)、SvelteKit(Svelte)などのサーバー側の処理をサポートするフレームワークを利用するか、Express・NestJSなどで別途APIを用意する必要があります。

Express.jsの場合、次のようなAPIを用意しましょう。

```js
const express = require("express");
const app = express();
const stripe = require("stripe")('sk_testから始まるStripeシークレットAPIキー（テスト環境用）');

app.use(express.static("public"));
app.use(express.json());

app.post("/create-payment-intent", async (req, res) => {
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1400,
    currency: "jpy",
    automatic_payment_methods: {
      enabled: true,
    },
  });

  res.send({
    clientSecret: paymentIntent.client_secret,
  });
});

app.listen(4242, () => console.log("Node server listening on port 4242!"));
```

生成した`clientSecret`を利用して、Elementsを描画します。

## Elementsで複数決済に対応した決済フォームを用意する

ReactでElementsを利用するには、２つのnpmモジュールを利用します。

```bash
$ npm i @stripe/stripe-js @stripe/react-stripe-js
```

前のステップで作成したAPIから`clientSecret`を取得し、インストールしたモジュールのセットアップを行います。

```jsx
import { loadStripe } from '@stripe/stripe-js'
import { Elements } from '@stripe/react-stripe-js'
import { useEffect, useRef, useState } from 'react'

export function StripePaymentElements() {
  const hasAPICalledRef = useRef(false)
  const [piClientSecret, setPiClientSecret] = useState('')
  
  useEffect(() => {
    if (hasAPICalledRef.current) return
    hasAPICalledRef.current = true
    // APIのURLに適宜変更する
    fetch('http://localhost:3000/create-payment-intent', {
      method: 'post'
    }).then(data => data.json())
      .then(response => setPiClientSecret(response.clientSecret))
  }, [])

  if (!piClientSecret) return null;
  return (
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY as string)}
      options={{
        clientSecret: piClientSecret,
      }}
    >
      { //@TODO Next Step}
    </Elements>
  )
}
```

続いて決済フォーム本体のコンポーネントを作成します。

```jsx
import { PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js'

export function StripePaymentForm() {
  const stripe = useStripe()
  const elements = useElements()

  return (
      <form
        style={{marginTop: '20px'}}
        onSubmit={async e => {
          e.preventDefault()
          if (!stripe || !elements) return
          const result = await stripe.confirmPayment({
            elements,
            redirect: 'if_required',
          })
          console.log(result)
        }}
      >
          <PaymentElement />
          <button type='submit'>注文する</button>
      </form>
  )
}
```

モジュールのセットアップで作成した`StripePaymentElements`要素に、決済フォーム要素（`StripePaymentForm`）を追加しましょう。

```diff_javascript
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY as string)}
      options={{
        clientSecret: piClientSecret,
      }}
    >
-      { //@TODO Next Step}
+       <StripePaymentForm />
    </Elements>
```

決済フォームが表示されれば、完了です。

![スクリーンショット 2022-11-21 16.21.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/eb350dfb-389a-05b9-7929-b9dca618b9ff.png)

Stripeダッシュボードから、コンビニ決済などの決済手段を有効化すると、`<PaymentElement />`内で顧客が利用可能な決済手段を動的に表示してくれるようになります。

### [Appendix] 銀行振込をサポートする方法

銀行振込をサポートするには、Customerデータの作成が必要です。

サーバー側のPayment Intent作成処理を、次のように変更しましょう。

```diff_javascript
  const paymentIntent = await stripe.paymentIntents.create({
    amount: 1400,
    currency: "jpy",
    automatic_payment_methods: {
      enabled: true,
    },
+    customer: 'cus_xxx',
+    payment_method_data: {
+      type: 'customer_balance'
+    },
+    payment_method_options: {
+      customer_balance: {
+        funding_type: 'bank_transfer',
+        bank_transfer: {
+          type: 'jp_bank_transfer'
+        }
+      },
+    }
  });
```

customerのIDは、1度作成してCookieやユーザー管理系サービス（firebase/Auth0など）で再利用できるようにすることをお勧めします。

### 決済手段をサーバー側で動的に変更する方法

商品やサービスの種類によって決済手段を変更したい場合などでは、サーバー側の`Payment Intent`作成処理で明示的に指定することも可能です。

以下の例では、「クレジットカード」「コンビニ決済」「銀行振込」の3つをサポートします。

```js
    const paymentIntent = await stripe.paymentIntents.create({
        customer: 'cus_xxxx',
        amount: 200,
        currency: 'jpy',
        payment_method_types: ['card', 'konbini', 'customer_balance'],
        payment_method_data: {
          type: 'customer_balance'
        },
        statement_descriptor_suffix: 'Food delivery',
        payment_method_options: {
          customer_balance: {
            funding_type: 'bank_transfer',
            bank_transfer: {
              type: 'jp_bank_transfer'
            }
          },
          card: {
            statement_descriptor_suffix_kanji: '食料品配送',
            statement_descriptor_suffix_kana: 'ショクリョウヒンハイソウ',
          },
          konbini: {
              product_description: "食料品配送",
              expires_after_days: 7
          }
        },
    })
```

## 決済フォームのレイアウトを変更する方法

複数の決済手段が利用できる場合、デフォルトでは決済手段の選択ボタンが横に並びます。

![スクリーンショット 2022-11-21 17.31.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/92fe801b-be81-4dfe-fef7-3e9b8d5551c9.png)

### 縦並び（アコーディオン）配置にする

`PaymentElement`のパラメータを変更することで、縦並びに変更できます。

```diff_javascript
         <PaymentElement
+           options={{
+             layout: {
+               type: 'accordion',
+             }
+           }}
         />
```

![スクリーンショット 2022-11-21 17.32.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/b70c6721-4770-3149-acce-6a83c72133bf.png)

### 縦並び配置（ラジオボタンなし）にする
`type: 'accordion'では、`ラジオボタンを非表示にすることもできます。

```diff_javascript
         <PaymentElement
            options={{
              layout: {
                type: 'accordion',
+               radios: false,
              }
            }}
         />
```


![スクリーンショット 2022-11-21 17.34.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d5613b67-c090-40c5-5069-bd5e8d8b2558.png)

### 縦並びで、デフォルト表示を無くす

デフォルトでは通常、クレジットカードの決済フォームがはじめに表示されます。

縦並びの場合、これを非表示にすることもできます。

```diff_javascript
         <PaymentElement
            options={{
              layout: {
                type: 'accordion',
+               defaultCollapsed: true,
+               spacedAccordionItems: true
              }
            }}
         />
```

![スクリーンショット 2022-11-21 17.35.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/13d79df9-d332-5009-f57f-423e2deb3f71.png)

顧客が利用したい決済フォームを選択すると、フォームが表示されます。

![スクリーンショット 2022-11-21 17.35.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/a89ba4fa-486b-ca94-6109-a1b1de4ebdd7.png)

## 決済フォームの見た目をカスタマイズする方法

Stripe Elementsで描画される決済フォームは、セキュリティなどの目的で`iframe`の中に配置されます。

![スクリーンショット 2022-11-21 17.56.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/21cdc8ed-bc94-08a2-2d0f-52c3c7cb3535.png)

そのため、CSSでの内容・見た目のカスタマイズは**Element Appearance API**を利用して行う必要があります。

https://stripe.com/docs/elements/appearance-api

### プリセットスタイル（テーマ）を設定する

StripeのPayment ELementsでは、いくつかのスタイルプリセットが用意されています。

![スクリーンショット 2022-11-21 17.58.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/d38e6024-2640-e495-ebc5-abcdbccf25b0.png)
[ドキュメントサイトでデモを見る](https://stripe.com/docs/elements/appearance-api)

これらは、`<Elements/>`の`options.appearance`で設定します。

```js
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
          theme: 'stripe'
        },
        clientSecret: piClientSecret,
      }}
    >
```

### プリセットを使わずに見た目を調整する

独自のデザインを設定したい場合、`theme: 'none'`を設定することで、装飾がほぼない状態にできます。

```diff_javascript

    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY)}
      options={{
        appearance: {
-          theme: 'stripe'
+          theme: 'none'
        },
        clientSecret: piClientSecret,
      }}
    >
```

![スクリーンショット 2022-11-21 18.01.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/78501cf5-444f-b356-734c-a22969a2e194.png)

続いて、`options.appearance.variables`と`options.appearance.rules`で見た目を調整します。

```jsx
    <Elements
      stripe={loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_API_KEY as string)}
      options={{
        appearance: {
          theme: 'none',
          variables: {
            colorPrimary: '#333333',
            colorBackground: '#ffffff',
            colorText: '#30313d',
            colorDanger: '#654321',
            fontFamily: 'Osaka',
            spacingUnit: '10px',
            borderRadius: '4px',
            spacingTab: '100px',
            spacingGridRow: '50px',
            spacingGridColumn: '50px'
          },
          rules: {
            '.Tab': {
              border: '1px solid #E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02)',
            },      
            '.Tab:hover': {
              color: 'var(--colorText)',
            },
            '.Tab--selected': {
              borderColor: '#E0E6EB',
              boxShadow: '0px 1px 1px rgba(0, 0, 0, 0.03), 0px 3px 6px rgba(18, 42, 66, 0.02), 0 0 0 2px var(--colorPrimary)',
            },
            '.Input': {
              boxShadow: '0 1px 1px 0 rgba(0, 0, 0, 0.07), 0 0 0 2px var(--colorPrimary)',
            },      
            '.Input--invalid': {
              boxShadow: '0 1px 1px 0 rgba(0, 0, 0, 0.07), 0 0 0 2px var(--colorDanger)',
            },
          }
        },
        clientSecret: piClientSecret,
      }}
    >
```

![スクリーンショット 2022-11-21 18.12.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/173a993f-df80-e917-20ff-ede9883774bb.png)

利用可能な変数やルール、クラス名の法則などについてはドキュメントをご確認ください。

https://stripe.com/docs/elements/appearance-api?platform=web#variables