Tipsi-stripe: Tipsi-Stripe compliant with SCA and 3D Secure

Created on 6 Dec 2020  路  13Comments  路  Source: tipsi/tipsi-stripe

So I am totally new to the whole payment part in my apps or websites. I have created a new Mobile app using React-Native in which would be my first app that would hopefully make me some money.

I have followed a tutorial (https://www.youtube.com/watch?v=Dzq1zDlZVUg) and now have a working credit card payment going on. However in the Stripe documentation I found that for Europe (where I am based) I need to comply with Strong Customer Authentication (SCA) and 3D Secure for credit and debit card payments.

The tutorial I followed used the charge API call in a Firebase function. Is this "secure" enough for Europe? Or should I use the PaymentIntent api in a new app?

Altought I'm scrolling trough the documentation the entire morning again I still don't get the correct flow with the PaymentIntent api call. Does PaymentIntent replace the charge API in my Firebase cloud function? Any link to a update tutorial/simple explanation of which API to use now would be much appreciated!

wont-fix

Most helpful comment

@Tinmania2018 You're halfway there. The experimental branch is the branch for the beta versions.
Make sure you're not reading the docs on https://tipsi.github.io/tipsi-stripe/docs/index.html, and instead have done the following:

  • clone locally
  • git checkout experimental
  • npm install
  • yarn start-docs

As the docs on the github.io link are for the previous version.

A quick high level guide of what you need to do is this:

  • trigger the payment flow client side (e.g. const paymentMethod = await stripe.paymentRequestWithCardForm())
  • Send this to your server and create a paymentIntent from it, attempting to charge the customer:
    const paymentIntent = await stripeService.paymentIntents.create(
      { ...intent stuff ...
      payment_method: paymentMethod,
      // I have these in my logic, YMMV
      confirmation_method: 'manual',
      confirm: false,
      }
    )
// Because i have manual confirmation method, i need to confirm the payment
    const { status } = await stripeService.paymentIntents.confirm(paymentIntent.id)
    const scaRequired = status === 'requires_action'

    // send back to client: paymentIntent.client_secret, paymentIntent.status, etc
  • Now back in the client, if SCA is required, you'll need to trigger the prompt
  • Status can be either succeeded (payment flow complete) or requires_action for SCA flow, which you can trigger like so:
  const { status, clientSecret } = paymentIntent // from server
  if (status === 'requires_action') {
    let authResponse
    try {
      authResponse = await stripe.authenticatePaymentIntent({
        clientSecret
      })
    } catch (error) {
      const errorObject = new Error(
        'Your security check failed. Please try another payment method.'
      )
      errorObject.type = 'sca'
      throw errorObject
    }

    if (authResponse.status === 'requires_payment_method') {
      const errorObject = new Error(
        'Your payment failed. Please try another payment method.'
      )
      errorObject.type = 'sca'
      throw errorObject
    } else if (authResponse.status === 'requires_confirmation') {
      // Great, SCA passed. Now debit for amount
    }
  • stripe.authenticatePaymentIntent can return a status object for different scenarios that you need to handle:

    • error throwing - SCA failed, restart checkout flow

    • requires_payment_method for failed payments - restart checkout flow

    • requires_confirmation - successful, debit for amount!

  • Once you've reached requires_confirmation, you need to go back to the server and re-confirm the payment intent:
confirmedIntent = await stripeService.paymentIntents.confirm(id) // server side

if (confirmedIntent.status === 'succeeded') {
  // bon, all good
}
  • After this, your customer has completed the flow for card checkouts.
  • Native flow is very very similar, the paymentintent/SCA flow should be identical, but you will receive a TOKEN instead of a paymentMethod.
    token = await stripe.paymentRequestWithNativePay(
      {
        // Android
        currency_code: 'GBP',
        total_price: totalPrice,
        line_items: lineItems,
        // iOS
        currencyCode: 'GBP',
        countryCode: 'GB'
      },
      lineItems
    )

Hope this helps you.

All 13 comments

Hey @Tinmania2018,

I have the same questions. Have you found anymore info on this?

Hello both, you will need to follow the instructions/documentation found in the experimental branch.

I found it useful to clone the repo locally, checkout that branch, and run the docs on my local machine.

Best of luck.

Hey @Tinmania2018,

I have the same questions. Have you found anymore info on this?

From what I have gathered now the charge API is not enough for the European market. We should look into the PaymentIntent API.

@jack828 Are you referring to information about the PaymentInent API that you gathered from the experimental documentation? Or regarding the correct flow for payments?

@Tinmania2018 Kind of both.

You will need to use the experimental branch and change your server side code to use the payment intents API to ensure you are SCA compliant. It's quite the challenge, but the Stripe docs on this matter are pretty comprehensive.

If you get stuck at any point, feel free to reach out to me and I will do my best.

@Tinmania2018 Kind of both.

You will need to use the experimental branch and change your server side code to use the payment intents API to ensure you are SCA compliant. It's quite the challenge, but the Stripe docs on this matter are pretty comprehensive.

If you get stuck at any point, feel free to reach out to me and I will do my best.

Thank you for your reply Jack, what do you mean with experimental branche? The Tipsi beta versions? Currently I have 8.0.0-beta 11 installed. For me the Tipsi docs are somewhat to comprehensive as I have a hard time following all the documentation and I am overwhelmed with all the information and possible flows :D

@Tinmania2018 You're halfway there. The experimental branch is the branch for the beta versions.
Make sure you're not reading the docs on https://tipsi.github.io/tipsi-stripe/docs/index.html, and instead have done the following:

  • clone locally
  • git checkout experimental
  • npm install
  • yarn start-docs

As the docs on the github.io link are for the previous version.

A quick high level guide of what you need to do is this:

  • trigger the payment flow client side (e.g. const paymentMethod = await stripe.paymentRequestWithCardForm())
  • Send this to your server and create a paymentIntent from it, attempting to charge the customer:
    const paymentIntent = await stripeService.paymentIntents.create(
      { ...intent stuff ...
      payment_method: paymentMethod,
      // I have these in my logic, YMMV
      confirmation_method: 'manual',
      confirm: false,
      }
    )
// Because i have manual confirmation method, i need to confirm the payment
    const { status } = await stripeService.paymentIntents.confirm(paymentIntent.id)
    const scaRequired = status === 'requires_action'

    // send back to client: paymentIntent.client_secret, paymentIntent.status, etc
  • Now back in the client, if SCA is required, you'll need to trigger the prompt
  • Status can be either succeeded (payment flow complete) or requires_action for SCA flow, which you can trigger like so:
  const { status, clientSecret } = paymentIntent // from server
  if (status === 'requires_action') {
    let authResponse
    try {
      authResponse = await stripe.authenticatePaymentIntent({
        clientSecret
      })
    } catch (error) {
      const errorObject = new Error(
        'Your security check failed. Please try another payment method.'
      )
      errorObject.type = 'sca'
      throw errorObject
    }

    if (authResponse.status === 'requires_payment_method') {
      const errorObject = new Error(
        'Your payment failed. Please try another payment method.'
      )
      errorObject.type = 'sca'
      throw errorObject
    } else if (authResponse.status === 'requires_confirmation') {
      // Great, SCA passed. Now debit for amount
    }
  • stripe.authenticatePaymentIntent can return a status object for different scenarios that you need to handle:

    • error throwing - SCA failed, restart checkout flow

    • requires_payment_method for failed payments - restart checkout flow

    • requires_confirmation - successful, debit for amount!

  • Once you've reached requires_confirmation, you need to go back to the server and re-confirm the payment intent:
confirmedIntent = await stripeService.paymentIntents.confirm(id) // server side

if (confirmedIntent.status === 'succeeded') {
  // bon, all good
}
  • After this, your customer has completed the flow for card checkouts.
  • Native flow is very very similar, the paymentintent/SCA flow should be identical, but you will receive a TOKEN instead of a paymentMethod.
    token = await stripe.paymentRequestWithNativePay(
      {
        // Android
        currency_code: 'GBP',
        total_price: totalPrice,
        line_items: lineItems,
        // iOS
        currencyCode: 'GBP',
        countryCode: 'GB'
      },
      lineItems
    )

Hope this helps you.

@jack828 thank you for your extra input! I believe I'm heading in the right direction now. Below is my Firebase function that appears to be working;

exports.StripePaymentIntent = functions.https.onRequest(async(request, response) => {
    await stripe.paymentIntents.create({
          amount: request.body.amount,
          payment_method: request.body.token,
          payment_method_types: request.body.paymenttype,
          confirmation_method: 'automatic',
          currency: 'eur',
          confirm: false,
          statement_descriptor: "my first payment"
      })
      .then(response => {
        console.log(response)
      })
      .catch(error => {
        console.log(error)
      })

    }
  );

Somewhere in the output I get the client_secret so that's good. Unfortunately I also receive a status of 'Requires_payment_method' and the Firebase function times out after 60 seconds so I guess something isn't quite right yet.

@Tinmania2018 Sure, glad to hear you're nearly at a working solution.

Couple things:

  1. I'd need to see what your request.body looks like for that function
  2. Your function times out because (I assume, no experience with FB functions here) you aren't returning anything/sending any kind of response back to the client.
  3. You don't _always_ need to include payment_method_types - I missed another snippet for you - how my server builds the payment intent request:
    if (isCardPaymentRequest) {
      intentRequest.payment_method = paymentMethod
    } else if (isNativePaymentRequest) {
      intentRequest.payment_method_data = {
        type: 'card',
        card: { token: paymentMethodToken }
      }
    }

Note the differences in behaviour - this module returns something different depending on card/native payments, and we need to handle that.

For example, with a native payment our app logic could be something like:

  token = await stripe.paymentRequestWithNativePay(
      {
        ...options
      },
      items
    )
  paymentIntent = await sendAndCompletePaymentOnServer({
    paymentMethodToken: token.tokenId
  })
// Don't forget:
  await stripe.cancelNativePayRequest() // on error
  await stripe.completeNativePayRequest() // on success

Then our server code could do the request with paymentMethodToken, setting the payment_method_data property.

If we was doing a card checkout, we would:

    paymentMethod = await stripe.paymentRequestWithCardForm()
    paymentIntent = await sendAndCompletePaymentOnServer({
      paymentMethod: paymentMethod.id,
    })

See how the way we tell stripe what we are doing slightly differs.

@jack828 a quick reply on your questions. Tomorrow I will look into your reply a bit more in dept and try to work forward on the payment function.

Q1; As a test I now have a button to fetch a token with stripe.paymentRequestWithCardForm. After that I push a button to make a payment / starting the Firebase function. The request.body is filled with amount (fixed to 100 for now), token, which is fetched with the handleCardPayPress function and payment type card (fixed for now). When everything works I want to make this function more complex with a choice on which payment methode is available depending on which country you are in etc.

    handleCardPayPress = async () => {
        try {
          setLoading(true)
          const token = await stripe.paymentRequestWithCardForm({
            // Only iOS support this options
            smsAutofillDisabled: true,
            requiredBillingAddressFields: 'full',
            prefilledInformation: {
              billingAddress: {
                name: 'Gunilla Haugeh',
                line1: 'Canary Place',
                line2: '3',
                city: 'Macon',
                state: 'Georgia',
                country: 'US',
                postalCode: '31217',
                email: '[email protected]',
              },
            },
          })
          setLoading(false)
          setToken(token)
          console.log('tipsi token',token)
        } catch (error) {
          setLoading(false)
          console.log('A Tipsi error occurred!' , error)
        }
      }


      makePayment = async () => {
        console.log('make payment started')
        setLoading(true)
            axios({
              method: 'POST',
              url: 'https://cloudfunction',
              data:{
                amount: 100,
                token: token.id,
                paymenttype: "card"
          },
        }).then(response => {
          console.log('response payment intent', response)
          setLoading(false)
        })
      }

Q2; It could be true, this I'll look into more tomorrow.

Q3; Payment_method_types is present in the function because the idea is to use the request.body.paymenttype in the function to create a payment intent for the payment types I would like to offer.

You're saying you don't use Firebase cloud functions, do you use another online database for this or are you handling everything local within the app and am I making it to complex then needed. As I understand local handling is possible but it is not secure?

@Tinmania2018 Sure, no rush at all.

  1. Great - you're free to do this however you like. It does sound right for your case to do that.
  2. 馃憤馃徎
  3. Sure, then yes you would need to pass it along.

I have the luxury of a dedicated host for my server. I use MongoDB for the database and a custom project setup to handle everything, with a CMS.

Let me know how you get on when you can!

@Tinmania2018 Kind of both.

You will need to use the experimental branch and change your server side code to use the payment intents API to ensure you are SCA compliant. It's quite the challenge, but the Stripe docs on this matter are pretty comprehensive.

If you get stuck at any point, feel free to reach out to me and I will do my best.

@jack828 a quick reply on your questions. Tomorrow I will look into your reply a bit more in dept and try to work forward on the payment function.

Q1; As a test I now have a button to fetch a token with stripe.paymentRequestWithCardForm. After that I push a button to make a payment / starting the Firebase function. The request.body is filled with amount (fixed to 100 for now), token, which is fetched with the handleCardPayPress function and payment type card (fixed for now). When everything works I want to make this function more complex with a choice on which payment methode is available depending on which country you are in etc.

    handleCardPayPress = async () => {
        try {
          setLoading(true)
          const token = await stripe.paymentRequestWithCardForm({
            // Only iOS support this options
            smsAutofillDisabled: true,
            requiredBillingAddressFields: 'full',
            prefilledInformation: {
              billingAddress: {
                name: 'Gunilla Haugeh',
                line1: 'Canary Place',
                line2: '3',
                city: 'Macon',
                state: 'Georgia',
                country: 'US',
                postalCode: '31217',
                email: '[email protected]',
              },
            },
          })
          setLoading(false)
          setToken(token)
          console.log('tipsi token',token)
        } catch (error) {
          setLoading(false)
          console.log('A Tipsi error occurred!' , error)
        }
      }


      makePayment = async () => {
        console.log('make payment started')
        setLoading(true)
            axios({
              method: 'POST',
              url: 'https://cloudfunction',
              data:{
                amount: 100,
                token: token.id,
                paymenttype: "card"
          },
        }).then(response => {
          console.log('response payment intent', response)
          setLoading(false)
        })
      }

Q2; It could be true, this I'll look into more tomorrow.

Q3; Payment_method_types is present in the function because the idea is to use the request.body.paymenttype in the function to create a payment intent for the payment types I would like to offer.

You're saying you don't use Firebase cloud functions, do you use another online database for this or are you handling everything local within the app and am I making it to complex then needed. As I understand local handling is possible but it is not secure?

what to do after makepayment .

how to confirm the payment from the cloudfn

@jack828 I have implemented 3d secure flow in out app, it's working fine on some cards but on some cards authentication fails and we get 'There was an unexpected error -- try again in a few seconds" error. Can you please explain why are we getting this error on some cards?

@mudassar6969 Hey, Unfortunately as it's to do with banking you're not going to get very helpful errors. However, you can add a lot more logging to your application and see where exactly that error string is coming from. It could be stripe or it could be this library.

The end result is usually "something wrong with the user and their bank", in which case the user needs to contact the bank to try and sort it.

Good luck sorting it!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

projectpublius picture projectpublius  路  37Comments

agrosner picture agrosner  路  25Comments

alibarisoztekin picture alibarisoztekin  路  33Comments

apolishch picture apolishch  路  56Comments

ecarrera picture ecarrera  路  40Comments