ちょっと話題の記事

Stripeのオンライン決済を実装する時はChargesよりPayment Intentsを使おう #Stripe

Charges APIからPayment Intents APIへの移行方法を解説します。
2020.07.21

まずStripeのオンライン決済を実装するには3通りの方法があります。

  • Stripe Checkout: Stripeが提供する支払いページにリダイレクトして決済する
  • Charges API: Charges APIを呼んで自前の支払いフォームで決済する
  • Payment Intents API: Payment Intents APIを呼んで自前の支払いフォームで決済する

ChargesPayment Intents 、どちらを使っても自前の支払いフォームでの支払いフローを構築できます。
しかし、Chargesの方は今後は拡張せずPayment Intentsの製品開発に集中することがこちらのページで説明されています。
なのでできればPayment Intentsの方を使いましょう。

補足ですが、移行は推奨されていますが2020/07/21現在Payment IntentsにChargesの全ての機能が移行されているわけではありません。
Charges自体の廃止が決まっているわけではないので要件に応じてChargesを使うことも検討してください。
例えば、Alipay・WeChat PayはPayment Intentsではサポートされていませんが、Chargesではサポートされています。

ChargesからPayment Intentsへの移行

Payment Intentsへ移行するには、クライアント側とサーバー側両方変更する必要があります。

ざっくり移行手順

  1. APIバージョンとライブラリのバージョンを更新する
  2. アプリ内で使っているChargesのプロパティを置き換える
    • APIを変更する前に読み取るプロパティを置き換えることで読み取り後の処理が保証されます。
  3. (クライアント側)ソースをPayment Intentsへ移行
  4. (サーバー側)カード登録フローを統合
  5. (サーバー側)決済フローを統合
  6. 移行後のフローでテスト

アプリ内で使っているChargesのプロパティを置き換える

段階的に移行するためにChargeオブジェクトにpayment_method_detailsとbilling_detailsが追加されています。
charge.sourcecharge.payment_method_details, charge.billing_detailsに置き換えましょう。

よく使われているプロパティはこちらです。

プロパティ Charges AFTER
請求に使用した支払い方法の詳細 charge.source charge.payment_method_details
請求に使用された支払い方法のID charge.source.id charge.payment_method
使用された支払い方法のタイプ charge.source.object charge.payment_method_details.type
請求の請求情報(例:請求郵便番号) charge.source.address_zip charge.billing_details.address.postal_code
カード所有者の名前 charge.source.name charge.billing_details.name
使用したカードの下4桁 charge.source.last4 charge.payment_method_details.card.last4
カードの指紋 charge.source.fingerprint charge.payment_method_details.card.fingerprint
請求のCVC検証ステータス charge.source.cvc_check charge.payment_method_details.card.checks.cvc_check
カードブランド charge.source.brand charge.payment_method_details.card.brand
Google PayとAndroid Pay charge.source.tokenization_method is android_pay card.wallet.type within charge.payment_method_details is google_pay

カードを登録して支払うフロー

カード情報の送信(クライアント側)

旧APIのcreateTokencreateSourceconfirmCardSetupに置き換えます。

stripe.confirmCardSetup(
  clientSecret,
  {
    payment_method: {
      card: cardElement,
      billing_details: {name: cardholderName.value}
    }
  }
).then(function(result) {
  if (result.error) {
    // Display error.message in your UI.
  } else {
    // The SetupIntent was successful!
  }
});

成功するとカード情報をID化したPaymentMethodが返却されるので、それをサーバーに送信します。

カードを登録(サーバー側)

クライアント側で作成したPaymentMethodをカスタマーに追加します。

既存カスタマーの場合

const customer = await stripe.paymentMethods.attach(
  'pm_1FWS6ZClCIKljWvsVCvkdyWg',
  {customer: 'cus_HgIgWLxUAsvn0A'},
);

新規カスタマーの場合

カスタマー登録はCharges APIの時と変わりありません。

const customer = await stripe.customers.create({
  email: 'jenny.rosen@example.com',
  payment_method: 'pm_1FWS6ZClCIKljWvsVCvkdyWg',
  invoice_settings: {
    default_payment_method: 'pm_1FWS6ZClCIKljWvsVCvkdyWg', // デフォルトの支払いに設定
  },
});

確認

const paymentMethods = await stripe.paymentMethods.list(
  { customer: 'cus_HgIgWLxUAsvn0A', type: 'card' }
);

決済処理

customerを指定してpaymentIntentを作成します。confirmはデフォルトで分離されているので省略したい場合はfalseを指定してください。

const paymentIntent = await stripe.paymentIntents.create({
        amount: 550,
        currency: 'JPY',
        customer:  'cus_HgIgWLxUAsvn0A',
        confirm: true
})

上で作成されたpaymentIntentを確定

const paymentIntent = await stripe.paymentIntents.capture('pi_1EUnBFF5IfL0eXz9XzeK6PqQ');

1回限りの決済フロー

アプリケーションに会員登録しないまま決済をする場合、もちろんStripe側にもカスタマー登録はされていないのでフローが変わってきます。

決済情報を作成(サーバー側)

サーバー側で決済情報を作成します。

const paymentIntent = await stripe.paymentIntents.create({
  amount: 1099,
  currency: 'jpy',
});

カード情報(クライアント側)

confirmCardPaymentを使ってstripeに直接カード情報を送ります。

stripe.confirmCardPayment(
  'pi_1EUnBFF5IfL0eXz9XzeK6PqQ',
  {
    payment_method: {card: cardElement}
  }
).then(function(result) {
  if (result.error) {
    // Display error.message in your UI.
  } else {
    // The payment has succeeded
    // Display a success message
  }
});

webhooksで決済の結果を取得(サーバー側)

confirmCardPaymentで直接Stripeでの決済を作成するので加盟店サーバーの方でwebhookを受け取って決済結果を取得します。

app.use(require('body-parser').text({type: '*/*'}));

const endpointSecret = 'whsec_...';

app.post('/webhook', function(request, response) {
  const sig = request.headers['stripe-signature'];
  const body = request.body;

  let event = null;

  try {
    event = stripe.webhooks.constructEvent(request.body, sig, endpointSecret);
  } catch (err) {
    // invalid signature
    response.status(400).end();
    return;
  }

  let intent = null;
  switch (event['type']) {
    case 'payment_intent.succeeded':
      intent = event.data.object;
      console.log("Succeeded:", intent.id);
      break;
    case 'payment_intent.payment_failed':
      intent = event.data.object;
      const message = intent.last_payment_error && intent.last_payment_error.message;
      console.log('Failed:', intent.id, message);
      break;
  }

  response.sendStatus(200);
});