Last updated
How saving a payment card works in FTW
An overview of how the Flex Template for Web functionality for storing payment cards works.
Table of Contents
- Introduction
- Saving card details
- Saving cards without making an initial payment
- Saving cards after a payment
- Getting permission to save a card
- Charging Saved Cards
- Saving Payment Card with PaymentIntents payment flow
- Initial data for Checkout:
- StripePaymentForm changes - show correct form fields:
- Submit StripePaymentForm
Introduction
When a customer first comes to the marketplace and books a listing, there are several form fields the user needs to fill: expiration month, card verification code (CVC), card holder's name and possibly other billing details. To improve user experience for returning customers, it's good to have an option to save payment card details for future bookings. If there is an existing payment card available, the user can just click the "Send request" button to complete checkout page.
From that point forward, the typical case is that customer pays upfront (aka makes an on-session payment). In practice, a preauthorization is created: the money is reserved, but not yet moved to Stripe. When a provider accepts the request, the preauthorization is captured (i.e. the payment is charged from the card and money moved to Stripe and held on the connected account of the provider until payout). The actual payout happens when the transaction completes.
Payment card needs also be saved for off-session payments if those are in use. They are automatic one-time payments that happen when the user is not interacting with your application. You can read more about off-session payments from a separate article.
Note: saving payment card was added to FTW in version v3.3.0.
Saving card details
There are 2 ways to save payment card details:
- saving cards details without making an initial payment
- saving cards details after a payment
The former of those can be done in payment methods page (profile menu -> account settings -> payment methods), but you can also save payment card details on the checkout page when making a one-time payment.
Note: currently, it is only possible to save one card, which becomes the default payment method. I.e. if there is already one payment card saved, your only option is to replace the card with a new one.
Saving cards without making an initial payment
If the user has no existing default payment card, PaymentMethodsPage
component will show just a form to save payment card details:
If there is a default payment methods saved, user will see a dropdown instead, where the current default method is selected. There are two options the user can do: replace the current payment card (it's an option inside the dropdown) or delete the current payment card.
Under the hood, saving a new payment method needs to complete following steps:
- Create Stripe Setup Intent through Flex API to obtain the client secret.
- Call
stripe.handleCardSetup
with the client secretstripe.handleCardSetup
will handle user actions like 3D Secure authentication
- Save payment method:
- Create a new Stripe Customer if needed and attach the new payment
method to
stripeCustomer
entity. - There's no replace or update option for
defaultPaymentMethod
entity. To replace a payment card, one needs to remove previous card first and then add a new payment method.
- Create a new Stripe Customer if needed and attach the new payment
method to
Saving cards after a payment
If the user doesn't have a default payment card saved, or if she is making a one-time payment with a different payment card, there's an option (checkbox) to save the new payment card as the default payment method.
If the default payment method exists, user needs to select one-time payment first and then she is able to replace the default card.
Under the hood, there are few changes made to the Payment Intents checkout flow:
- We need to know if the user has already saved a default payment
method:
- Fetch
currentUser
entity withstripeCustomer.defaultPaymentMethod
relationship
- Fetch
- Show correct form fields:
- If there's a default payment method saved: show SavedCardDetails component. That component allows you to also select one-time payment instead (and optionally replace the current default payment method with this new payment card). If there's no default payment method saved, one-time payment form is shown on its own.
- One-time payment needs card details, card holder's name etc. and optional permission to save card details for future bookings.
- In
request-payment
transition, a new parameter needs to be passed if card details are saved at the same time:setupPaymentMethodForSaving
stripe.confirmCardPayment
call,confirm-payment
transition and the call to send initial message are handled as before- Save the payment method if user has given permission to do that:
- Create a
stripeCustomer
entity for the current user if needed. - Attach the created payment method to
stripeCustomer
entity.
- Create a
Note: Even if the call to save payment method fails, one-time payment itself has succeeded. Therefore, it is better to forward user to
TransactionPage
anyway.
Getting permission to save a card
Once you set up your payment flow to properly save a card with the Payment Intents or Setup Intents API, Stripe will mark any subsequent off-session payment as a merchant-initiated transaction to reduce the need to authenticate. Merchant-initiated transactions require an agreement (also known as a “mandate”) between you and your customer. Read more about needed permissions from Stripe's documentation.
Charging Saved Cards
If the user has a default payment method and she chooses to use it to book a listing, there are couple of changes needed:
- The id of Stripe's payment method needs to be sent to Flex API as
paymentMethod
, when requesting payment. stripe.confirmCardPayment
: Stripe Elements (card) is not needed if the default payment method is used.
Saving Payment Card with PaymentIntents payment flow
Here's the description of complete call sequence on CheckoutPage.
Note: PaymentIntents flow needs transaction process change as described in doc:
Enable Payment Intents if you are using an old version of FTW
Initial data for Checkout:
Check if the user has already saved a default payment method. Fetch
currentUser entity with stripeCustomer.defaultPaymentMethod
relationship. In FTW, we call a thunk function:
fetchCurrentUser
in CheckoutPage.duck.js.
Behind the scenes, this is essentially the following call:
sdk.currentUser.show({
include: ['stripeCustomer.defaultPaymentMethod'],
});
StripePaymentForm changes - show correct form fields:
If there's a default payment method saved: show SavedCardDetails component. That component allows you to also select one-time payment instead (and optionally replace the current default payment method with this new payment card). If there's no default payment method saved, one-time payment form is shown on its own. One-time payment needs card details, card holder's name etc. and optional permission to save card details for future bookings.
Submit StripePaymentForm
After submitting StripePaymentForm
, there are up to 5 calls in
sequence (to Flex and Stripe APIs):
Step 1.
sdk.transactions
.initiate({ processAlias, transition: 'transition/request-payment', ...})
What happens behind the scene:
- API creates PaymentIntent against Stripe API and returns stripePaymentIntentClientSecret inside transaction's protectedData.
- Booking is created at this step - so, availability management will block dates for conflicting bookings.
- Automatic expiration happens in 15 minutes, if process is not transitioned to 'transition/confirm-payment' before that.
- After this call, created transaction is saved to session storage in FTW (or existing enquiry tx is updated).
When you intend to save card details, a new parameter needs to be
passed if card details are saved at the same time:
setupPaymentMethodForSaving
sdk.transactions.initiate({
processAlias,
transition: 'transition/request-payment',
params: {
listingId,
bookingStart,
bookingEnd,
setupPaymentMethodForSaving: true,
},
});
When you are using previously saved payment card, the id of Stripe's
payment method needs to be sent to Flex API as paymentMethod
, when
requesting payment.
sdk.transactions.initiate({
processAlias,
transition: 'transition/request-payment',
params: {
listingId,
bookingStart,
bookingEnd,
paymentMethod: stripePaymentMethodId,
},
});
Note: params might be different in different transaction process graphs.
Check
initiateOrder
thunk function
and related
orderParams
from FTW.
Step 2.
stripe.confirmCardPayment(
stripePaymentIntentClientSecret,
paymentParams
);
paymentParams
should include card element and billing details. See
stripe.confirmCardPayment documentation..
Note: paymentParams are not needed when using previously saved payment card. FTW handles this in
confirmCardPayment
thunk function.
Stripe's frontent script checks if PaymentIntent needs extra actions from customer. Some payments might need Strong Customer Authentication (SCA). In practice, Stripe's script creates a popup (iframe) to card issuers site, where 3D secure v2 authentication flow can be completed.
Step 3.
sdk.transactions.transition({
id: transactionId,
transition: 'transition/confirm-payment',
params,
});
Inform Marketplace API, that PaymentIntent is ready to be captured after
possible SCA authentication has been requested from user. FTW does that
in
confirmPayment
thunk call
Step 4.
sdk.messages.send({ transactionId: orderId, content: message });
FTW sends an initial message to transaction if customer has added a message.
Step 5.
As a final step, we need to save the payment method, if customer has selected the "Save for later use" checkbox. So, this is relevant if user has selected onetime payment - instead of making a charge from the previously saved credit card.
On FTW, we call savePaymentMethod function that creates stripe customer and adds updates default payment method.
There are 3 different scenarios, which require different calls to Flex API:
1. No StripeCustomer entity connected to Flex API:
sdk.stripeCustomer.create(
{ stripePaymentMethodId },
{ expand: true, include: ['defaultPaymentMethod'] }
);
FTW:
dispatch(createStripeCustomer(stripePaymentMethodId))
2. Current user has already defaultPaymentMethod - 2 calls:
=> sdk.stripeCustomer.deletePaymentMethod({}, { expand: true })
=> sdk.stripeCustomer.addPaymentMethod({ stripePaymentMethodId }, { expand: true })
FTW:
dispatch(updatePaymentMethod(stripePaymentMethodId))
3. Current user has StripeCustomer entity connected, but no defaultPaymentMethod:
=> sdk.stripeCustomer.addPaymentMethod({ stripePaymentMethodId }, { expand: true })
FTW:
dispatch(addPaymentMethod(stripePaymentMethodId))
After these steps, customer is redirected to inbox and the ball is thrown to Provider to accept or decline the booking.