Riot.jsでStripe.jsのPayment Elementなどの決済フォームを表示させる方法

この記事は、[Riot.js Advent Calendar 2022](https://qiita.com/advent-calendar/2022/riotjs) 4日目の記事です。

Riot.jsはシンプルなUIライブラリの１つで、バンドルの軽量さやカスタム要素などの機能を提供しています。

2022/11月時点で、Riot.jsでStripe.jsを利用するためのライブラリは公開されていない様子でした。

ここでは、Stripe.jsをRiot.jsで利用する方法を紹介します。

## Riot.jsプロジェクトを立ち上げる

まずはRiot.jsのプロジェクトを作成します。

```bash
$ mkdir riot-demo && cd riot-demo
$ npm init riot
```

対話形式で、`package.json`の各項目について聞かれます。

```bash
package name: (riot) demo-stripe-riotjs
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /Users/sandbox/riot/package.json:

{
  "name": "demo-stripe-riotjs",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

```

続いて、テンプレートを選択する画面が表示されます。

```bash
? Please select a template … 
❯ Webpack Project Template
  Parcel Project Template
  Rollup Project Template
  Simple Component
  SPA (Webpack) Project Template
  Custom Template (You will need to provide a template path to your template zip file)
```

今回は`Webpack Project Template`を選択しました。

以下のように、完了メッセージが表示されればOKです。

```bash
✔ Downloading the template files
✔ Unzipping the file downloaded
✔ Deleting the zip file
✔ Copying the template files into your project
✔ Deleting the temporary folder
✔ Template successfully created!
```

最後に、テンプレートから作成したプロジェクトのライブラリをインストールします。

```bash
$ npm install
```

`npm start`を実行すると、初期状態のアプリが起動します。

![スクリーンショット 2022-11-15 11.36.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/1dfd393a-e7e5-e1f0-9ea6-ebccd0d97406.png)


## Stripe.jsを読み込む

ここからはStripe.jsの組み込みを行います。

### ライブラリのインストール

まずは必要なライブラリを追加しましょう。

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

### Riotのライフサイクルで、Stripe.jsを読み込みする

続いて、Riotのライフサイクル内でライブラリを読み込みます。

DOMへのアクセスが発生するため、ここでは`onMounted`を利用します。

https://riot.js.org/ja/documentation/#riot-%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB

```js
<my-component>
  <p>{ props.message }</p>
  <form>
    <div id="payment-element"></div>
    <button type="submit">Pay now</button>
  </form>
  
  <script>
    import { loadStripe } from "@stripe/stripe-js"

    export default {
      onMounted(props, state) {
        // Stripe Dashboardから「公開可能キー」を取得して設定する
        loadStripe("pk_test_xxxxxx")
          .then(stripe => {
            this.update({
              stripe
            })
          })        
      },
    }
  </script>
</my-component>
```

決済フォームのSubmit処理などで`loadStripe`の戻り値を利用するため、Stateに格納しました。

### APIを呼び出して、Payment Intentを取得する

Stripe.jsのカード要素を表示するには、事前にPayment IntentまたはSetup Intent（カード情報などの保存のみする場合）を作成します。

StripeドキュメントのクイックスタートなどでAPIを用意しましょう。

https://stripe.com/docs/payments/quickstart

### 取得したPayment Intentで、決済フォームを表示する

Riot側では、`onMounted`でそのまま読み込みまで行います。

```diff_javascript
      onMounted(props, state) {
        loadStripe("pk_test_xxxxx")
          .then(stripe => {
            this.update({
              stripe
            })
+            return fetch("http://localhost:3100/stripe", {
+                 method: "POST"
+             })
+          })
+          .then(data => data.json())
+          .then(result => {
+            const elements = stripe.elements({
+              appearance: {
+                theme: "stripe"
+              },
+              clientSecret: result.client_secret // クイックスタートの場合、「result.clientSecret」
+            })
+            this.update({
+              elements
+            })
+            const paymentElement = elements.create("payment")
+            paymentElement.mount("#payment-element")
          })    
      },
```

これで決済フォームが表示されます。

![スクリーンショット 2022-11-15 13.53.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e29a2edb-613f-bf3f-db54-93141d88ed38.png)

なお、SSRが利用できる場合、サーバー側でPayment Intentを作成する方法も可能です。

https://riot.js.org/ja/documentation/#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%B5%E3%82%A4%E3%83%89%E3%83%AC%E3%83%B3%E3%83%80%E3%83%AA%E3%83%B3%E3%82%B0

### フォームのSubmit処理を実装しよう

最後に、カード情報を入力した後のSubmit処理を追加します。

まずは`form`に`onsubmit`イベントを設定しましょう。

```diff_html
-  <form>
+  <form onsubmit={ submit }>
```

https://riot.js.org/ja/documentation/#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9

その後、`script`タグ内にてSubmit処理を実装します。

```diff_javascript
    export default {
+      submit(e) {
+        e.preventDefault()
+        const { stripe, elements } = this.state
+        if (!stripe || !elements) return
+        stripe.confirmPayment({
+          elements,
+          confirmParams: {
+            return_url: "http://localhost:3000"
+          },
+          redirect: "if_required"
+        }).then(({ error, ...result }) => {
+          if (error) {  
+            console.log(error)
+            window.alert("Error")
+          } else {
+            console.log(result)
+            window.alert("Success")
+          }
+        })
+      },
      onMounted(props, state) {
```

Stripeが用意している「テストカード番号」を利用して、決済をテストしましょう。

https://stripe.com/docs/testing

`Success`が表示されれば、実装完了です。

![スクリーンショット 2022-11-15 13.58.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/e6baccdb-c461-f57f-e19c-5cfb290bcef0.png)

## 金額を親要素から渡したい場合は、propsを利用する

ECのカート機能などの金額が変化するケースでは、propsを利用して金額等を動的に設定できます。

```diff_javascript
      onMounted(props, state) {
        loadStripe("pk_test_xxxxx")
          .then(stripe => {
            this.update({
              stripe
            })
            return fetch("http://localhost:3100/stripe", {
                 method: "POST",
+                     body: JSON.stringify({
+                         amount: Number(props.amount)
+                     })
             })
          })
```

あとは`<div is="my-component">`や`<my-component>`などのコンポーネントタグに、`amount`プロパティを設定すればOKです。


```diff_html
-        <div is="my-component" data-riot-component message="Hello There">
+
        <div is="my-component" data-riot-component message="Hello There" amount="1000">
```

決済フォームを**表示させた後に金額を変更する**場合は、Payment Intentを**Update**して金額を変更する必要がある点にご注意ください。

https://stripe.com/docs/api/payment_intents/update