Stripeでリダイレクトなし決済を実装する方法|CheckoutとElementsの設定変更手順

「モーダルで決済機能を実装したいのにページ遷移してしまう」「SPAアプリで決済後にユーザーが元の画面に戻れない」と困っていませんか?実は、Stripeの標準設定ではリダイレクトが発生しますが、パラメータ変更により完全にリダイレクトを無効化できます。

この記事では、Stripe CheckoutとElementsの両方でリダイレクトを停止し、現在のページ内で決済を完結させる実装方法を、実際のコード例とともに詳しく解説します。

目次

Stripeのリダイレクト問題とは

通常の決済フローでのリダイレクト発生

Stripeで決済フォームを実装する場合、CheckoutとElements両方でリダイレクトが標準動作として組み込まれています。これは以下の理由によるものです:

  • 決済プロバイダによる認証ページへの遷移処理
  • 3Dセキュア認証などのセキュリティ要件への対応
  • 多様な決済手段への統一的な対応
  • 新しい決済手段追加時の対応コスト削減

通常の決済フローでは、以下のようにリダイレクトが発生します:

  1. ユーザーが決済情報を入力
  2. 決済プロバイダの認証ページへリダイレクト
  3. 認証完了後、指定したURLへ再リダイレクト
  4. 元のアプリケーションで決済完了処理

このフローは一般的なウェブサイトでは問題ありませんが、モーダルやSPAなどの特殊な環境では課題となります。

モーダル・SPA環境での課題

リダイレクトが問題となる具体的なケースは以下の通りです:

モーダルウィンドウでの決済:
ポップアップモーダル内で決済を実行する場合、リダイレクトにより親ページから離脱してしまい、ユーザーが元の操作状態に戻れなくなります。

Single Page Application(SPA):
ReactやVue.jsなどのSPAでは、URLまたはCookieで状態が管理されていない画面が存在し、一度離脱すると同じ画面状態を復元できません。

kintoneなどのビジネスアプリケーション:
既存のプラットフォーム上に決済機能を組み込む場合、ページ離脱により元のワークフローが中断されてしまいます。

フレーム内での実装:
iframeや特定のコンテナ内で決済機能を動作させる場合、リダイレクトによりレイアウトが崩れたり、予期しない動作が発生します。

リダイレクトオフの必要性

上記の課題を解決するため、Stripeではリダイレクトを無効化する機能が提供されています。ただし、以下の制限があることを理解する必要があります:

  • 対応決済手段の制限:リダイレクトが必須の決済手段は利用不可
  • セキュリティ機能の制限:一部の認証フローが利用できない可能性
  • 実装の複雑化:決済完了後の処理を自前で実装する必要

それでも、ユーザー体験の向上や特定のアプリケーション要件を満たすためには、リダイレクトオフの実装が必要不可欠です。

Stripe Checkoutでのリダイレクト制御

redirect_on_completion: ‘never’の設定方法

Stripe Checkoutでリダイレクトを停止するには、セッション作成時にredirect_on_completionパラメータを設定します。以下は基本的な実装例です:

// リダイレクトありの従来の実装(変更前)
const checkoutSession = await stripe.checkout.sessions.create({
  ui_mode: 'embedded',
  line_items: [{
    price: '{{PRICE_ID}}',
    quantity: 1,
  }],
  mode: 'payment',
  return_url: `${YOUR_DOMAIN}/return.html?session_id={CHECKOUT_SESSION_ID}`,
});

// リダイレクトなしの実装(変更後)
const checkoutSession = await stripe.checkout.sessions.create({
  ui_mode: 'embedded',
  line_items: [{
    price: '{{PRICE_ID}}',
    quantity: 1,
  }],
  mode: 'payment',
  redirect_on_completion: 'never', // この行を追加
  // return_urlは削除
});

重要なポイント:

  • return_urlの削除:redirect_on_completion: ‘never’を設定した場合、return_urlは不要
  • ui_mode: ‘embedded’:埋め込みモードでの使用が前提
  • 決済手段の自動制限:リダイレクト必須の決済手段は自動的に非表示

onCompleteコールバックの実装

リダイレクトを無効化した場合、決済完了後の処理をJavaScriptで自前実装する必要があります。以下は完全な実装例です:

<!DOCTYPE html>
<html>
<head>
  <title>Stripe Checkout Demo</title>
  <script src="https://js.stripe.com/v3/"></script>
</head>
<body>
  <h1>Stripe Checkout Demo</h1>
  <form id="payment-form">
    <div id="payment-element"></div>
  </form>
  
  <!-- 決済完了メッセージ(初期は非表示) -->
  <div id="success-message" style="display: none;">
    <h2>決済が完了しました!</h2>
    <p>ありがとうございます。</p>
  </div>

  <script>
    let elements;
    const stripe = Stripe('${STRIPE_PUB_API_KEY}');

    // Stripe Elementsを初期化
    stripe.initEmbeddedCheckout({
      fetchClientSecret: async () => "${checkoutSession.client_secret}",
      onComplete: async () => {
        if (!elements) return;
        
        // 決済フォームを非表示にする
        elements.destroy();
        
        // 成功メッセージを表示
        document.getElementById('payment-form').style.display = 'none';
        document.getElementById('success-message').style.display = 'block';
        
        // 必要に応じて追加の処理を実行
        // 例:分析イベントの送信、在庫更新API呼び出しなど
        console.log('決済完了処理を実行しました');
      }
    }).then(elm => {
      elements = elm;
      elements.mount('#payment-element');
    });
  </script>
</body>
</html>

実際のコード例

React環境での実装例:

import { useState, useEffect } from 'react';
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY);

function CheckoutForm({ sessionId }) {
  const [isCompleted, setIsCompleted] = useState(false);
  const [elements, setElements] = useState(null);

  useEffect(() => {
    const initializeCheckout = async () => {
      const stripe = await stripePromise;
      
      const checkoutElements = await stripe.initEmbeddedCheckout({
        fetchClientSecret: async () => sessionId,
        onComplete: async () => {
          // 決済完了時の処理
          if (elements) {
            elements.destroy();
          }
          setIsCompleted(true);
          
          // カスタム処理を実行
          await handlePaymentSuccess();
        }
      });
      
      checkoutElements.mount('#checkout-element');
      setElements(checkoutElements);
    };

    if (sessionId) {
      initializeCheckout();
    }

    // クリーンアップ
    return () => {
      if (elements) {
        elements.destroy();
      }
    };
  }, [sessionId]);

  const handlePaymentSuccess = async () => {
    // 決済成功後のカスタム処理
    try {
      await fetch('/api/payment-success', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sessionId })
      });
      console.log('決済完了処理が成功しました');
    } catch (error) {
      console.error('決済完了処理でエラーが発生:', error);
    }
  };

  if (isCompleted) {
    return (
      <div className="success-container">
        <h2>決済が完了しました!</h2>
        <p>ご購入ありがとうございます。</p>
        <button onClick={() => setIsCompleted(false)}>
          新しい決済を開始
        </button>
      </div>
    );
  }

  return <div id="checkout-element"></div>;
}

export default CheckoutForm;

Stripe Elementsでのリダイレクト制御

redirect: ‘if_required’の設定方法

Stripe Elementsでリダイレクトを制御するには、confirmPayment関数でredirectパラメータを設定します:

// リダイレクトありの従来の実装(変更前)
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: "http://localhost:4242/complete.html",
  },
});

// リダイレクトなしの実装(変更後)
const { error, paymentIntent } = await stripe.confirmPayment({
  elements,
  redirect: 'if_required' // この行を追加
  // confirmParamsは削除
});

設定のポイント:

  • confirmParamsの削除:redirect: ‘if_required’使用時は不要
  • 戻り値の変更:paymentIntentオブジェクトが返される
  • 条件付きリダイレクト:必要な場合のみリダイレクトが実行

confirmPaymentレスポンスの処理

リダイレクトを無効化した場合、confirmPaymentのレスポンスを適切に処理する必要があります:

const handleSubmit = async (event) => {
  event.preventDefault();

  if (!stripe || !elements) {
    return;
  }

  setIsLoading(true);

  try {
    const { error, paymentIntent } = await stripe.confirmPayment({
      elements,
      redirect: 'if_required'
    });

    if (error) {
      // エラー処理
      if (error.type === 'card_error' || error.type === 'validation_error') {
        setErrorMessage(error.message);
      } else {
        setErrorMessage('予期しないエラーが発生しました。');
      }
    } else if (paymentIntent) {
      // 決済状況に応じた処理
      switch (paymentIntent.status) {
        case 'succeeded':
          setSuccessMessage('決済が完了しました!');
          // 成功時の追加処理
          await handlePaymentSuccess(paymentIntent);
          break;
        case 'processing':
          setSuccessMessage('決済を処理中です...');
          break;
        case 'requires_payment_method':
          setErrorMessage('決済方法に問題があります。再度お試しください。');
          break;
        default:
          setErrorMessage('決済処理中に問題が発生しました。');
      }
    }
  } catch (err) {
    setErrorMessage('システムエラーが発生しました。');
    console.error('Payment error:', err);
  } finally {
    setIsLoading(false);
  }
};

エラーハンドリングの実装

完全なエラーハンドリングを含む実装例:

import { useState } from 'react';
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js';

function PaymentForm() {
  const stripe = useStripe();
  const elements = useElements();
  
  const [isLoading, setIsLoading] = useState(false);
  const [errorMessage, setErrorMessage] = useState('');
  const [successMessage, setSuccessMessage] = useState('');

  const handleSubmit = async (event) => {
    event.preventDefault();
    
    // Stripe.jsがロードされているかチェック
    if (!stripe || !elements) {
      return;
    }

    setIsLoading(true);
    setErrorMessage('');
    setSuccessMessage('');

    try {
      const { error, paymentIntent } = await stripe.confirmPayment({
        elements,
        redirect: 'if_required'
      });

      if (error) {
        // 詳細なエラーハンドリング
        handlePaymentError(error);
      } else if (paymentIntent) {
        // 成功時の処理
        handlePaymentSuccess(paymentIntent);
      }
    } catch (err) {
      setErrorMessage('システムエラーが発生しました。しばらく後に再度お試しください。');
      console.error('Unexpected error:', err);
    } finally {
      setIsLoading(false);
    }
  };

  const handlePaymentError = (error) => {
    switch (error.type) {
      case 'card_error':
        setErrorMessage(`カードエラー: ${error.message}`);
        break;
      case 'validation_error':
        setErrorMessage(`入力エラー: ${error.message}`);
        break;
      case 'invalid_request_error':
        setErrorMessage('リクエストに問題があります。サポートにお問い合わせください。');
        break;
      case 'api_connection_error':
        setErrorMessage('ネットワークエラーが発生しました。接続を確認してください。');
        break;
      case 'api_error':
        setErrorMessage('決済サービスでエラーが発生しました。しばらく後に再度お試しください。');
        break;
      case 'authentication_error':
        setErrorMessage('認証エラーが発生しました。');
        break;
      case 'rate_limit_error':
        setErrorMessage('リクエストが多すぎます。しばらく後に再度お試しください。');
        break;
      default:
        setErrorMessage('予期しないエラーが発生しました。');
    }
  };

  const handlePaymentSuccess = async (paymentIntent) => {
    setSuccessMessage('決済が完了しました!');
    
    // 追加の処理を実行
    try {
      // サーバーサイドでの処理通知
      await fetch('/api/payment-completed', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          paymentIntentId: paymentIntent.id,
          amount: paymentIntent.amount,
          currency: paymentIntent.currency
        })
      });
      
      // 必要に応じて他の処理を実行
      // 例:在庫更新、メール送信、ページ更新など
      
    } catch (error) {
      console.error('Post-payment processing error:', error);
      // エラーがあっても決済は完了しているので、ユーザーには成功メッセージを表示
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      
      {errorMessage && (
        <div className="error-message" style={{ color: 'red', marginTop: '10px' }}>
          {errorMessage}
        </div>
      )}
      
      {successMessage && (
        <div className="success-message" style={{ color: 'green', marginTop: '10px' }}>
          {successMessage}
        </div>
      )}
      
      <button 
        type="submit" 
        disabled={!stripe || isLoading}
        style={{
          backgroundColor: isLoading ? '#ccc' : '#007cff',
          color: 'white',
          padding: '12px 24px',
          border: 'none',
          borderRadius: '4px',
          marginTop: '20px',
          cursor: isLoading ? 'not-allowed' : 'pointer'
        }}
      >
        {isLoading ? '処理中...' : '決済を実行'}
      </button>
    </form>
  );
}

export default PaymentForm;

制限事項と注意点

利用できなくなる決済手段

リダイレクトを無効化すると、リダイレクトが必須の決済手段は自動的に利用不可となります:

決済手段 リダイレクトなしでの利用 備考
クレジットカード ✅ 利用可能 一般的なVisa、Mastercard等
デビットカード ✅ 利用可能 クレジットカードと同等
Apple Pay ✅ 利用可能 モバイル環境で有効
Google Pay ✅ 利用可能 Android環境で有効
銀行振込 ❌ 利用不可 銀行サイトへのリダイレクトが必要
PayPal ❌ 利用不可 PayPalサイトでの認証が必要
SEPA ❌ 利用不可 銀行認証ページが必要
コンビニ決済 ❌ 利用不可 決済代行会社サイトが必要

payment_method_typesでの制限:
手動で決済手段を指定している場合、リダイレクト必須の決済手段を含めるとエラーが発生します:

// ❌ エラーが発生する設定
const checkoutSession = await stripe.checkout.sessions.create({
  ui_mode: 'embedded',
  payment_method_types: ['card', 'paypal'], // paypalはリダイレクト必須
  redirect_on_completion: 'never', // リダイレクトを無効化
  // ... 他の設定
});

// ✅ 正しい設定
const checkoutSession = await stripe.checkout.sessions.create({
  ui_mode: 'embedded',
  payment_method_types: ['card'], // リダイレクト不要な決済手段のみ
  redirect_on_completion: 'never',
  // ... 他の設定
});

テスト時の確認ポイント

リダイレクト無効化実装時は、以下の点を必ず確認してください:

決済手段の動作確認:

  • 各種クレジットカードでの決済成功・失敗テスト
  • Apple Pay、Google Payでの動作確認
  • 3Dセキュア認証が必要なカードでのテスト
  • 決済金額やカード情報のバリデーション確認

エラーハンドリングの確認:

  • 無効なカード情報入力時の挙動
  • ネットワークエラー時の対応
  • 決済途中でのページリロード対応
  • 重複決済の防止機能

テスト用コード例:

// テスト用のカード番号(Stripe提供)
const testCards = {
  success: '4242424242424242',           // 成功
  decline: '4000000000000002',           // 拒否
  auth_required: '4000002500003155',     // 3Dセキュア認証必要
  insufficient_funds: '4000000000009995', // 残高不足
  processing_error: '4000000000000119',   // 処理エラー
};

// テスト実行関数
const runPaymentTests = async () => {
  for (const [testName, cardNumber] of Object.entries(testCards)) {
    console.log(`Testing: ${testName} with card ${cardNumber}`);
    
    try {
      // 各テストカードでの決済実行
      const result = await simulatePayment(cardNumber);
      console.log(`Result: ${result.status}`);
    } catch (error) {
      console.error(`Error in ${testName}:`, error);
    }
  }
};

パフォーマンスへの影響

リダイレクト無効化実装では、以下の点でパフォーマンスに影響する可能性があります:

メモリ使用量:
決済フォームがページ内に残存するため、長時間の利用でメモリリークが発生する可能性があります。適切なクリーンアップ実装が重要です:

// React例:適切なクリーンアップ
useEffect(() => {
  // 初期化処理
  const initializePayment = async () => {
    // ... 初期化コード
  };

  initializePayment();

  // クリーンアップ関数
  return () => {
    if (elements) {
      elements.destroy(); // Stripe Elementsを破棄
    }
    // その他のリソース解放
  };
}, []);

// Vue.js例:beforeUnmountでクリーンアップ
beforeUnmount(() => {
  if (this.elements) {
    this.elements.destroy();
  }
});

ネットワーク負荷:
ページ遷移がないため、定期的なセッション更新や状態確認APIの呼び出しが必要になる場合があります。適切な間隔での呼び出しを心がけましょう。

実装パターン別の選択指針

Checkout vs Elements の使い分け

リダイレクト無効化において、CheckoutとElementsの選択基準は以下の通りです:

比較項目 Stripe Checkout Stripe Elements
実装難易度 簡単 中程度
カスタマイズ性 限定的 高い
UI制御 Stripe提供のUI 完全にカスタム可能
機能範囲 豊富(税金、割引等) 基本的な決済のみ
保守性 Stripeが自動更新 自前で保守必要
モーダル適性

各アプローチのメリット・デメリット

Stripe Checkoutを選ぶべきケース:

  • 開発工数を最小限に抑えたい場合
  • 税金計算や割引機能が必要な場合
  • Stripeの最新機能を自動的に利用したい場合
  • セキュリティを最優先にしたい場合

Stripe Elementsを選ぶべきケース:

  • 決済フォームのデザインを完全に制御したい場合
  • 既存のUIフレームワークとの統合が必要な場合
  • 複雑な入力バリデーションが必要な場合
  • 決済フロー全体をカスタマイズしたい場合

実装判断フローチャート:

  1. モーダル内での実装が必要 → リダイレクト無効化を検討
  2. UI制御が重要 → Elements、簡単実装優先 → Checkout
  3. 既存システムとの統合度 → 高い場合はElements、標準的な場合はCheckout
  4. 開発リソース → 限定的な場合はCheckout、十分な場合はElements

ハイブリッドアプローチ:
段階的な実装も有効です。まずCheckoutでプロトタイプを作成し、必要に応じてElementsに移行するアプローチにより、リスクを最小化できます。

Stripeリダイレクト制御 よくある質問

❓ リダイレクトを無効化すると安全性は下がりますか?

基本的なセキュリティレベルは維持されます。ただし、一部の高度な認証フロー(銀行の追加認証等)は利用できなくなります。クレジットカード決済であれば、同等のセキュリティが確保されています。

❓ どの決済手段が使えなくなりますか?

銀行振込、PayPal、SEPA、コンビニ決済など、外部サイトでの認証が必要な決済手段は利用できません。クレジットカード、デビットカード、Apple Pay、Google Payは問題なく利用可能です。

❓ モーダル内での実装で注意すべき点は?

決済完了後のElements破棄、メモリリークの防止、エラー時のUI状態管理が重要です。また、モーダルを閉じる際の適切なクリーンアップ処理を必ず実装してください。

❓ 既存の実装を変更する際の影響は?

設定変更により一部の決済手段が利用不可になる可能性があります。事前に決済手段ごとのテストを実行し、ユーザーへの影響を確認してから本番適用することを強く推奨します。

まとめ:最適なUXを実現するリダイレクト制御

Stripeでリダイレクトを無効化する実装は、モーダルやSPAなどの現代的なウェブアプリケーション要件に対応する重要なテクニックです。適切な実装により、ユーザー体験を大幅に向上させることができます。

重要なポイントの再確認

  • Checkout:`redirect_on_completion: ‘never’` + `onComplete`コールバック
  • Elements:`redirect: ‘if_required’` + `paymentIntent`レスポンス処理
  • 制限事項:リダイレクト必須決済手段の利用不可
  • テスト重要:決済手段ごとの動作確認が必須
  • 適切なクリーンアップ:メモリリーク防止のためのリソース管理

実装成功のカギ

成功する実装のために、以下の順序で進めることを推奨します:

  1. 要件定義:必要な決済手段と制約の明確化
  2. アプローチ選択:CheckoutまたはElementsの決定
  3. プロトタイプ作成:基本機能の動作確認
  4. 包括的テスト:各種決済手段とエラーケースの確認
  5. 段階的リリース:部分的な展開による影響確認

特に「kintoneなどのビジネスアプリケーションに決済機能を埋め込む時のような、現在いるページからの離脱をできるだけ回避したいケース」では、リダイレクト制御が不可欠な機能となります。

適切な実装により、ユーザーがページを離脱することなく、スムーズな決済体験を提供できるでしょう。まずはテスト環境での検証から始めて、段階的に本番環境に適用することをお勧めします。

Stripeリダイレクト制御をさらに活用する関連記事

Stripeのリダイレクト制御を習得したら、他のStripe機能や開発効率化ツールも学んでより効果的な決済システムを構築しましょう:

Stripe決済システム関連

React・フロントエンド開発スキル

エラー対応・開発効率化

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次