PAY.JPの決済フォームを独自デザインで作る

この記事は「[オンライン決済サービスPAY.JPを使ってみた情報をシェアしよう！ by PAY Advent Calendar 2024](https://qiita.com/advent-calendar/2024/payjp-01) 」の14日目の記事です

## はじめに

PAY.JPはチェックアウトライブラリを用いることで、`<script>` タグを一行書くだけで完成済みの決済フォームを実装することができます。
https://pay.jp/docs/checkout

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457851/b9caf94e-4678-9172-5129-992a8c7e4206.png)

非常に洗練されたデザインで、かつモーダルで表示されるので、自社プロダクトにスピーディーに組み込むことができそうです。
しかし、プロダクトによってはモーダルではなく既存の画面に組み込みたい, 自社のサービスのデザインルールに則るようにスタイリングを調整したいという要件は一般的によく発生します。

そんな時のために、この記事ではPAY.JPの決済フォームを独自デザインで作った時の記録を紹介しようと思います。

## 作ったもの

![pay.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/457851/770648fa-b5cb-52ad-1993-bdb0a6b0c006.gif)

- `<input>`にフォーカスがあるときは枠を緑色にする
- `<input>`がエラー状態のときは背景と枠を赤色にする

## 実装

Reactで実装しました。
基本的には以下のドキュメントだけ読めば、実装を進められると思います。
https://pay.jp/docs/payjs

```tsx
import { useState } from "react";
import type PayjpJs from "typedef-payjp-js";

const payjp = window.Payjp?.(
  "pk_test_xxxxxxxxxxxxxx"
) as PayjpJs.Payjp;

const App = () => {
  const [token, setToken] = useState("");

  let cardNumber: PayjpJs.PayjpElement | null = null;
  let cardExpiry: PayjpJs.PayjpElement | null = null;
  let cardCvc: PayjpJs.PayjpElement | null = null;

  const handleCardElementMounted = () => {
    const elements = payjp.elements();
    cardNumber = elements.create("cardNumber", {
      placeholder: "4242 4242 4242 4242",
    });
    cardExpiry = elements.create("cardExpiry", {
      placeholder: "12 / 29",
    });
    cardCvc = elements.create("cardCvc", {
      placeholder: "123",
    });
    cardNumber.mount("#card-number");
    cardExpiry.mount("#card-expiry");
    cardCvc.mount("#card-cvc");
  };

  const handleSubmit = async () => {
    if (!cardNumber) {
      alert("予期せぬエラーが発生しました。");
      return;
    }
    const token = await payjp.createToken(cardNumber);
    setToken(token.id);
  };

  return (
    <div id="payment-form" className="payment-form">
      <div>
        <div>カード番号</div>
        <div id="card-number" className="card-element" />
      </div>
      <div>
        <div>有効期限</div>
        <div id="card-expiry" className="card-element" />
      </div>
      <div>
        <div>セキュリティ番号</div>
        <div
          id="card-cvc"
          className="card-element"
          ref={handleCardElementMounted}
        />
      </div>
      <button
        id="submit-button"
        className="submit-button"
        onClick={handleSubmit}
      >
        get token
      </button>
      <div>token: {token}</div>
    </div>
  );
};

export default App;
```

CSSは以下のように実装しました。
https://pay.jp/docs/payjs#element-container に`Element`の状態によって付与されるクラスの一覧が記述されています。これを元にスタイリングしていきます
```css
.payment-form {
  display: grid;
  gap: 8px;
  max-width: 400px;
  margin: 50px auto;
  padding: 20px;
  background: #f9f9f9;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.card-element {
  margin-top: 4px;
  border-radius: 5px;
  background: white;
  border: 1px solid #c2c8d2;
  border-radius: 4px;
  padding: 12px;
}

.PayjpElement--focus {
  border-color: #6ee7b7;
}

.PayjpElement--invalid {
  border-color: #dc2626;
  background-color: #fee2e2;
}

.submit-button {
  width: 100%;
  padding: 10px;
  margin-top: 20px;
  background: #4caf50;
  color: white;
  border: none;
  border-radius: 5px;
  font-size: 16px;
  cursor: pointer;
}

.submit-button:hover {
  background: #45a049;
}
```

## 終わりに

最後まで読んでいただきありがとうございました！

今回PAY.JPを色々触ってみた感想としては、ドキュメントが非常に読みやすく開発者に優しいサービスだと感じました。
また、決済フォームに関してはフォームの状態が常にDOMのクラスに反映され、またJavaScriptからも細かく取得できるようになっているので自社の要件に合った決済フォームを実装しやすいと感じました。


開発体験が非常によかったので、個人開発等で決済機能が必要になったときは積極的に使っていきたいと思っています。

## 参考資料

- https://pay.jp/docs
- https://pay.jp/docs/payjs