Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

confirmSetupIntent not working as expected #371

Closed
fdelacruzsoto opened this issue Jun 25, 2021 · 28 comments
Closed

confirmSetupIntent not working as expected #371

fdelacruzsoto opened this issue Jun 25, 2021 · 28 comments
Labels
need triage question Further information is requested

Comments

@fdelacruzsoto
Copy link

Describe the bug

Currently when I try to call confirmSetupIntent I get the following error:

{
  "stripeErrorCode": null,
  "declineCode": null,
  "localizedMessage": "Card details not complete",
  "message": "Card details not complete",
  "type": null,
  "code": "Failed"
}

Card details are always complete

To Reproduce
Steps to reproduce the behavior:

This is my current code for the component

    <StripeProvider publishableKey="my test key">
      <CardField
        postalCodeEnabled={true}
        placeholder={{
          number: '4242 4242 4242 4242',
          expiration: '10/24',
          cvc: '123',
          postalCode: 'C.P.',
        }}
        cardStyle={{
          backgroundColor: '#FFFFFF',
          textColor: '#000000',
          borderWidth: 1,
          borderColor: COLOR.gray_100,
        }}
        style={{
          width: '100%',
          height: 50,
          marginVertical: SIZES.MD,
          borderRadius: SIZES.MD,
          ...STYLE_SHADOW,
        }}
        onCardChange={inputCard => {
          setCardDetails(inputCard);
        }}
        onFocus={focusedField => {
          setInputField(
            focusedField ? INPUT_FIELD[focusedField.toString()] : '',
          );
        }}
      />
      <Text>{inputField}</Text>
      <Button
        disabled={!cardDetails?.complete}
        onPress={submit}
        style={{
          marginTop: SIZES.MD,
          borderRadius: SIZES.MD,
          ...STYLE_SHADOW,
        }}>
        Guardar
      </Button>
    </StripeProvider>

And this is the submit function

  const submit = async () => {
    setLoading(true);
    try {
      await createOrUpdateClient();
      await updateProfile({
        variables: {
          input: {
            card: {
              last_digits: cardDetails?.last4,
              expiry_date: `${cardDetails?.expiryMonth}/${cardDetails?.expiryYear}`,
            },
          },
        },
      });
      const setupIntentResponse = await createSetupIntent();
      const billingDetails: PaymentMethods.BillingDetails = {
        email,
      };
      const { setupIntent, error } = await confirmSetupIntent(
        setupIntentResponse.data?.stripeSetUpIntent?.client_secret as string,
        {
          type: 'Card',
          billingDetails,
        },
      );
      console.log('LOG: error ', JSON.stringify(error, null, 2));
      console.log('LOG: setupIntent ', JSON.stringify(setupIntent, null, 2));
      if (error) {
        throw error;
      }
      Navigation.pop(componentId);
    } catch (error) {}
    setLoading(false);
  };

client_secret is always defined, so no issue there.

Expected behavior

The card is saved for future payments.

I debugged the issue directly on android studio and xcode, so I found that the native card manager is always null when it gets to the confirmSetupIntent native method on both platforms. I'm attaching the evidence from xcode.

Screenshots

Captura de Pantalla 2021-06-24 a la(s) 20 55 41

Desktop (please complete the following information):

  • OS: MacOS big sur (M1)

Smartphone (please complete the following information):

  • Device: I used the iOS simulator, the android emulator, an iPhone 11 and a moto g5 plus
  • OS: iOS 14, android 11

Additional context

Please, don't close this issue until you can help, this is becoming a huge block for our company.

@fdelacruzsoto
Copy link
Author

And btw, I also tried with initStripe instead of StripeProvider and it didn't work either.

@thorsten-stripe thorsten-stripe added enhancement New feature or request question Further information is requested need triage and removed enhancement New feature or request labels Jun 25, 2021
@stripe stripe deleted a comment from dearlove0007 Jun 25, 2021
@stupidtools
Copy link

stupidtools commented Jun 27, 2021

I wanted to second this. On android, I'm getting a null pointer exception when trying to run this because of a null card manager.

java.lang.NullPointerException
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.reactnativestripesdk.PaymentMethodCreateParamsFactory.createCardPaymentSetupParams(PaymentMethodCreateParamsFactory.kt:160)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.reactnativestripesdk.PaymentMethodCreateParamsFactory.createSetupParams(PaymentMethodCreateParamsFactory.kt:41)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.reactnativestripesdk.StripeSdkModule.confirmSetupIntent(StripeSdkModule.kt:479)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at java.lang.reflect.Method.invoke(Native Method)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:151)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at android.os.Handler.handleCallback(Handler.java:938)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at android.os.Handler.dispatchMessage(Handler.java:99)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:27)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at android.os.Looper.loop(Looper.java:223)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:226)
06-27 13:39:51.518 16650 16778 E unknown:ReactNative: 	at java.lang.Thread.run(Thread.java:923)
06-27 13:39:51.519 16650 16778 E unknown:ReactNative: Exception in native call```

@fdelacruzsoto
Copy link
Author

@stupidtools glad to hear I'm not the only one with this same issue, I've been debugging on both platforms, but haven't found anything in particular yet

@stupidtools
Copy link

stupidtools commented Jun 28, 2021

@fdelacruzsoto I found a work around, but it might not work depending on your payment flow. I'm trying to store credit cards during a checkout process (in which the user is buying something, not sure storing the card for future use), and then I want to keep them on file so that I can charge them again in the future. In this case, you can use the createPaymentMethod from the useStripe hook. The process is explained in depth here: https://stripe.com/docs/payments/accept-a-payment-synchronously?platform=react-native . What they don't tell you in that walkthrough is that you can still associate the payment method with the stripe customer and call it later. You just have to make sure you've created a stripe customer for the user before calling createPaymentMethod. You then pass the customerId to the stripe.paymentIntents.create method that you call on the server. This whole flow works with the SCA compliance stuff in the sdk as well, which is the main reason i have to use it. Hope that helps!

@fdelacruzsoto
Copy link
Author

Thanks!

Our flow need to store the card first and made the charge later without the client present, I was thinking about implementing the flow you mention as a workaround, the only issue is that it probably won't work for our business requirements :(

Anyways, if we can't fix this issue at least in the next week we either are going to change to another payments provider or implement a different flow and see how it works.

@stupidtools

This comment has been minimized.

@fdelacruzsoto
Copy link
Author

@thorsten-stripe I found the root cause for the error.

For some reason whenever I call confirmSetupIntent from my js code it calls the onDropViewInstance (android) and onDidDestroyViewInstance (iOS) on the native code, which removes the existing instance from the map you are using on both platforms to keep the instance reference.

I commented these both lines in both platforms and it works well, I did a couple of testing and it only keeps one instance of the card view even without the code to remove it.

https://github.com/stripe/stripe-react-native/blob/master/android/src/main/java/com/reactnativestripesdk/StripeSdkCardViewManager.kt#L64

https://github.com/stripe/stripe-react-native/blob/master/ios/CardFieldManager.swift#L12

I know that they may be a reason for keeping that code in that specific place, but may be a workaround will be to move that code these other lines, so in that way you remove the instance before creating it again maybe? I don't know if that works for you guys.

https://github.com/stripe/stripe-react-native/blob/master/android/src/main/java/com/reactnativestripesdk/StripeSdkCardViewManager.kt#L47

https://github.com/stripe/stripe-react-native/blob/master/ios/CardFieldManager.swift#L7

I'd be happy to submit a PR with this fix, but first I wanted to touch base with you and see if this change makes sense.

Btw, I suspect that this issue may be caused by wix react native navigation library, but I haven't found a way to verify so.

@arekkubaczkowski
Copy link
Collaborator

@fdelacruzsoto have you had a chance to verify if it's related with react native navigation library?
if not I can help you with that but please provide code snippet which contains also navigation configuration.

@Sunhat

This comment has been minimized.

@arekkubaczkowski

This comment has been minimized.

@fdelacruzsoto
Copy link
Author

@arekkubaczkowski the current configuration for the project is a bit complex but I can give you some steps of what I did to get to the screen with the card component.

  • Install wix react native navigation.
  • Set up the bottom tabs navigation pattern with at least two screens.
  • In one of those screens, use the push screen method to open a screen that will display the card component.
  • Create the client if it doesn't exist.
  • Get the client secret.
  • Hit button to confirmSetup.

As soon as the user hits the button the screen for some reason calls the ondestroy method that removes the reference to the card view and all the details are lost, which prompts the missing details error on the confirm setup call. The card component with all the data is still there and I can continue changing its data, so I'm 100% the screen was not destroyed.

The PR you rejected did actually fix the issue in both platforms.

@jackbridger
Copy link

jackbridger commented Jul 2, 2021

I believe I'm having the same/similar issue when trying to call confirmPayment

import {CardField, useStripe} from '@stripe/stripe-react-native';

function PaymentScreen() {
  const {confirmPayment} = useStripe();

  const [billingDetails, setBillingDetails] = useState(null);

  const handleBuy = async () => {
    const res = await buySomething({
      amount: 500
    });
    const clientSecret = res.data.clientSecret;
// This is a call to my backend being set as something like: "pi_************_secret_************"
    const {error, paymentIntent} = await confirmPayment(clientSecret, {
      type: 'Card',
      billingDetails,
    });

    if (error) {
      Alert.alert(`Error Code:${error.code}`, error.message);
    } else if (paymentIntent) {
      Alert.alert('Success', 'Payment Successful');
    }
  };

  return (
    <View>
      <CardField
        postalCodeEnabled={false}
        placeholder={{
          number: '4242 4242 4242 4242',
        }}
        onCardChange={cardDetails => {
          setBillingDetails(cardDetails);
        }}
      />
      <ConfirmButton title="Buy" onPress={handleBuy} />
    </View>
  );
}

Note: have removed some non-essential code e.g. styling, non-relevant imports

The error coming back is:

{code: "Failed", localizedMessage: "Card details not complete", message: "Card details not complete", declineCode: null, type: null, …}

On the backend it's more or less like this:

const Stripe = require('stripe');
const stripe = new Stripe(*MY_SK*);

const buySomething = async (data) => {

  const {amount} = data;

  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: 'gbp',
  });

  return {clientSecret: paymentIntent.client_secret};
};

Whatever I put into confirmPayment, I seem to get this same error back.

Thank you!

@prox2
Copy link

prox2 commented Jul 5, 2021

I am having the same issue as well !! :)
I have installed the example and run it and i tested it and was working fine but when i have integrated same steps in my app im getting the same error

@prox2
Copy link

prox2 commented Jul 5, 2021

OKAY, so finally worked for me and what i did was the following:
I have changed the page extension to type script (tsx) instead of javascript(js)
i have specified the type of billing billingDetails same as stated in example which to be something like:

 const billingDetails: PaymentMethodCreateParams.BillingDetails = {
            email: email,
        };

and after success result or fail must re-render to call initiate stripe again or will throw an error in case for example first attempt for adding the card failed, which i think is an issue related to stripe provider.

this may consider as temporary workaround the issue

@idcuesta
Copy link

idcuesta commented Jul 5, 2021

Background

We are in the process of migrating from tipsi-stripe to official react native library now due to Google Play Store not allowing anymore the version of stripe used by tipsi.

When migrating code, we have followed these steps:

  • When creating a new payment method (user adds a new card), we open a modal screen to capture payment method. We have replaced tipsi payment form with our own screen, and we are using CardField component. This screen calls stripe.createToken and returns the tokenId in a callback function.
  • From our checkout screen (totally different from the modal we use to capture a new card), and after user has assigned some payment method card, either by adding a new card (we work here with token), or by selecting from saved cards retrieved from our API (we work with paymentMethodId in this case), we make a call to confirmSetupIntent, passing as parameters either the token or paymentMethodId (by the time we make this call there is no UI related to stripe on screen).

Note: ConfirmSetupIntent.Param spec doesn't consider any card parameter except for type.

Issues found

After calling confirmSetupintent, we are getting these errors:

  • On iOS, we get error Card details not complete when passing paymentMethodId instead of token.
  • On Android, app is crashing in both scenarios.

iOS investigation

When inspecting what happens under the hood when calling confirmSetupIntent on iOS, we can see that StripeSdk.swift#confirmSetupIntent is not handling paymentMethodId parameter, so because by the time this is called there is no cardFieldView and no token param is passed, error PaymentMethodError.cardPaymentMissingParams is resolved.

Android investigation

When inspecting what happens under the hood when calling confirmSetupIntent on Android, we can see that StripeSdkModule#confirmSetupIntent is not handling token or paymentMethodId parameters, so because by the time this is called there is no instance, and this causes cardParams to be null, app is throwing an exception when trying to create card params, and trying to use card!!

Conclusion

When using tipsi-stripe, they are under the hood calling stripe API V1 manually, and they handle passing paymentMethodId to POST v1/setup_intents/:id/confirm, but when replacing it with this library, paymentMethodId is ignored and not passed correctly. On top of that, Android is not handling correctly passing a token parameter.

@prox2
Copy link

prox2 commented Jul 5, 2021

so how to over come this issue? @idcuesta

@idcuesta
Copy link

idcuesta commented Jul 6, 2021

@prox2 I have just created a PR with the fixes we did to get our flows working.

@thorsten-stripe
Copy link
Contributor

When creating a new payment method (user adds a new card), we open a modal screen to capture payment method. We have replaced tipsi payment form with our own screen, and we are using CardField component. This screen calls stripe.createToken and returns the tokenId in a callback function.

Tokens are considered legacy, you should call confirmSetupIntent here instead rather than creating a token.

From our checkout screen (totally different from the modal we use to capture a new card), and after user has assigned some payment method card, either by adding a new card (we work here with token), or by selecting from saved cards retrieved from our API (we work with paymentMethodId in this case), we make a call to confirmSetupIntent, passing as parameters either the token or paymentMethodId (by the time we make this call there is no UI related to stripe on screen).

Here you should confirm the PaymentIntent server-side using the selected payment method ID. If you already have a list of setup payment methods for the customer, you don't need a SetupIntent at this point.

@riordanpawley
Copy link

riordanpawley commented Jul 8, 2021

@arekkubaczkowski the current configuration for the project is a bit complex but I can give you some steps of what I did to get to the screen with the card component.

  • Install wix react native navigation.
  • Set up the bottom tabs navigation pattern with at least two screens.
  • In one of those screens, use the push screen method to open a screen that will display the card component.
  • Create the client if it doesn't exist.
  • Get the client secret.
  • Hit button to confirmSetup.

As soon as the user hits the button the screen for some reason calls the ondestroy method that removes the reference to the card view and all the details are lost, which prompts the missing details error on the confirm setup call. The card component with all the data is still there and I can continue changing its data, so I'm 100% the screen was not destroyed.

The PR you rejected did actually fix the issue in both platforms.

I have exactly the same issue

@fdelacruzsoto
Copy link
Author

@thorsten-stripe @arekkubaczkowski any chance you are looking into this issue? It seems a lot of people is facing the same problem, and I already give you guys steps to reproduce the error.

My company is close to launching our apps and we heavily depend on this, so this is becoming a major blocking for us.

We can get into a call and I can give you details if that helps.

We really need this to get fixed 😢

@jheslop25
Copy link

When creating a new payment method (user adds a new card), we open a modal screen to capture payment method. We have replaced tipsi payment form with our own screen, and we are using CardField component. This screen calls stripe.createToken and returns the tokenId in a callback function.

Tokens are considered legacy, you should call confirmSetupIntent here instead rather than creating a token.

From our checkout screen (totally different from the modal we use to capture a new card), and after user has assigned some payment method card, either by adding a new card (we work here with token), or by selecting from saved cards retrieved from our API (we work with paymentMethodId in this case), we make a call to confirmSetupIntent, passing as parameters either the token or paymentMethodId (by the time we make this call there is no UI related to stripe on screen).

Here you should confirm the PaymentIntent server-side using the selected payment method ID. If you already have a list of setup payment methods for the customer, you don't need a SetupIntent at this point.

@thorsten-stripe So this is the problem that we are running into. In our case we are launching in the UK so we will need 3D secure. From our reading of the docs it seems like confirming the payment intent server side is not going to work with 3Dsecure.

For background we are migrating from tipsi (like basically everyone else). Our workflow is a bit more complicated than normal because we are building a platform integration with connected accounts. We don't want the payment methods directly exposed to each vendor on the platform so when the user adds a stored payment method to an order we create a payment intent with a temporary token of the payment method that allows the connected account to make a one time charge to the stored card. With the old library we could simply call the stripe.confirmPaymentIntent() function and pass the client secret of the payment intent. Now it seems like that functionality doesn't exist in your library, which is profoundly frustrating to say the least. I suppose we can write up some functions to hit the api endpoints manually, but that pretty much defeats the purpose of having an SDK in the first place.

Can you offer any insight here?

@jspauld
Copy link

jspauld commented Jul 11, 2021

I'm using react-navigation and was able to work around this issue by moving the credit card screen from a modal stack navigator

<Stack.Navigator mode="modal">

to a normal one

<Stack.Navigator>

No "Card details not complete" error after this

@thorsten-stripe
Copy link
Contributor

@fdelacruzsoto can you please try #371 (comment)

@jheslop25

From our reading of the docs it seems like confirming the payment intent server side is not going to work with 3Dsecure.

When you already have a payment method ID you should create the PaymentIntent with said method and confirm:true and only call confirmPayment on the client if needed. That way you minimize the amount of API calls needed. See the docs: https://stripe.com/docs/payments/save-and-reuse?platform=react-native#react-native-create-payment-intent-off-session

@jheslop25
Copy link

@thorsten-stripe Thanks! We managed to sort it out. It took a few hours of bending our heads around the docs, but we got it. I think the confusion came from the fact that calling confirmPaymentIntent() from the client-side was a bit of a useful hack in the old libraries, but not the standard workflow. As far as I can tell stripe intends for the payment intent to be confirmed from the server and only from the client if unavoidable. Of course, I'm generalizing. Stripe is a rather large ecosystem at this point and it's hard to capture everything succinctly in a few words.

@fdelacruzsoto
Copy link
Author

@thorsten-stripe as I said before, we are using wix react native navigation, not react-navigation

@dhairyasenjaliya
Copy link

I believe I'm having the same/similar issue when trying to call confirmPayment

import {CardField, useStripe} from '@stripe/stripe-react-native';

function PaymentScreen() {
  const {confirmPayment} = useStripe();

  const [billingDetails, setBillingDetails] = useState(null);

  const handleBuy = async () => {
    const res = await buySomething({
      amount: 500
    });
    const clientSecret = res.data.clientSecret;
// This is a call to my backend being set as something like: "pi_************_secret_************"
    const {error, paymentIntent} = await confirmPayment(clientSecret, {
      type: 'Card',
      billingDetails,
    });

    if (error) {
      Alert.alert(`Error Code:${error.code}`, error.message);
    } else if (paymentIntent) {
      Alert.alert('Success', 'Payment Successful');
    }
  };

  return (
    <View>
      <CardField
        postalCodeEnabled={false}
        placeholder={{
          number: '4242 4242 4242 4242',
        }}
        onCardChange={cardDetails => {
          setBillingDetails(cardDetails);
        }}
      />
      <ConfirmButton title="Buy" onPress={handleBuy} />
    </View>
  );
}

Note: have removed some non-essential code e.g. styling, non-relevant imports

The error coming back is:

{code: "Failed", localizedMessage: "Card details not complete", message: "Card details not complete", declineCode: null, type: null, …}

On the backend it's more or less like this:

const Stripe = require('stripe');
const stripe = new Stripe(*MY_SK*);

const buySomething = async (data) => {

  const {amount} = data;

  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: 'gbp',
  });

  return {clientSecret: paymentIntent.client_secret};
};

Whatever I put into confirmPayment, I seem to get this same error back.

Thank you!

Did you find any solution?

@arekkubaczkowski
Copy link
Collaborator

We think that this issue is related with other reported issue but we are still trying to find a solution, you can track the progress here: #391

@thorsten-stripe
Copy link
Contributor

This has been fixed in #436 and will roll out in v0.1.6

If you need it early you can follow these steps to build and install the library locally: https://github.com/stripe/stripe-react-native/blob/master/CONTRIBUTING.md#install-library-as-local-repository

Sorry that it took so long to get this resolved. The cause of this issue was not obvious and it took a time until we were able to reproduce it and find a non-breaking solution. It turned out that it occurs only on specific RN versions, as a solution we needed to figure out some other way to communicate two native modules which is somewhat tricky to do over the react-native bridge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
need triage question Further information is requested
Projects
None yet
Development

No branches or pull requests