payjp.js v2をReact+TypeScriptで使用する方法

## 前提

この記事は、payjp.jsをもともと使用していた人が、Reactでの実装方法を知りたいときに参考にすることを目的としています。payjp.jsの基本的な使い方は理解している前提です。

以下、pnpmを使用しますがnpmでもなんでも良いです。

## プロジェクトの作成

まずはViteでReactのプロジェクトを作成します。

```sh
pnpm create vite
```

## 型定義のインストール

プロジェクトのディレクトリに入って、有志の作成した型定義をインストールします。

```sh
pnpm add -D typedef-payjp-js
```

## 型定義のためのglobal.d.tsの用意

型定義を使用できるように、src下にglobal.d.tsを置きます（`@types`でもいいよ）。

```ts:global.d.ts
import type PayjpJs from "typedef-payjp-js";

/**
 * windowオブジェクトからpayjp.jsのAPIを参照
 */
declare global {
	interface Window {
		Payjp?: (
			publicKey: string,
			options?: { locale?: PayjpJs.Locale },
		) => PayjpJs.Payjp;
	}
}
```

## payjp.jsのスクリプトを追加

index.htmlにpayjp.jsのスクリプトを追加します。
（試してないけど、下記mount時にdynamic importもできるのかな？）

```diff_html:index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
+    <script src="https://js.pay.jp/v2/pay.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

## 邪魔なCSSなどの削除

```tsx:App.tsx
const App = () => {
  return (
    <div>
      <h1>Hello, React 18!</h1>
    </div>
  );
};

export default App;
```

わかりやすいように超シンプルな状態にしましょう。

```diff_tsx:main.ts
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
- import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

## App.tsxでpayjpを呼べるようにする

```diff_tsx:App.tsx
+ import type PayjpJs from "typedef-payjp-js";
+ const payjp = window?.Payjp?.("pk_test_token") as unknown as PayjpJs.Payjp;

const App = () => {
  return (
    <div>
      <h1>Hello, React 18!</h1>
    </div>
  );
};

export default App;
```

もちろん`payjpUtil.ts`みたいなファイルからexportしてもOK。
これ以後、`pk_test_token`はご自身の持つテストトークンにしてください。

## カード入力欄用のdivを作成

```diff_tsx:App.tsx
import type PayjpJs from "typedef-payjp-js";
const payjp = window?.Payjp?.("pk_test_token") as unknown as PayjpJs.Payjp;

const App = () => {
  return (
+   <div style={{ maxWidth: "400px", backgroundColor: "#eee" }}>
-   <div>
-    <h1>Hello, React 18!</h1>
+    <div id="v2-demo" />
    </div>
  );
};

export default App;
```

ついでにちょっとだけデザインも追加。

## cardElementの作成

```diff_tsx:App.tsx
import type PayjpJs from "typedef-payjp-js";
const payjp = window?.Payjp?.("pk_test_token") as unknown as PayjpJs.Payjp;

const App = () => {
+  let cardElement: PayjpJs.PayjpElement | null = null;
+  const handlePayDivMount = () => {
+    const elements = payjp.elements();
+    cardElement = elements.create("card");
+    cardElement.mount("#v2-demo");
+  };
  return (
    <div style={{ maxWidth: "400px", backgroundColor: "#eee" }}>
-     <div id="v2-demo" />
+     <div id="v2-demo" ref={handlePayDivMount} />
    </div>
  );
};

export default App;
```

:::note info
Reactのrefは関数を渡すことができて、mountされたときに関数を作動させることができます。もともとdivが存在しているのであればuseEffectなどでも処理可能ですが、たとえばモーダルを開いたときにdivが出現するような場合、useRefとuseEffectだとうまく動作しないことがあります。そういうときはrefで関数を呼ぶと良いです。ちなみに、今回は使用しませんが、関数の引数にはHTMLElementが入ります。
:::

ここまで進むと、以下の画像のようになっていると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679704/ff7a576e-927e-8ee9-dc05-3320db005d80.png)

これで大体OKです。残りはおまけ。

## tokenの取得

```tsx:App.tsx
import { useState } from "react";
import type PayjpJs from "typedef-payjp-js";
const payjp = window?.Payjp?.(
  "pk_test_token"
) as unknown as PayjpJs.Payjp;

const App = () => {
  const [token, setToken] = useState("");
  let cardElement: PayjpJs.PayjpElement | null = null;
  const handlePayDivMount = () => {
    const elements = payjp.elements();
    cardElement = elements.create("card");
    cardElement.mount("#v2-demo");
  };
  const handleGetToken = async () => {
    if (!cardElement) {
      alert("cardElement is not mounted");
      return;
    }
    const token = await payjp.createToken(cardElement);
    setToken(token.id);
  };

  return (
    <div style={{ maxWidth: "400px", backgroundColor: "#eee" }}>
      <div id="v2-demo" ref={handlePayDivMount} />
      <button type="button" onClick={handleGetToken}>
        get token
      </button>
      <div>token: {token}</div>
    </div>
  );
};

export default App;
```

buttonを押したらhandleGetToken関数が呼ばれ、トークンが生成されます。当たり前ですが、実際にアプリを作成する際にはtokenを表示しないように。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679704/c62ca953-0af9-fc59-06ba-6638dd31e13f.png)

上掲した画像のようになっていればOK

## まとめ

以上が基本的な使用方法となります。
デザインについては適宜styleで調整できます。このあたりはpayjp.jsのドキュメントをお読みください。
また、cardElementとして一体化したものではなく、カード番号入力欄、CVCなど個別になったものも同じように作成できます。

## おまけ

```tsx:App2.tsx
import type PayjpJs from "typedef-payjp-js";
const payjp = window?.Payjp?.("pk_test_token") as unknown as PayjpJs.Payjp;

const App2 = () => {
  let numberElement: PayjpJs.PayjpElement | null = null;
  let expiryElement: PayjpJs.PayjpElement | null = null;
  let cvcElement: PayjpJs.PayjpElement | null = null;

  const mounted = () => {
    const elements = payjp.elements();
    numberElement = elements.create("cardNumber", {
      style: {
        base: {
          color: "#fff",
          backgroundColor: "#000",
          fontSize: "20px",
        },
        invalid: {
          color: "#f00",
        },
      },
    });
    numberElement.mount("#number-form");
    expiryElement = elements.create("cardExpiry");
    expiryElement.mount("#expiry-form");
    cvcElement = elements.create("cardCvc");
    cvcElement.mount("#cvc-form");

    // Add event listener
    numberElement.on("change", (event) => {
      console.log(event);
    });
    expiryElement.on("change", (event) => {
      console.log(event);
    });
    cvcElement.on("change", (event) => {
      console.log(event);
    });
  };

  const getToken = async () => {
    if (!numberElement || !expiryElement || !cvcElement) {
      alert("element is not mounted");
      return;
    }
    const token = await payjp.createToken(numberElement);
    console.log(token);
  };
  return (
    <div style={{ maxWidth: "400px", backgroundColor: "#eee" }}>
      <div id="number-form" />
      <div id="expiry-form" />
      <div id="cvc-form" ref={mounted} />
      <button type="button" onClick={getToken}>
        get token
      </button>
    </div>
  );
};

export default App2;
```

こんな感じにするとnumber, expire, cvcを別個のフォームにできます。