expo / stripe-expo

Use the Stripe HTTP API in Expo without the DOM, node, or native deps
MIT License
159 stars 23 forks source link

Stripe: Setting up future payments not working as expected. #46

Open Brainilio opened 2 years ago

Brainilio commented 2 years ago

Hi all! I’m in desperate need for help.

So I have a side project for an iOS app using Expo / React Native. And I'm having issues with setting up future payment methods using Stripe & Expo’s stripe library.

Our back-ender set up a graphql back-end, and provides me with all the variables I need. I’m trying to se up payment methods to charge clients later, but I’m having trouble having with the paymentIntentSheet not showing up after creating an intent and fetching the clientSecret, ephemeralKey and customerId from our back-end. Now i don’t know where the issue is.. Is it because of me using the wrong versions? Maybe incorrect installation? Are the variables I’m using right..?

I used the following documentation page(s) as a guide: https://stripe.com/docs/payments/save-and-reuse?platform=react-native https://github.com/stripe/stripe-react-native#expo

These are the version numbers of the libraries I’m using, relevant to this topic/issue:

"expo": "~41.0.1",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-41.0.0.tar.gz",
"@stripe/stripe-react-native": "0.1.1"

These are the steps I took:

  1. Install stripe-react-native, and add it to my app.json as a plugin:

    "plugins": [
            [
                "@stripe/stripe-react-native",
                {
                    "merchantIdentifier": "",
                    "enableGooglePay": false
                }
            ]
        ],
  2. On global level, I import the StripeProvider component and pass down the given publishable key: pk_live_51[.....]

On global level it’ll look like this:

<StripeProvider
publishableKey="pk_live_51[...]"
        >
            <AuthProvider>
                <ApolloProvider client={client}>
                    <InnerApp />
                </ApolloProvider>
            </AuthProvider>
        </StripeProvider>
  1. Then according to the stripe docs, at the location where I’ll be using the bulk of the logic, I am supposed to fetch the setupIntent, ephemeralKey, and the customer from the back-end, in this case, in the useEffect of my component. I was provided with a graphql mutation to obtain these values:
mutation (
        $createUserPaymentMethodSetupIntentInput: CreateUserPaymentMethodSetupIntentInput!
    ) {
        createUserPaymentMethodSetupIntent(
            input: $createUserPaymentMethodSetupIntentInput
        ) {
            setupIntentId
            clientSecret
            customerId
            ephemeralKeySecret
        }
    }

Then, I call the function that will eventually provide me with all the necessary variables:


createIntent({
            variables: {
                createUserPaymentMethodSetupIntentInput: {
                    userUid: userUid,
                },
            },
        })
            .then((res) => {
                const clientSecret =
                    res.data.createUserPaymentMethodSetupIntent.clientSecret
                const setupIntentId =
                    res.data.createUserPaymentMethodSetupIntent.setupIntentId
                const ephemeralKeySecret =
                res.data.createUserPaymentMethodSetupIntent.ephemeralKeySecret
                const customerId =
                    res.data.createUserPaymentMethodSetupIntent.customerId

                // IGNORE THIS FOR NOW
                initializePaymentSheet(
                    clientSecret,
                    setupIntentId,
                    ephemeralKeySecret,
                    customerId
                )
            })
            .catch((err) => console.log({ graphqlError: err }))

The function gives me the following response:

Object {
  "data": Object {
    "createUserPaymentMethodSetupIntent": Object {
      "__typename": "CreatedUserPaymentMethodSetupIntent",
      "clientSecret": "seti_1K[....]",
      "customerId": "cus_[...]",
      "ephemeralKeySecret": "ek_live_[...]",
      "setupIntentId": "seti_[...]",
    },
  },
  1. According to the docs, I should use the setupIntent, ephemeralKey, and customer values as variables in ONE of their given functions/hooks called “initPaymentSheet” that should initialize the paymentsheet on their end.

These functions are imported like this:

const { initPaymentSheet, presentPaymentSheet } = useStripe();

In step 3, you see that I call a function that then calls the initPaymentSheet after successfully fetching the values from the server.

initializePaymentSheet(
                    clientSecret,
                    setupIntentId,
                    ephemeralKeySecret,
                    customerId
                )

The initializePaymentSheet looks like this:

const initializePaymentSheet = (
        clientSecret,
        setupIntentId,
        ephemeralKeySecret,
        customerId
    ) => {
        initPaymentSheet({
            customerId: customerId,
            customerEphemeralKeySecret: ephemeralKeySecret,
            setupIntentClientSecret: setupIntentId,
        })
            .then((res) => {
                console.log(res)
                setDisabledButton(false)
            })
            .catch((err) => console.log("error.."))
    }

As you can see, I call the initPaymentSheet hook there, exactly like shown on the docs, and pass in the values i received from the back-end. However, after doing this i get the following error in the console:

Object {
  "error": Object {
    "code": "Failed",
    "message": "You must provide the paymentIntentClientSecret",
  },
}

This didn’t seem like a huge error, so I went ahead and changed the initPaymentSheet parameters by adding the paymentIntentClientSecret field and passed in the clientSecret value which wasn’t previously used:

initPaymentSheet({
            customerId: customerId,
            customerEphemeralKeySecret: ephemeralKeySecret,
            setupIntentClientSecret: setupIntentId,
            paymentIntentClientSecret: clientSecret
        })
            .then((res) => {
                console.log(res)
                setDisabledButton(false)
            })
            .catch((err) => console.log("little error.."))

After calling the function and seeing the error disappear, and the console.log shown above logs the following in the console:

Object {
  "paymentOption": null,
}

I didn’t think too much of this, and thought it says null just because I have no previously set paymentOptions. I was just happy there were no more errors. In the .then chain, you see that i enable a button that basically allows a user to call a function that would present a payment sheet where users can submit their paymentMethod. This button is disabled, because I think you should initialize the paymentSheet first before presenting it?

<WideButton
                disabled={disabledButton}
                text="Add New Payment Method"
                clicked={openPaymentSheet}
            />
  1. Anyways, now that the button is finally enabled, the user can click on it and it'll call the following function:
const openPaymentSheet = async () => {
        setDisabledButton(true)
        const { error, paymentOption } = await presentPaymentSheet()

        if (error) {
            console.log(error)
            setDisabledButton(false)
            Alert.alert(`Error code: ${error.code}`, error.message)
        }

        if (paymentOption) {
            setDisabledButton(false)
            Alert.alert(
                "Success",
                "Your payment method is successfully set up for future payments!"
            )
            console.log(paymentOption)
        }

    }

Now to quote the stripe docs: When your customer taps the Set up button, call presentPaymentSheet() to open the sheet. After the customer completes setting up their payment method for future use, the sheet is dismissed and the promise resolves with an optional StripeError.

So, that's exactly what I did: Call the presentPaymentSheet, but then i get the following error:

Object {
  "code": "Failed",
  "message": "There was an unexpected error -- try again in a few seconds",
}

Now this is where I’m stuck, because it doesn’t provide me with any more information than given above. I’ve tried looking everywhere, and some resources tell me that I should update my stripe, some say i should add stripe to my plugins in app.json. I’ve done all of that and I can’t still figure it out.

Here is a video showing you the behavior in action: https://user-images.githubusercontent.com/29804130/146274443-82c581ba-8913-4c87-ad2e-5b8719680fed.mov

Here is the code of the entire component:


// steps
// 1. call graphql query to set up intent, retrieve the clientsecret and setupintentid
// 2. call stripes initPaymentSheet's function and pass in useruid, clientsecret and setupintentid
// 3. when  initpaymentsheet is ready, enable button for user to add payment information
// 4. Retrieve the payment information and call the createpaymentmethod mutation
// 5. disable button again, and refresh page

export default function PaymentMethods({ userUid }) {
    const { initPaymentSheet, presentPaymentSheet } = useStripe()

    const [disabledButton, setDisabledButton] = useState(false)

    const [createIntent, { data, loading, error }] = useMutation(
        ADD_PAYMENT_METHOD_INTENT
    )

    useEffect(() => {
        createUserPaymentMethodIntent()
    }, [])

    const createUserPaymentMethodIntent = () => {
        setDisabledButton(true)

        createIntent({
            variables: {
                createUserPaymentMethodSetupIntentInput: {
                    userUid: userUid,
                },
            },
        })
            .then((res) => {
                console.log(res)

                const clientSecret =
                    res.data.createUserPaymentMethodSetupIntent.clientSecret
                const setupIntentId =
                    res.data.createUserPaymentMethodSetupIntent.setupIntentId
                const ephemeralKeySecret =
                    res.data.createUserPaymentMethodSetupIntent.ephemeralKeySecret
                const customerId =
                    res.data.createUserPaymentMethodSetupIntent.customerId

                initializePaymentSheet(
                    clientSecret,
                    setupIntentId,
                    ephemeralKeySecret,
                    customerId
                )
            })
            .catch((err) => console.log({ graphqlError: err }))
    }

    const initializePaymentSheet = (
        clientSecret,
        setupIntentId,
        ephemeralKeySecret,
        customerId
    ) => {
        initPaymentSheet({
            customerId: customerId,
            customerEphemeralKeySecret: ephemeralKeySecret,
            setupIntentClientSecret: setupIntentId,
            paymentIntentClientSecret: clientSecret,
        })
            .then((res) => {
                console.log(res)
                setDisabledButton(false)
            })
            .catch((err) => console.log("little error.."))
    }

    const openPaymentSheet = async () => {
        setDisabledButton(true)

        const { error } = await presentPaymentSheet()

        if (error) {
            Alert.alert(`Error code: ${error.code}`, error.message)
        } else {
            Alert.alert(
                "Success",
                "Your payment method is successfully set up for future payments!"
            )
        }
    }

    return (
        <ScrollView>
            <PaymentMethodList userUid={userUid} />
            <WideButton
                disabled={disabledButton}
                text="Add New Payment Method"
                clicked={openPaymentSheet}
            />
        </ScrollView>
    )
}

someone plz help :(