😺

Migrate from Sources and Tokens APIs to PaymentMethods API

2023/12/24に公開

This post is a part of YAMAP Engineers Advent Calendar 2023.
https://qiita.com/advent-calendar/2023/yamap-engineers

Introduction

If you're dealing with a legacy system that utilizes Stripe, chances are it's relying on the Sources and Tokens APIs to manage credit card information. However, the current recommended approach involves using the PaymentMethods API in conjunction with the PaymentIntents and SetupIntents APIs. In this post, I'll share my experience transitioning to these new APIs.

Differences between the old and new APIs

Old workflow

In my scenario, when a user registers a credit card, the frontend system generates a token and sends it to the backend. The backend then saves this token as the customer.default_source.

When the user pays a PaymentIntent or creates a Subscription, Stripe automatically uses the default_source as the payment method.

New flow

With the new APIs, if you wish to register a credit card before making a payment, you need to employ SetupIntents.

Alternatively, you can save the card after the user has made a payment using a PaymentIntent with the setup_future_usage parameter. The flow is essentially the same as with the SetupIntent, but the key difference is that when the frontend confirms the PaymentIntent, the user completes the payment. A helpful tip here is that when you want a PaymentIntent with multiple payment method options (credit card, konbini, bank transfer, etc.), you can set the setup_future_usage within the payment_method_options parameter.

Stripe::PaymentIntent.create(
  amount: 2000,
  currency: 'jpy',
  customer: 'cus_NffrFeUfNV2Hib',
  payment_method_types: ['card', 'konbini'],
  payment_method_options: {
    card: { setup_future_usage: 'off_session' }
  }
)

In the new workflows, the card is saved as a PaymentMethod object. To make it the customer's default payment method, you need to update the customer's invoice_settings.default_payment_method.

Stripe::Customer.update(
  'cus_NffrFeUfNV2Hib',
  invoice_settings: {
    default_payment_method: payment_intent.payment_method
  }
)

Note that invoice_settings.default_payment_method is only automatically used for Invoice and Subscription payments. For other PaymentIntents, you'll need to set it manually.

Gradual migration

For legacy systems, replacing all instances using the old APIs can be challenging, especially when multiple teams are involved (web frontend, mobile frontend, backend, etc.). Luckily, Stripe allows for a gradual approach.

In existing card-saving scenarios, the backend can continue to receive credit card tokens. Instead of saving them to customer.default_source, create a PaymentMethod from the token, attach it to the Customer, and update the Customer's invoice_settings.default_payment_method. Additionally, remove other payment methods, including the default_source.

payment_method = Stripe::PaymentMethod.create(
  type: 'card', card: { token: params[:credit_card_token] }
)
Stripe::PaymentMethod.attach(
  payment_method.id, customer: 'cus_NffrFeUfNV2Hib'
)
Stripe::Customer.update(
  'cus_NffrFeUfNV2Hib',
  invoice_settings: { default_payment_method: payment_method.id }
)
Stripe::Customer.list_payment_methods(
  'cus_NffrFeUfNV2Hib',
  type: 'card'
).data.each do |pm|
  next if pm == payment_method.id
  
  Stripe::PaymentMethod.detach(pm.id)
end

For places creating Subscriptions, if the default_source parameter isn't set, you won't need to change anything. Stripe will automatically use customer.invoice_settings.default_payment_method. If none is available, it will fallback to customer.default_source.

For places confirming PaymentIntents, Stripe won't automatically use the invoice_settings.default_payment_method. You'll need to manually implement the payment method setting. Keep in mind that old customers still use default_source, so only use default_payment_method if it's available.

customer = Stripe::Customer.retrieve('cus_NffrFeUfNV2Hib')
payment_intent_params = {
  amount: 1000,
  currency: 'jpy',
  customer: 'cus_NffrFeUfNV2Hib',
  confirm: true,
}.tap do |params|
  payment_method = customer.invoice_settings.default_payment_method
  params[:payment_method] = payment_method if payment_method
end
Stripe::PaymentIntent.create(**payment_intent_params)

During the migration period, new card registrations will go to invoice_settings.default_payment_method, gradually replacing existing default_sources. Both default_source and default_payment_method will be used interchangeably in Subscriptions and PaymentIntents.

One thing to note is that for Source data, you'll have a cardholder name attribute. On the other hand, a PaymentMethod object doesn't have this field. The best approach is to set up an input for it in the frontend and then add the data to billing_details.name when confirming the PaymentIntent (or SetupIntent).

stripe
  .confirmCardPayment('{PAYMENT_INTENT_CLIENT_SECRET}', {
    payment_method: {
      card: cardElement,
      billing_details: {
        name: 'Jenny Rosen',
      },
    },
  })
  .then(function(result) {
    // Handle result.error or result.paymentIntent
  });

Conclusion

I grateful that Stripe allows us to adopt new and improved methods without completely overhauling legacy code. I hope this post proves helpful to you when facing a similar situation.

YAMAP テックブログ

Discussion