0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

# Next.js + TypeScript で payjp.js実装してみた

Posted at

Next.js + TypeScript での Payjp.js実装してみた

クレカ支払い実装をpayjp.jsとNext.js + TypeScriptで実装する機会があったので、備忘録として記事にしてみました。

🚀 今回の実装に関わるライブラリ一覧

ライブラリ名 バージョン
next 14.2.15
react 18.3.1
typedef-payjp-js 1.2.0
@types/react 18.3.12
@types/react-dom 18.3.1
@types/node 20.17.7
typescript 5.7.2
tailwindcss 3.4.15

💳 Pay.jpとpayjp.jsとは?

Pay.jp は日本国内向けのオンライン決済サービスで、クレジットカード決済、サブスクリプション、Apple Pay / Google Pay などを安全かつ簡単に組み込めます。

その中核となるフロントエンドライブラリが payjp.js です。主な役割は:

  • クレジットカード情報を トークン化
  • 自社サーバにカード番号を送らず、安全に決済
  • PCI DSS準拠のセキュアな決済フロー実現

🔄 クレジットカード決済の仕組み

  1. ユーザーがWebサイトでクレジットカード情報を入力
  2. フロント側(payjp.js)がカード情報をトークン化
  3. バックエンドにトークンを送信
  4. バックエンドがトークンを使ってpay.jp APIに課金リクエスト
  5. Pay.jpがカード会社に与信確認
  6. 承認されると決済完了

この仕組みを使うことで、加盟店側はカード情報を直接保存・処理しなくてもよく、PCI DSSへの対応負担を大幅に軽減できます。

⚙️ Next.js + TypeScriptでのpayjp.js実装手順

1. ライブラリと型定義のインストール

npm install @payjp/payjp-js payjp
npm install --save-dev @types/payjp typedef-payjp-js

2. 環境変数の設定

.env.local に公開鍵と秘密鍵を設定:

NEXT_PUBLIC_PAYJP_PUBLIC_KEY=pk_test_xxxxxxxxx
PAYJP_SECRET_KEY=sk_test_xxxxxxxxx

3. カード入力フォームの作成(app/checkout/page.tsx)

"use client";

import React, { useEffect, useRef, useState } from "react";
import type PayjpJs from "typedef-payjp-js";

declare global {
  interface Window {
    Payjp?: (key: string) => PayjpJs.Payjp;
    __payjpInstance__?: PayjpJs.Payjp; // グローバルにインスタンス保持
  }
}

export default function CheckoutPage() {
  const [payjp, setPayjp] = useState<PayjpJs.Payjp | null>(null);
  const [ready, setReady] = useState(false);

  const numberRef = useRef<PayjpJs.PayjpElement | null>(null);
  const expiryRef = useRef<PayjpJs.PayjpElement | null>(null);
  const cvcRef = useRef<PayjpJs.PayjpElement | null>(null);

  // Payjpインスタンスを1回だけ作る
  useEffect(() => {
    if (typeof window === "undefined" || !window.Payjp) return;

    if (!window.__payjpInstance__) {
      const publicKey = "pk_test_token";
      window.__payjpInstance__ = window.Payjp(publicKey);
    }

    setPayjp(window.__payjpInstance__!);
    setReady(true);
  }, []);

  // カード要素を1回だけmountする
  useEffect(() => {
    if (!payjp || !ready) return;

    const elements = payjp.elements();

    if (!numberRef.current) {
      const numberElement = elements.create("cardNumber");
      numberElement.mount("#number-form");
      numberRef.current = numberElement;
    }

    if (!expiryRef.current) {
      const expiryElement = elements.create("cardExpiry");
      expiryElement.mount("#expiry-form");
      expiryRef.current = expiryElement;
    }

    if (!cvcRef.current) {
      const cvcElement = elements.create("cardCvc");
      cvcElement.mount("#cvc-form");
      cvcRef.current = cvcElement;
    }

    return () => {
      numberRef.current?.unmount();
      expiryRef.current?.unmount();
      cvcRef.current?.unmount();
      numberRef.current = null;
      expiryRef.current = null;
      cvcRef.current = null;
    };
  }, [payjp, ready]);

  const getToken = async () => {
    if (!payjp || !numberRef.current) {
      alert("カードフォームが初期化されていません");
      return;
    }

    const result = await payjp.createToken(numberRef.current);
    if (result.error) {
      alert(`エラー: ${result.error.message}`);
    } else {
      alert(`Token取得成功: ${result.id}`);
      // サーバーへの送信処理はここで書く
    }
  };

  return (
    <div className="max-w-md mx-auto p-4 bg-white rounded-lg shadow">
      <h2 className="text-xl font-bold mb-4">お支払い情報</h2>

      <div className="mb-4">
        <label className="block text-sm font-medium mb-1">カード番号</label>
        <div id="number-form" className="p-3 border rounded bg-gray-50" />
      </div>

      <div className="flex gap-4 mb-4">
        <div className="flex-1">
          <label className="block text-sm font-medium mb-1">有効期限</label>
          <div id="expiry-form" className="p-3 border rounded bg-gray-50" />
        </div>
        <div className="flex-1">
          <label className="block text-sm font-medium mb-1">セキュリティコード</label>
          <div id="cvc-form" className="p-3 border rounded bg-gray-50" />
        </div>
      </div>

      <button
        onClick={getToken}
        className="w-full p-3 bg-blue-600 hover:bg-blue-700 text-white rounded-md font-medium"
      >
        ¥3,000を支払う
      </button>
    </div>
  );
}

完成したUI

image.png

🛡️ セキュリティと実装上の注意点

必ず実装すべき対策

  • 環境変数管理: 公開鍵・秘密鍵を.envファイルで管理し、コードに直書きしない
  • サーバーサイド検証: 金額・商品情報をサーバー側で再検証(フロントの値を信用しない)
  • HTTPS強制: TLS1.2以上を使用し、すべての通信を暗号化
  • CORS設定: 適切なオリジン制限でクロスサイトリクエストを保護
  • エラーハンドリング: ユーザーに分かりやすいエラーメッセージを表示
  • 認証と認可: 支払い処理前にユーザーの認証状態を確認

推奨対策

  • 不正検知: 不自然な購入パターンの監視
  • ロギング: 決済処理の適切なログ記録(個人情報は除く)
  • タイムアウト設定: 長時間の処理を防止
  • Webhookの活用: 非同期決済状態の更新に対応
  • 依存パッケージ管理: 定期的なアップデートと脆弱性チェック

📋 リソース、参考にした記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?