Last updated

How to take PaymentIntents into use

Overview of how Stripe PaymentIntents work in FTW, and how you can change older FTW version to support for Strong Customer Authentication (SCA).

This guide walks you through the process of taking PaymentIntents into use. Stripe's PaymentIntent is a new way to handle Strong Customer Authentication (SCA) by using frictionless 3D Secure 2 authentication.

Before starting to read this article, you probably want to get familiar with Strong Customer Authentication and how PaymentIntent flow works by reading related background articles.

Note: Taking Stripe PaymentIntent flow into use, is a big change for CheckoutPage and includes process change. You should carefully check what kind of changes are made in FTW release: v3.0.0. Taking update from upstream or even cherry-picking commits might make the update easier, but you should first track your custom-code to affected components.

1. Process change

Stripe's Payment Intents API is a new way to build dynamic payment flows. Its automatic confirmation flow helps a lot since all the authentication actions for a customer are included in a single call: stripe.handleCardPayment. When called, Stripe's SDK checks if there's a need for Strong Customer Authentication (SCA) and creates a popup to card issuer's website.

However, to be able to call handleCardPayment, there needs to be a new state to the transaction process. Because of this, we have split the previous transition (Initial - request -> Preauthorized) into two:

PaymentIntents flow needs a process change

So, after transitions (request-payment or request-payment-after-enquiry), API returns stripePaymentIntentClientSecret among the protected data of the current transaction. This client-secret is used for the call to stripe.handleCardPayment. Then there is another transition made against Marketplace API, so that it can confirm the PaymentIntent and preauthorize the order. Transaction process continues normally after that - i.e. Provider has to accept or reject the order.

Concrete steps here are requesting transaction process change from Flex support, then updating bookingProcessAlias in config.js and making necessary changes to src/util/transaction.js file. Remember, when transaction process is changed, you need to go through all the files that import transitions or utility functions from util/transaction. In practice, we made changes to InboxPage, TransactionPage, TransactionPanel, ActivityFeed, BookingBreakdown, BookingDatesForm, and CheckoutPage. The list might be different if you have customized your components or process.

2. Add new thunk calls to stripe.duck.js

When using PaymentIntent flow, we don't need stripe.createToken anymore, but we need to add two new thunk calls: stripe.handleCardPayment and stripe.retrievePaymentIntent.

stripe.handleCardPayment is needed to provide SCA as mentioned earlier. However, since customers are making several AJAX calls on CheckoutPage, it is possible that there is a network error or something else happening between those calls. Even the whole page, might be reloaded at some point. We need to retrieve up-to-date PaymentIntent from Stripe API and check its status to be able to continue the payment process. This can be done with stripe.retrievePaymentIntent.

3. CheckoutPage: add new API calls and call them in sequence

The biggest change happens in CheckoutPage. When a user submits StripePaymentForm and handleSubmit is called from CheckoutPage, new data needs to be prepared (billing details: name, email, and billing address) and then 4 thunk-calls/Promises need to be made in sequence:

Step 1. onInitiateOrder

  • This tells Marketplace API to create booking and PaymentIntent

    • Booking is created, so availability management blocks dates for conflicting bookings
    • API returns stripePaymentIntentClientSecret inside transaction's protectedData
  • This combines both transitions:

    • sdk.transitions.initate aka request-payment
    • sdk.transitions.transition aka continue enquiry with request-payment-after-enquiry
  • Automatic expiration happens in 15 minutes, if process is not transitioned to 'transition/confirm-payment' before that.
  • Created transaction is saved to session storage or existing enquiry tx is updated. (There is more about this step later.)

Step 2. onHandleCardPayment

  • This is a call stripe.handleCardPayment
  • If the customer must perform additional steps to complete the payment, such as authentication, Stripe.js walks them through that process.

Step 3. onConfirmPayment

  • This tells Marketplace API that customer has completed the payment requirements. API will validate and mark the payment confirmed in Flex.

Step 4. onSendMessage

  • If the customer has added an initial message to the provider, the app sends that message after the payment is confirmed.

Stripe.js: PaymentIntents authentication modal

Note: stripe.handleCardPayment needs an instance of Stripe to be passed from StripePaymentForm. stripe.handleCardPayment will check card details from connected Stripe Elements input.

4. CheckoutPage: save updated transaction

We use session storage to buffer checkout page against page reloads and errors - customer needs to be able to continue payment after accidental page refresh and network errors. This is a UX issue, but more importantly, it builds trust. Because of this need, we save booking dates and other data there. Previously enquiredTransaction was saved there too, but that concept is now expanded a bit: any transaction can now be saved to session storage under the key "transaction".

So, if there is an existing transaction in enquiry state and customer books the listing, TransactionPage sends that transaction to CheckoutPage. As a first step CheckoutPage saves received data to the session store. This is pretty much the same functionality as with previous card-token payment process - only the key is changed from enquiryTransaction to transaction. However, after transition request-payment (or request-payment-after-enquiry) the updated transaction is saved again. (the relevant new data in transaction is stripePaymentIntentClientSecret.)

In addition, the handling of booking breakdown with SpeculatedTransaction needs to be changed because saved transaction already contains booking in some cases and a new call to sdk.transactions.initiateSpeculative would just return a conflict error telling about an already existing booking.

5. StripePaymentForm: adding billing details and showing errors

Most of the visual changes happen in StripePaymentForm. Billing details are added to the form and most of the errors of different thunk calls are shown inside it.

The default mode for FTW is to show billing address fields. Even though it is recommended by Stripe, you might want to remove those fields due to UX reasons. That can be made just by not adding StripePaymentAddress sub-component.

Note: if the page is reloaded after successful call to stripe.handleCardPayment billing details should not be shown to the user since credit card number and other billing details are already sent to Stripe.

6. Test with live credit cards

Since 3D Secure authentication flow is different between different credit card issuers, you should test at least some credit cards how they work in a live environment.

This can be done by creating another Heroku instance (or some other production environment) that uses your production Client Id for Flex with live Stripe keys (both publishable and secret). Then create a new Git branch that takes PaymentIntents flow into use and adds Basic Authentication configuration into environment variables. After that, you could deploy your payment-intent branch into Heroku (or your alternative production environment). Then you can just book some existing listing and maybe reject it to get refund to your live card account.