Skip to content

Instantly share code, notes, and snippets.

@mindlapse
Last active September 7, 2021 15:05
Show Gist options
  • Save mindlapse/72139f022d6e620e4f0d59dc50c1797e to your computer and use it in GitHub Desktop.
Save mindlapse/72139f022d6e620e4f0d59dc50c1797e to your computer and use it in GitHub Desktop.
A guide on how to use PaymentIntents with tipsi-stripe

Introduction

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.

  1. Creating a PaymentMethod (with card data, a card token, or a token from Google Pay / Apple Pay)
  2. Initiating a payment from the mobile app
  3. Asking the user to authenticate a transaction that was attempted off-session by you, the Merchant
  4. Saving a card for future use

Creating a PaymentMethod

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,
  }),
}

Initiating a payment from the mobile app

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 setting confirmation_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

... with manual confirmation

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).
  • 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 the client_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.

... with automatic confirmation

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 is succeeded 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).
  • If the resulting status is requires_action then return to the client with the client_secret from the resulting payment intent you just created.
  • Call await stripe.confirmPaymentIntent({ clientSecret: "..." }), passing in the client_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 the stripe.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.

You initiated a payment on the server that required authentication from the user

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:

  1. Present the option to attempt the payment using the same card, or to provide a new one.
  2. Attach the selected card to the payment method to the PaymentIntent on the server side.
  3. Handle the payment as though it were an on-session payment. See the section Initiating a payment from the mobile app

The user is saving a card for future use

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:

  1. Create a SetupIntent on the server (use confirmation_method=automatic) for the selected payment method.
  2. Return the client_secret of the SetupIntent to the app.
  3. 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
}
@unfrgivn
Copy link

stripe.paymentRequestWithCardForm now returns a PaymentMethod so you would actually pass the id from that response object to your backend and create a Customer/Intent and skip the stripe.createPaymentMethod step

@shtefanilie
Copy link

@unfrgivn so basically if I do this, with paymentMethod being the exact thing that results from const paymentMethod = await stripe.paymentRequestWithCardForm();, should that confirm the payment?

stripe.confirmPaymentIntent({
  clientSecret: paymentIntentClientSecret,
  paymentMethod
});

@unfrgivn
Copy link

unfrgivn commented Oct 1, 2019

@shtefanilie I pass all my paymentmethods to my backend and create/confirm the paymentintent there so I have not used that client method. Are you creating a PaymentIntent first somewhere? The comments for this library say confirmPaymentIntent "generates a paymentintent" but that is not my understanding. You also won't have a piClientSecret until you receive a response from your backend generating the PI.

Anyways, in your example: const paymentMethod = await stripe.paymentRequestWithCardForm(); paymentMethod would be an object like the following (depending on what options you pass to paymentRequestWithCardForm()

 {
    id: 'pm_abc123....',  
    billingDetails: {
        name: 'John Smith',
        address: {...}
    }
}

So you'll want to get the PM Id out of that object like const { id : paymentMethodId } = paymentMethod and then pass that Id. Are you

@shtefanilie
Copy link

Yes, that's what it returns. So, I'm first creating a PaymentIntent on my server. After, I call paymentRequestWithCardForm in order to get all the card details.
After I collect that information, I call const result = await stripe.confirmPaymentIntent(paymentIntentClientSecret, {element: card}); or stripe.confirmPaymentIntent(paymentIntentClientSecret) .
Unfortunately I get this error:

unknown:ReactNative: Exception in native call
    java.lang.ClassCastException: java.lang.String cannot be cast to com.facebook.react.bridge.ReadableNativeMap
        at com.facebook.react.bridge.ReadableNativeArray.getMap(ReadableNativeArray.java:155)
        at com.facebook.react.bridge.ReadableNativeArray.getMap(ReadableNativeArray.java:24)
        at com.facebook.react.bridge.JavaMethodWrapper$8.extractArgument(JavaMethodWrapper.java:100)
        at com.facebook.react.bridge.JavaMethodWrapper$8.extractArgument(JavaMethodWrapper.java:96)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:359)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)
        at android.os.Looper.loop(Looper.java:193)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:232)
        at java.lang.Thread.run(Thread.java:764)

If I do the call like this,

stripe.confirmPaymentIntent({
  clientSecret: paymentIntentClientSecret,
  paymentMethod
});

the result is this:

 com.facebook.react.bridge.NoSuchKeyException: cvc
        at com.facebook.react.bridge.ReadableNativeMap.getNullableValue(ReadableNativeMap.java:137)
        at com.facebook.react.bridge.ReadableNativeMap.getNullableValue(ReadableNativeMap.java:141)
        at com.facebook.react.bridge.ReadableNativeMap.getString(ReadableNativeMap.java:192)
        at com.gettipsi.stripe.StripeModule.extractPaymentMethodCreateParams(StripeModule.java:594)
        at com.gettipsi.stripe.StripeModule.extractConfirmPaymentIntentParams(StripeModule.java:501)
        at com.gettipsi.stripe.StripeModule.confirmPaymentIntent(StripeModule.java:362)
        at java.lang.reflect.Method.invoke(Native Method)

My code is:

const paymentIntent = await myAPI.post('/api/v1/payment_intent', newCharge);

const { type, tokenId, card } = await stripe.paymentRequestWithCardForm();

const { paymentIntentClientSecret } = paymentIntent;

const result = await stripe.confirmPaymentIntent(paymentIntentClientSecret, {element: card});

@unfrgivn
Copy link

unfrgivn commented Oct 1, 2019

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, but confirm 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 API PaymentMethod::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?

const paymentIntent = await myAPI.post('/api/v1/payment_intent', newCharge);

// Use either of these below in your confirm
const paymentMethod = await stripe.paymentRequestWithCardForm();
const { id: paymentMethodId } = paymentMethod; 

const { paymentIntentClientSecret: clientSecret } = paymentIntent;

const result = await stripe.confirmPaymentIntent(paymentIntentClientSecret, {element: card});

stripe.confirmPaymentIntent({
    clientSecret,
    // AND
    paymentMethod, // Assuming your payment card form has the billingDetails and card: {} properties
    // OR
    paymentMethodId
});

You can see the inputs available to that method in Stripe.js in the experiment branch of tipsi-stripe

/**
 * @typedef {Object} ConfirmPaymentIntentParams
 * @property {string} clientSecret
 * @property {CreatePaymentMethodParams} paymentMethod
 * @property {string} paymentMethodId
 * @property {string} sourceId
 * @property {string} returnURL - Optional see: https://stripe.com/docs/mobile/ios/authentication#return-url
 * @property {boolean} savePaymentMethod
 */

Hope that was pertinent/helpful to your situation, if not you could always hit me up from the Discord channel for this library.

@nicopir
Copy link

nicopir commented Nov 22, 2019

Hi everyone! Thank you very much for all the help!

I'm just wondering if the stripe card form accessible with stripe.paymentRequestWithCardForm() is customizable? I'm working in a french/dutch business and I don't see anywhere in the tipsi documentation that I can set the language or set the titles in the form's options. Could you tell me if it's possible to handle multilingual or if it hasn't been managed in the tipsi library?

Thanks again for your help!

@mkamranhamid
Copy link

@nicopir +1 for labeling

@yemi
Copy link

yemi commented Dec 31, 2019

Trying to get tipsi-stripe work with RN 0.61 but getting build errors, is there a current workaround for 0.60+ or is support coming?

@Elijah23Johnson
Copy link

I just got it working in RN 0.60.
ext {
buildToolsVersion = "28.0.3"
minSdkVersion = 21
compileSdkVersion = 28
targetSdkVersion = 28
firebaseVersion = "17.0.0"
googlePlayServicesVersion = "17.0.0"
}

@rahultrivedy
Copy link

I am trying to send Payment Method as Payment Intent .
RN - 0.61 .
Android.
I get the error -
Screenshot_20200123-123554
The code is:
image

If we use the below code (with PaymentMethod.id) it is working fine but not able to pass the billing details :

image (1)

Is there any solution where I can send the billing details with Payment Intent to stripe (compulsory requirement in current project to generate and send invoices to user directly through stripe on successful payment) ?

Any help would be appreciated. Thanks.

@Elijah23Johnson
Copy link

Elijah23Johnson commented Jan 23, 2020 via email

@rahultrivedy
Copy link

rahultrivedy commented Jan 25, 2020

@PosSoolutions Thanks for the quick reply.
We tried the code provided. We put in all the details but are getting null for the billing details.
Hence we are not able to send invoices to client through stripe. Please let us know if we are missing something.
Attached screenshot of the result.
image (3)

@Elijah23Johnson
Copy link

Elijah23Johnson commented Jan 25, 2020 via email

@rahultrivedy
Copy link

@PosSoolutions We tried the code but some reason it isn't working in our case. Do you have any branch where we can view the implementation of above code. It'll be a huge help as we are running a bit short of time. If you want to see more of our code, let us know.
Also, when can we expect a fix for this in the library?

@Elijah23Johnson
Copy link

Elijah23Johnson commented Jan 28, 2020 via email

@avaltat
Copy link

avaltat commented Jan 31, 2020

Hi where can I find the installation guide please?

@Elijah23Johnson
Copy link

Elijah23Johnson commented Jan 31, 2020 via email

@avaltat
Copy link

avaltat commented Jan 31, 2020 via email

@joewired
Copy link

use 8.0.0-beta.9
8.0.0-beta.8 will give you a build error which is fixed by 8.0.0-beta.9

@kavindadilshan
Copy link

Error: The payment_method parameter supplied pm_1Gl9ooD1IUozKSmgOoYAckbG belongs to the Customer cus_HJnPE3Jech06WI. Please include the Customer in the customer parameter on the PaymentIntent.

@PARAGJYOTI
Copy link

PARAGJYOTI commented Jun 17, 2020

Anyone knows how to close the modal after successful setupIntent, or paymentInent properly? I am using react-native-navigation, I can close the modal after successful status by Navigation.dismissAllModals(), but if someone tries to retry the process, it gives an error with [Error: Previous request is not completed]. Can anyone solve it?

edit : I can see, on Android, it is behaving as it should be, it closes the payment authorization modal automatically, but on iOS I have to manually close the modal by pressing the close button. Please can anyone fix this?

@AppKidd
Copy link

AppKidd commented Jul 17, 2020

Thank you very much for producing this excellent guide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment