Card payments with Stripe should be performed with PaymentIntents.
This API was created to handle modern payments, where the cardholder's bank may require the user to authenticate themselves with the bank before a payment can be authorized.
Authentication requirements first started to appear with European banks regulated by PSD2 which introduced Strong Customer Authentication (SCA) requirements.
tipsi-stripe helps to support key parts of the payment experience, with sections on each below.
- Creating a PaymentMethod (with card data, a card token, or a token from Google Pay / Apple Pay)
- Initiating a payment from the mobile app
- Asking the user to authenticate a transaction that was attempted off-session by you, the Merchant
- Saving a card for future use
Creating a payment method using card details - note that the return value is a PaymentMethod object, not a token, with the properties it was created with (except for the card number).
try {
const paymentMethod = await stripe.createPaymentMethod({
card : {
number : '4000002500003155',
cvc : '123',
expMonth : 11,
expYear : 2020
}
})
} catch (e) {
// Handle error
}
Creating a payment method using a card token. This could be:
- a token from Google Pay
- a token returned by
await stripe.paymentRequestWithCardForm(...)
- a token returned by
await stripe.createTokenWithCard(...)
- a token returned by
await stripe.createTokenWithBankAccount(...)
try {
const paymentMethod = await stripe.createPaymentMethod({
card : {
token : '1F70U2HbHFZUJkLLGyJ26n5rWDBfofzDJmdnal0dMrcEHTvKd',
}
})
} catch (e) {
// Handle error
}
Here are the PropTypes that defines the shape of what can be provided to createPaymentMethod:
{
// Card properties:
// - As an alternative to providing card PAN info, you can also provide a Stripe token:
// https://stripe.com/docs/api/payment_methods/create#create_payment_method-card
card: PropTypes.oneOfType([
PropTypes.shape({
cvc: PropTypes.string,
expMonth: PropTypes.number,
expYear: PropTypes.number,
number: PropTypes.string,
}),
PropTypes.shape({ token: PropTypes.string }),
]),
// You can also attach billing information to a payment method
billingDetails: PropTypes.shape({
address: PropTypes.shape({
city: PropTypes.string,
country: PropTypes.string,
line1: PropTypes.string,
line2: PropTypes.string,
postalCode: PropTypes.string,
state: PropTypes.string,
}),
email: PropTypes.string,
name: PropTypes.string,
phone: PropTypes.string,
}),
}
To do this, make a call from your mobile app to create a Payment Intent on your backend server
- If you created the payment intent with
confirmation_method='manual'
then you're using a manual confirmation flow, and payment intents can only be confirmed from the backend using the secret key. Jump to the ... with manual confirmation section - Otherwise, if you created the payment intent without specifying
confirmation_method
or by settingconfirmation_method='automatic'
then you are using an automatic confirmation flow. In this flow, you can confirm (process) the payment intent right from the mobile app, and webhooks sent by Stripe will notify your backend of success. This is the preferred flow. Jump to the ... with automatic confirmation section
In this flow, follow these steps:
- Obtain a PaymentMethod (either one saved to the customer or a new one as described in the Creating a PaymentMethod section.),
- Create a PaymentIntent on the backend, with the provided PaymentMethod ID and the amount.
- set
confirmation_method=manual
when creating the intent - do not specify
off_session=true
, since these are steps for creating an on-session payment (a payment where the user is present).
- set
- Confirm the PaymentIntent on the backend. If the PaymentIntent moves to a
succeeded
state, then that's it! The payment was successful. - If the PaymentIntent status moves to
requires_action
, then return theclient_secret
of the PaymentIntent to the mobile app, along with the ID of the PaymentIntent. - Call
await stripe.authenticatePaymentIntent({ clientSecret: "..." })
, passing in the client_secret. This will launch an activity where the user can then authenticate the payment. - If the call above succeeds, then call your backend with the PaymentIntent ID, Retrieve the PaymentIntent, and then Confirm the PaymentIntent.
In this flow, follow these steps:
- Obtain a PaymentMethod (either one saved to the customer or a new one as described in the Creating a PaymentMethod section.),
- Create a PaymentIntent on the backend, with the provided PaymentMethod ID and the amount.
- set
confirmation_method=automatic
when creating the intent (or omit it, since it is the default) - set
confirm=true
(note: this will attempt to complete the payment, if the resulting status issucceeded
then the customer will not have to provide authtentication and the payment is complete) - do not specify
off_session=true
, since these are steps for creating an on-session payment (a payment where the user is present).
- set
- If the resulting status is
requires_action
then return to the client with theclient_secret
from the resulting payment intent you just created. - Call
await stripe.confirmPaymentIntent({ clientSecret: "..." })
, passing in theclient_secret
. If an authentication is needed then an activity will be launched where the user can then authenticate the payment. If the user authenticates, then the payment is confirmed automatically and thestripe.confirmPaymentIntent
call resolves with the result, which includes the resulting status of the payment intent. The statuses in a Payment Intent Lifecycle can be viewed through that link. - On your backend, you can listen for webhooks of the payment intent succeeding that will be sent by Stripe.
In this scenario, you attempted to confirm a PaymentIntent on the server using a payment method
with off_session=true
, however the payment required authentication.
The /confirm
API call would fail and the PaymentIntent would transition to status=requires_payment_method
.
At this stage the user needs to be brought back on-session, via an email or notification. When the user is brought into the app, you should, for the same PaymentIntent:
- Present the option to attempt the payment using the same card, or to provide a new one.
- Attach the selected card to the payment method to the PaymentIntent on the server side.
- Handle the payment as though it were an on-session payment. See the section Initiating a payment from the mobile app
When saving a card as a PaymentMethod to either bill a user later, or for the user to make purchases with, we want to collect authentication up-front, if it's needed by the card, to minimize the chance that we will need to interrupt them for authentication on future payments. We can prepare the card by using a SetupIntent. Here are the steps:
- Create a SetupIntent on the server (use
confirmation_method=automatic
) for the selected payment method. - Return the
client_secret
of the SetupIntent to the app. - Call
stripe.confirmSetupIntent()
. This will prompt the user for authentication (if needed) and finishes the setup.
try {
const result = await stripe.confirmSetupIntent({
clientSecret : "..."
})
} catch (e) {
// handle exception here
}
Got it you're creating your intent before the paymentmethod (which is fine and one of the recommended flows), but I create my intent after I have the paymentmethod and auto-confirm ("manual"
confirmation_method
, butconfirm
set to "true") since I don't have a checkout flow where someone could drop out, just a single click-to-pay experience. Even if I did, I would set up the paymentintent as you do, get the paymentmethod from the Stripe library cardform and then pass that to my back-end to update and confirm the intent. When I am cleaning up processing of the order/transaction in my backed offline, I get the exp month/year, last4, etc... by calling Stripe APIPaymentMethod::retrieve
.Anyways, back to your code, did you try sending the payment method wholesale or just the Id? You shouldn't have a
tokenId
from that cardForm component since you're using paymentmethods and not the older card/source/tokens. If you are getting a token, what version of the Stripe API are you using in your Pod/build.gradle?You can see the inputs available to that method in Stripe.js in the experiment branch of tipsi-stripe
Hope that was pertinent/helpful to your situation, if not you could always hit me up from the Discord channel for this library.