PayPal Javascript SDK v6 使ったシンプル処理実装の提案(Gemini3 pro)

ご提示いただいたドキュメント(SDK v6)に基づき、セキュリティ(堅牢性)UX(ユーザー体験)メンテナンス性のバランスが取れた、最も標準的かつ最適化された実装プランを提案します。

この構成は、機密情報を守りつつ、ユーザーの環境に合わせて最適な決済方法(PayPal, Pay Later等)を自動で出し分ける「推奨実装」です。

決済処理シーケンス図
mermaid-diagram-2025-12-01-172703

フローの重要ポイント解説

図の中の番号に対応した詳細な解説です。

1. 初期化 (ステップ 1-2)

  • トークン取得: SDK v6では、セキュリティのためにサーバー経由で取得した「クライアントトークン」を使ってSDKを起動します。
  • Eligibility Check: ページロード時にPayPalと通信し、「このユーザーには後払いを表示すべきか?」などを判定します。これにより、無駄なボタンが表示されるのを防ぎます。

2. 注文作成 (ステップ 3-4)

  • ここがセキュリティの要です。
  • ユーザーがボタンを押した際、フロントエンドは「1000円で注文作成して」とは言いません。「カートID: A123 の注文を作って」とだけサーバーに伝えます。
  • サーバーはデータベースから正しい金額を引き出し、PayPalに注文を作成します。これにより、悪意あるユーザーによる金額の書き換えを完全に防ぎます。

3. ユーザー承認 (ステップ 5)

  • presentationMode: "auto" の設定により、PayPal SDKが最適な方法(通常はポップアップ)でログイン画面を出します。
  • ユーザーが承認しても、まだお金は動きません(Authorizationの状態)。

4. 決済確定 (ステップ 6-7)

  • フロントエンドで onApprove(承認完了)を受け取ったら、即座にバックエンドへ「確定 (Capture)」を依頼します。
  • バックエンドがPayPalへ最終的な通信を行い、成功が返ってきて初めて「支払い完了」となります。

実装アーキテクチャの概要

フロントエンドだけで完結させず、必ずバックエンド(サーバー)を経由させます。

  1. フロントエンド: UI表示、ボタンの出し分け、決済フローの起動。
  2. バックエンド: PayPal認証、注文データの作成(金額計算)、決済の確定。

1. バックエンドの実装 (Node.js / Express 例)

セキュリティのため、「金額の計算」と「API認証情報の管理」は必ずサーバー側で行います。

必要なエンドポイントは以下の3つです。

  1. GET /api/paypal/token: フロントエンド用SDKの起動トークンを発行
  2. POST /api/orders: 注文を作成(金額確定)
  3. POST /api/orders/:orderId/capture: 決済を確定
// サーバーサイド (例: Node.js + Express)
const express = require('express');
const app = express();
// ※実際にはPayPalのアクセストークン取得ロジックが必要です

// 1. フロントエンド初期化用のクライアントトークンを返す
app.get('/api/paypal/token', async (req, res) => {
  // PayPal API (/v1/identity/generate-token) を呼んでトークンを取得
  const clientToken = await generatePayPalClientToken(); 
  res.json({ clientToken });
});

// 2. 注文作成 (Create Order)
app.post('/api/orders', async (req, res) => {
  // 【重要】金額はフロントから受け取らず、DBの商品ID等からサーバー側で計算する
  const orderPayload = {
    intent: "CAPTURE",
    purchase_units: [{
      amount: { currency_code: "USD", value: "100.00" } // 計算済みの金額
    }]
  };

  // PayPal API (/v2/checkout/orders) をコール
  const response = await createPayPalOrder(orderPayload);

  // v6 SDKの仕様に合わせて { orderId: "..." } を返す
  res.json({ orderId: response.id });
});

// 3. 決済確定 (Capture Order)
app.post('/api/orders/:orderId/capture', async (req, res) => {
  const { orderId } = req.params;

  // PayPal API (/v2/checkout/orders/{id}/capture) をコール
  const response = await capturePayPalOrder(orderId);
  res.json(response);
});

2. フロントエンドの実装 (HTML + JavaScript)

SDK v6の最大の特徴である「Eligibility(利用資格)チェック」を取り入れ、ユーザーに利用可能なボタンだけを表示する最適化されたコードです。

HTML

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>PayPal Checkout</title>
  <style>
    /* ボタンのコンテナ: 最初は非表示にしておく */
    .buttons-container {
      display: flex;
      flex-direction: column;
      gap: 12px;
      max-width: 300px; /* 適切な幅に制限 */
    }
  </style>
</head>
<body>
  <h1>お支払い</h1>

  <div class="buttons-container">
    <!-- 初期状態は hidden。JSで資格を確認してから表示する -->
    <paypal-button type="pay" hidden></paypal-button>
    <paypal-pay-later-button hidden></paypal-pay-later-button>
    <paypal-credit-button hidden></paypal-credit-button>
  </div>

  <!-- アプリケーションのロジック -->
  <script src="app.js"></script>

  <!-- PayPal SDKの読み込み (非同期) -->
  <script
    async
    src="https://www.sandbox.paypal.com/web-sdk/v6/core"
    onload="initPayPal()"
  ></script>
</body>
</html>

JavaScript (app.js)

async function initPayPal() {
  try {
    // 1. サーバーからクライアントトークンを取得
    const { clientToken } = await fetch("/api/paypal/token").then(res => res.json());

    // 2. SDKインスタンスの作成
    const sdkInstance = await window.paypal.createInstance({
      clientToken,
      components: ["paypal-payments"], // 決済コンポーネントを指定
      pageType: "checkout",
    });

    // 3. 利用可能な決済方法を判定 (Eligibility Check)
    const paymentMethods = await sdkInstance.findEligibleMethods({
      currencyCode: "USD", // 決済通貨を指定
    });

    // 共通のコールバック設定 (承認、キャンセル、エラー)
    const sessionOptions = {
      async onApprove(data) {
        console.log("承認されました:", data);
        // サーバーに決済確定をリクエスト
        await fetch(`/api/orders/${data.orderId}/capture`, { method: "POST" });
        alert("支払いが完了しました!");
      },
      onCancel(data) {
        console.log("キャンセルされました");
      },
      onError(error) {
        console.error("エラー発生:", error);
      }
    };

    // 4. 利用可能なボタンのみセットアップして表示

    // --- PayPal (通常) ---
    if (paymentMethods.isEligible("paypal")) {
      const session = sdkInstance.createPayPalOneTimePaymentSession(sessionOptions);
      const btn = document.querySelector("paypal-button");

      btn.removeAttribute("hidden"); // ボタンを表示
      btn.addEventListener("click", () => {
        // presentationMode: "auto" で最適な表示(ポップアップ等)を自動選択
        session.start({ presentationMode: "auto" }, createOrderFromServer());
      });
    }

    // --- Pay Later (後払い) ---
    if (paymentMethods.isEligible("paylater")) {
      const session = sdkInstance.createPayLaterOneTimePaymentSession(sessionOptions);
      const details = paymentMethods.getDetails("paylater");
      const btn = document.querySelector("paypal-pay-later-button");

      // Pay Later固有の設定
      btn.productCode = details.productCode;
      btn.countryCode = details.countryCode;

      btn.removeAttribute("hidden");
      btn.addEventListener("click", () => {
        session.start({ presentationMode: "auto" }, createOrderFromServer());
      });
    }

    // (必要であれば PayPal Credit も同様に追加)

  } catch (error) {
    console.error("SDK初期化エラー:", error);
  }
}

// サーバーに注文作成を依頼するヘルパー関数
function createOrderFromServer() {
  return fetch("/api/orders", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    // カートの中身IDなどを送る (金額は送らない)
    body: JSON.stringify({ cartId: "my-cart-123" }) 
  })
  .then(res => res.json())
  // SDK v6 は { orderId: "..." } の形式を期待している
  .then(data => ({ orderId: data.orderId }));
}

解説:なぜこれが「最適化」されているのか?

1. UXの最適化 (Eligibility Check)

従来のSDKでは、使えないボタンが表示されてエラーになったり、無駄なスペースを取ることがありました。 この実装では、findEligibleMethods を使い、ユーザーの国や通貨、デバイス状況に合わせて、本当に使えるボタンだけを表示します(例:日本ユーザーにはPay Laterを出さない、など)。

2. 表示モードの自動化 (presentationMode: "auto")

presentationMode: "auto" を指定することで、SDKが状況を判断します。

  • 通常はユーザー体験の良い「ポップアップ」を開きます。
  • ポップアップブロッカーがある場合などは、自動的に「モーダル」などに切り替わる可能性があります。 これにより、開発者がブラウザごとの挙動を細かく管理する必要がなくなります。

3. セキュリティ (Server-Side Order Creation)

フロントエンドのJavaScriptで amount: 100.00 と書くのは危険です(ユーザーがブラウザの開発者ツールで金額を書き換えられるため)。 提案コードでは、createOrderFromServer 関数を通じてサーバーに「カートID」だけを送り、サーバー側で正しい金額を計算してPayPalに注文を作成しています。

4. パフォーマンス (Async Loading)

SDKスクリプトに async 属性をつけ、読み込み完了後に onload で初期化関数を呼ぶことで、ページの描画ブロックを防いでいます。