kintoneプラグインにて、kintoneアプリにStripeの決済フォームを表示する方法 with kintone-ui-component

この記事は、[kintoneアドベントカレンダー](https://qiita.com/advent-calendar/2023/kintone)3日目の記事です。

https://qiita.com/advent-calendar/2023/kintone

kintoneでは、JavaScriptを利用してアプリの表示や動作を拡張・カスタマイズすることができる、拡張機能機能が用意されています。

https://kintone-sol.cybozu.co.jp/integrate/

拡張機能は、kintoneのスタンダードコースから利用できますが、動作確認や拡張機能の開発などの目的などであれば、[開発者ライセンス](https://cybozu.dev/ja/kintone/developer-license/)を利用して無料で試すこともできます。

## kintoneアプリに、決済フォームを直接組み込む

kintoneはJavaScriptでアプリの機能や表示をカスタマイズできます。そしてStripeでは、JavaScriptで決済フォームを表示するSDK（[@stripe/stripe-js](https://www.npmjs.com/package/@stripe/stripe-js)）を提供しています。

ということは、実装方法さえわかれば、プラグインを利用して、kintoneアプリ内にStripeが用意する決済フォームを埋め込むことも可能です！

![スクリーンショット 2023-11-22 13.23.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/f64d10e5-bf4a-a7c9-42c7-7ee16e8d3d77.png)

今回の記事では、**kintoneプラグインで、Stripeの決済フォームを表示する方法**を紹介します。

## kintoneプラグインで、Stripeの決済フォームを表示・動作させる４STEP

プラグインにStripeを組み込むには、次の４ステップが必要です。

まず、「決済フォームを表示させる場所」をアプリ内に用意しましょう。ここではkintone UI Componentライブラリを利用して、ダイヤログを表示させます。

続いてStripe APIをよびだし、決済処理に必要なリソース（Payment Intent）を生成します。

3つ目のステップで、Stripe ElementsをStripeのJavaScript SDKで表示させます。ここではフォームの表示のみですので、実際に決済処理などはまだ行われません。

最後のステップで、生成したPayment IntentとStripe Elementsの情報を利用し、決済を実施します。

それでは、一つずつ見ていきましょう。

### Step1: 決済フォームを表示するダイヤログを作成する

まずは決済フォームを表示させる場所の用意です。直接埋め込んでも良いのですが、今回はダイヤログに表示させましょう。


```javascript:src/js/desktop.js
import { Button, Dialog } from "kintone-ui-component";
const PLUGIN_ID = kintone.$PLUGIN_ID;

kintone.events.on("app.record.index.show", function () {
  const spaceElement = kintone.app.getHeaderSpaceElement();
  if (spaceElement === null) {
    throw new Error("The header element is unavailable on this page");
  }

  const buttonElement = new Button({
    text: "決済フォームを表示",
    type: "submit",
  });
  buttonElement.addEventListener("click", async function () {
    const paymentElementPlaceholder = document.createElement("div");
    paymentElementPlaceholder.id = "payment-element";
    const dialog = new Dialog({
      title: "Payment form",
      content: paymentElementPlaceholder,
    });
    dialog.open();
  });
  spaceElement.appendChild(buttonElement);

});
```

kintone UI Componentの`Button`で「ダイヤログ表示ボタン」を、`Dialog`で「ダイヤログそのもの」を実装しました。

コードをデプロイすると、プラグインを登録したアプリ（今回は見積書）の一覧画面に、[決済フォームを表示]ボタンが表示されます。

![スクリーンショット 2023-11-22 13.58.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/2caf6565-eb56-67b1-6f30-bc2b4c48e97f.png)

`Button`要素に`addEventListener`でクリックイベントを設定しています。その中でダイヤログ要素の作成と表示を行っているため、追加されたボタンをクリックすることで、ダイヤログが開きます。

![スクリーンショット 2023-11-22 13.58.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/ed87e80f-950f-1554-79f5-aaa2882ffa3b.png)

### Step2: kintone API Proxyを利用して、Stripe APIを呼び出す

決済フォームを表示する場所が用意できましたので、続いて決済処理に必要な情報をStripeで生成しましょう。

Stripeでは、決済を行うためのリソースとして「[Payment Intent](https://stripe.com/docs/payments/paymentintents/lifecycle) API」が用意されています。このAPIを利用して、クレジットカード / Apple Pay / Google Payなどのさまざまな支払い手段をサポートした決済フローを実装します。

#### StripeのシークレットAPIキーを安全に保存する
このAPIを利用するために、Stripeが発行するシークレットAPIキーを利用する必要があります。安全にシークレットAPIキーをkintone内で取り扱うための、「APIプロキシー機能」の設定方法などについては、以下の記事をお読みください。

https://qiita.com/hideokamoto/items/ba5dff7a0ec1cd91c648

今回は次のようなAPIキー入力フォームと、保存処理を追加しています。

```html:plugin/html/config.html
<section class="settings">
  <h2 class="settings-heading">Stripe API連携</h2>
  <p class="kintoneplugin-desc">Stripeと連携するためのAPIキーを設定します</p>
  <form class="js-submit-settings" id="js-submit-settings">
    <p class="kintoneplugin-row">
      <label for="message">
        Secret API KEY:
        <input type="text" id="stripe-secret-api-key" class="js-text-message kintoneplugin-input-text">
      </label>
    </p>
    <p class="kintoneplugin-row">
        <button type="button" class="js-cancel-button kintoneplugin-button-dialog-cancel">Cancel</button>
        <button class="kintoneplugin-button-dialog-ok" id="plugin-submit">Save</button>
    </p>
  </form>
</section>
```

```src/js/config.js
const PLUGIN_ID = kintone.$PLUGIN_ID;

const form = document.querySelector(".js-submit-settings");
const cancelButton = document.querySelector(".js-cancel-button");
const messageInput =
  document.querySelector<HTMLInputElement>(".js-text-message");
if (!(form && cancelButton && messageInput)) {
  throw new Error("Required elements do not exist.");
}

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const secretAPIKeyField = document.getElementById("stripe-secret-api-key");
  if (!secretAPIKeyField) return;
  const secretAPIKeyFieldValue = secretAPIKeyField.value;
  if (!secretAPIKeyFieldValue) return;
  const stripeAPIUrl = "https://api.stripe.com/v1";
  const requestHeader = {
    Authorization: `Bearer ${secretAPIKeyFieldValue}`,
  };
  kintone.plugin.app.setProxyConfig(stripeAPIUrl, "GET", requestHeader, {});
  kintone.plugin.app.setProxyConfig(
    stripeAPIUrl,
    "POST",
    {
      ...requestHeader,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    {}
  );
  alert("Saved");
});
cancelButton.addEventListener("click", () => {
  window.location.href = "../../" + kintone.app.getId() + "/plugin/";
});

```

#### 保存したAPIキーを利用したAPIリクエストを作成する

APIキーを利用できるようになりましたので、支払いを処理するためのデータ（[Payment Intent](https://stripe.com/docs/payments/paymentintents/lifecycle)）を作成しましょう。

`決済フォームを表示`ボタンを作成していますので、このボタンをクリックした際に、StripeのAPIを呼び出してPayment Intentを作成します。


```diff_javascript:src/js/desktop.js
  const buttonElement = new Button({
    text: "決済フォームを表示",
    type: "submit",
  });
  buttonElement.addEventListener("click", async function () {
+    const paymentIntentRequest = new URLSearchParams("");
+    paymentIntentRequest.set("amount", "1000");
+    paymentIntentRequest.set("currency", "jpy");
+    const paymentIntent = await new Promise((resolve, reject) => {
+      kintone.plugin.app.proxy(
+        kintone.$PLUGIN_ID,
+        "https://api.stripe.com/v1/payment_intents",
+        "POST",
+        {},
+        paymentIntentRequest.toString(),
+        (response) => {
+          const result = JSON.parse(response);
+          resolve(result);
+        },
+        (e) => reject(e)
+      );
+   });
    const paymentElementPlaceholder = document.createElement("div");
    paymentElementPlaceholder.id = "payment-element";
    const dialog = new Dialog({
      title: "Payment form",
      content: paymentElementPlaceholder,
    });
    dialog.open();
  });
```

StripeのAPIをkintoneのproxyで呼び出す場合、リクエストの本文（body）は`URLSearchParams`を利用して設定します。下にリクエスト内容を設定している部分のコードだけピックアップしました。

```javascript
    const paymentIntentRequest = new URLSearchParams("");
    paymentIntentRequest.set("amount", "1000");
    paymentIntentRequest.set("currency", "jpy");
```

このコードでは、「価格(`amount`)が`1000`」「通貨（`currency`）が`jpy`」のPayment Intentを作成しています。


どのようなデータが生成されるかを確認したい場合、`Dialog`要素の`content`を次のように変更してみましょう。

```diff_javascript
    const dialog = new Dialog({
      title: "Payment form",
      content: paymentElementPlaceholder,
      content: JSON.stringify(result, null, 2),
    });
```

kintoneアプリでボタンをクリックすると、次のようなJSONを表示するダイアログが確認できます。

![スクリーンショット 2023-11-27 11.45.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/8dc64564-253a-7f6e-a778-7ec223205312.png)


### Step3: Stripe Elementsで、決済フォームを表示する

ここからは、StripeのJavaScript SDKを利用して決済フォームを埋め込みましょう。

決済フォームを表示するには、[StripeのJavaScript SDK](https://www.npmjs.com/package/@stripe/stripe-js)を利用します。npmコマンドでライブラリをインストールしましょう。

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

`loadStripe`関数を利用しますので、インポートします。

```diff_javascript:src/js/desktop.js
import { Button, Dialog } from "kintone-ui-component";
+import { loadStripe } from "@stripe/stripe-js";
```

その後、Payment Intentを作成した処理の後に、インポートした`loadStripe`を呼び出しましょう。

```diff_javascript:src/js/desktop.js
    const paymentIntent = await new Promise((resolve, reject) => {
      kintone.plugin.app.proxy(
        kintone.$PLUGIN_ID,
        "https://api.stripe.com/v1/payment_intents",
        "POST",
        {},
        paymentIntentRequest.toString(),
        (response) => {
          const result = JSON.parse(response);
          resolve(result);
        },
        (e) => reject(e)
      );
   });
+    const stripe = await loadStripe(
+      "pk_test_から始まる、Stripeの公開可能キーを設定する"
+    );
+    if (!stripe) throw new Error("Failed to load Stripe client");
    const paymentElementPlaceholder = document.createElement("div");

```

JS SDKのセットアップを`loadStripe`で行いますので、セットアップに失敗した場合（戻り値が`null`の場合）用の処理も入れておきましょう。

:::note
**Stripeの公開可能キーは、プラグインの設定情報に保存しよう**
今回は便宜上プラグインコードに直接、Stripeの公開可能キーを記述しました。
しかしkintoneでは、プラグインごとに設定情報を保存する機能が用意されています。
実際の案件やプラグイン開発では、`kintone.plugin.app.setConfig`を利用しましょう。

https://cybozu.dev/ja/kintone/docs/js-api/plugins/set-config/

:::

セットアップに成功した後は、決済フォームを表示する処理を追加します。

```diff_javascript:src/js/desktop.js
    if (!stripe) throw new Error("Failed to load Stripe client");

    const paymentElementPlaceholder = document.createElement("div");
    paymentElementPlaceholder.id = "payment-element";
    const dialog = new Dialog({
      title: "Payment form",
      content: paymentElementPlaceholder,
    });
    dialog.open();
+    const elements = stripe.elements({
+      clientSecret: paymentIntent.client_secret,
+      appearance: {
+        theme: "stripe",
+      },
+    });
+    const paymentElement = elements.create("payment", {
+      layout: {
+        type: "accordion",
+        defaultCollapsed: false,
+        radios: false,
+        spacedAccordionItems: true,
+      },
+    });
+    paymentElement.mount("#payment-element");
```

Stripeでは、「すでにあるHTML要素に、決済フォームをマウントする」形で決済フォームを埋め込みます。そのため、`mount()`関数は`dialog.open()`処理を実行してから呼びだすようにしましょう。

変更をデプロイすると、ボタンをクリックした際のダイアログに、決済フォームが表示されます。
![スクリーンショット 2023-11-27 12.00.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/40d5c5c6-5ed2-475f-23c4-72957392d191.png)


### Step4: 決済を完了させよう

最後に、決済フォームに入力されたカード情報などを利用して、決済を完了させる処理を作りましょう。

まずは、注文操作を行うためのボタンをダイアログに追加します。

```diff_javascript:src/js/desktop.js
+    const paymentFormButton = new Button({
+      text: "注文する",
+      type: "submit",
+    });
    const paymentElementPlaceholder = document.createElement("div");
    paymentElementPlaceholder.id = "payment-element";
    const dialog = new Dialog({
      title: "Payment form",
      content: paymentElementPlaceholder,
+      footer: paymentFormButton,
    });
    dialog.open();
```

追加したボタンのクリックイベントを設定しましょう。

```diff_javascript:src/js/desktop.js
    dialog.open();
    paymentElement.mount("#payment-element");

+    paymentFormButton.addEventListener("click", async () => {
+      const confirmResult = await stripe.confirmPayment({
+        elements,
+        redirect: "if_required",
+      });
+      console.log(confirmResult);
+      alert(`決済を完了しました。決済ID: ${confirmResult?.paymentIntent?.id}`);
+      dialog.close();
+    });
```

決済フォームの動作を確認するには、ドキュメントに公開されている「テスト用カード番号」を利用します。

https://stripe.com/docs/testing

テスト用のカード情報をフォームに入力し、注文ボタンをクリックしてみましょう。
処理に成功した場合、決済成功のメッセージが表示され、ダイアログが閉じられます。

![スクリーンショット 2023-11-27 12.05.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2366300/81ded888-f97f-b4b8-aff2-afbbd92e4497.png)

これでkintoneアプリ内に、決済フォームを埋め込む作業が完了です。

## 終わりに

kintoneはJavaScriptを利用してアプリをカスタマイズできます。そのため、StripeのJavaScript系SDKと、APIを利用することで、決済や請求業務に関するカスタマイズや自動化も、kintone上で行うことが可能です。

特にkintoneでは、ゲストユーザー向けのスペースを用意することもできます。そのため、ゲストユーザーがアプリ内で注文や決済を行えるようなシステムを作りたい場合、今回のような決済フォームを埋め込む実装が役に立ちます。

https://kintone.cybozu.co.jp/price/guestprice.html

そのほかにも、請求書処理や、支払いリンクの作成など、様々な連携サンプルについても紹介しています。
こちらも併せてごらんください。

https://qiita.com/hideokamoto/items/a050230728bba2d8ab7d

https://qiita.com/hideokamoto/items/62c006779c10d0d3ce18

https://qiita.com/hideokamoto/items/a8ce64552f3ff8c666b7