flutter-stripe / flutter_stripe

Flutter SDK for Stripe.
https://pub.dev/packages/flutter_stripe
930 stars 514 forks source link

How to get token of card without charging at moment #1763

Closed VimarsSpaceo closed 3 months ago

VimarsSpaceo commented 4 months ago

Describe the bug I am developing a feature to payment with card but the app flow is something like this First I have to get card details from user and pass it to stripe and stripe will return me a token for this particular card and I have to pass it to backend for future reference

And with the token of card I will trigger the payment without showing any SDK or native payment sheet

So big question is how to get card token Didn't find any method which will accept card details like card number, expiry date, CVV etc

Flucadetena commented 4 months ago

Hi @VimarsSpaceo we had a similar issue long time ago. Let me guide you through the solutions and problems we encountered and what we are migrating right now. Hope it helps.

Our current flow: The user adds the cards s/he wants to use in the future and we save only his "default/preferred" selection. When s/he wants to pay for something we retrieve his "favorite" card or all if he navigates to the "Card selection" page to change the one he wants to use.

Problems in this flow:

Solution:

To make this work you: On you server have to create functions to:

  1. To get or create a stripe client is pretty straight forward in the docs. But you do it with:
    
    /// To check if the user already exists I store his "stripeId" in the db and check if that value exists. If not I create it.

const customer = await stripe.customers.create({ /// Or any other data you may have an want to be linked to the stripe user. email: user.mail, /// Optional to save any reference you might want to link your db with the Stripe records. Or search in the Stripe dashboard. metadata: { anyReferenceYouWant: UserIdInYourDb }, });

2. Retrieve all the cards:

const paymentMethods = await stripe.paymentMethods.list({ customer: customer.id, type: "card" }); console.log("PaymentMethods: ", paymentMethods);

  const response = paymentMethods.data;
3. Add a new Card
- First you need a "setupIntent" from the server. This is an intent that represent an intention to charge the user in the future. And you need to return the "client_secret" to use it the the form and "saving" part.

const setupIntent = await stripe.setupIntents.create({ customer: customer.id }); console.log("setupIntent: ", setupIntent); return client_secret: setupIntent.client_secret;

- In flutter:

// You show the CardFormField widget wherever you want. The important part is the "cardDetails", you must check the info is complete. I disable the "Add" button until the data is complete. Like this: onPressed: _cardDetails == null || !_cardDetails!.complete ? null : ()=> saveNewCard(). CardFormField( /// This only affects Android. Except for the "backgroundColor" that also affects IOS. style: CardFormStyle( backgroundColor: Colors.transparent, borderColor: Theme.of(context).colorScheme.onBackground.withOpacity(.7), borderWidth: 1, borderRadius: themeBorderRadius.toInt(), cursorColor: Theme.of(context).colorScheme.onBackground, textColor: Theme.of(context).colorScheme.onPrimary, textErrorColor: Theme.of(context).colorScheme.error, placeholderColor: Theme.of(context).colorScheme.onBackground.withOpacity(.7), ), onCardChanged: (cardDetails) { /// This is the data you need. setState(() { _cardDetails = cardDetails; }); }, ),

// The you have a button saying "Add" or the like that will trigger the adding of the card to the users Stripe account. //Function to save payment method. You don't need to pass the info from the "CardDetails" object, stripe does that for you. Future saveNewCard() async { try { // 1. Create setup intent on backend final clientSecret = await _createSetupIntentOnBackend();

  // 3. Confirm setup intent
  await Stripe.instance.confirmSetupIntent(
      paymentIntentClientSecret: clientSecret,
      params: const PaymentMethodParams.card(paymentMethodData: PaymentMethodData()));

  await fetchPaymentMethods();
} catch (err) {
  showGetSnackBar(message: 'Something happen saving your new card, please try again later');
  rethrow;
}

}

Now you can retrieve the cards again to get the newly added card.

4. Delete existing card
- For this you need the cardId. When you retrieve all the users cards you get the "id" of the card among other data. Make him choose the one and send it to a function in your server.
Server function:

const deletedCard = await stripe.paymentMethods.detach(paymentMethodId); console.log("Deleted card: ", deletedCard);


You don't event need the CustomerId for this part.

***Things to consider**
- To avoid showing the user the same card multiple time you can check the "fingerprint" element in the card info. As the user can save multiple times the same card and the "Id" will be different, but the "fingerprint" wont.

DONE. This is the way I use but there is a newer way as the CardFormField is being deprecated. You can use the "CustomerSheet" that it is currently in beta on Futter.

- I'm currently working on this implementation, i'll get back to you tomorrow with how it works in case you prefer this method.

Hope it helps ;)
VimarsSpaceo commented 4 months ago

Hi @Flucadetena thanks for your help its working fine for me

need some more details about it like there is no any option available to add name of card holder also when I am set "enablePostalCode: false" then its only works in Android device in IOS i have to add postal code every time

Other feature to implement I have to implement Google pay and Apple pay in my app without showing any stripe payment sheet like I have to choose radio Button option from my UI and click on button to start Google pay and Apple pay payment flow

for this where can i find proper documentation

waiting for your reply and Thanks for help

VimarsSpaceo commented 4 months ago

Hi @Flucadetena suddenly I am getting an error to adding card in Android only the error is '' StripeException(error: LocalizedErrorMessage(code: FailureCode.Failed, localizedMessage: Card details not complete, message: Card details not complete, stripeErrorCode: null, declineCode: null, type: null)) ''

Device :- samsung galaxy f23 Android version :- 14 in IOS its working fine

Flucadetena commented 4 months ago

Hi @VimarsSpaceo I'm very glad I could help you. In relation to your questions, I cannot answer all of them, but I'll do my best.

1.- As far as I know you don't need the name of the person in the card. Nevertheless if you want that data tied up in the stripes customer account, you should collect it and add it to the "billingDetails" param that is present almos everywhere related to a payment or a paymentMethod. 2.- The enablePostalCode option, I have no idea. We do require the field. But you can open an issue in the Repo of "Stripe-IOS" or on this one. See if someone can find the solution. You can try providing a fix postal code in the init of the controller of the CardFormField, like this:

CardFormField(
     controller: CardFormEditController(
     initialDetails: CardFieldInputDetails(complete: false, postalCode: '3488'),
      ),
),

But make sure is the right one.

3.- Implement Google Pay and Apple pay. I think you can do this through the Google.dev pay package it is compatible with stripe, so you can link the payments.

4.- In relation to the "FailureCode.Failed" I have no idea, can you give me more details about the flow? or when it happens? Have you tried enabling back the postal code field, to see if that is the issue?

Hope this helps.

If you are still interested in knowing how to implement the new "CustomerSheet" let me know and I'll find some time this week to documented here for you ;)

VimarsSpaceo commented 4 months ago

Hi @Flucadetena

Everything is working fine Except 2 platforms specific issue with cardformfield

1) Android In this I am getting '' StripeException(error: LocalizedErrorMessage(code: FailureCode.Failed, localizedMessage: Card details not complete, message: Card details not complete, stripeErrorCode: null, declineCode: null, type: null)) ''

Error in catch block of save card method I have checked all scenarios regarding this Let me describe my add card flow

I have payment screen which have add card button, After clicking on this there is bottom sheet open which will have add card holder name text box below this I have cardformfield and then add card button

When I found card details is complete from onchange listener of card form field , I will hit the backend api for client secret Once api success, will hit the save card method and it's called in response of it it's going to catch block and Getting this error

So question is I am getting card details.completed true Then why the api response showing error for details is not compled

2) IOS.

In ios device everything is working fine in debug build But when I create a release build the cardformfield UI is showing blank box

I don't know why it's happening

Please check for above two issues, I am stucked at this moment

Flucadetena commented 4 months ago

Hi @VimarsSpaceo I'll try to do my best to help.

2.- I'll start with the second issue. (IOS - blank cardForm)

If none of the above helps, can you add imgs? Code snippets? so I have a bit more info.

1.- First Issue (Android)

Hope this helps ;)

VimarsSpaceo commented 3 months ago

Hi @Flucadetena thanks for reply

I Got the exact issue of IOS App

it's not about the build variant it's all about the Device Theme Config

when device is in dark mode then the cardform field widgets value is not visible but in functional way its working fine

so right now How to deal with dark mode (In my app I have to give only support for Light mode)

also I am checking for Android app issue....

Flucadetena commented 3 months ago

@VimarsSpaceo I see. I thought it might be that, but was not sure from your answer.

The form field is visible but colors make it look like it is not. The problem is the color of your bottom sheet. Set it to "Colors.red", for example, and the CarFormField style like this:

CardFormField(
                              style: CardFormStyle(
                                backgroundColor: Colors.transparent,
                              ),
                            )

You will se the colors of the lines, inputs, etc of the CardFormField change depending on the systems theme. This is why it may look like "blank".

There is no solution for this, unfortunately.

Things you can try / Alternatives:

1.- If you don't allow the user to change the theme. Meaning, the theme of your app will match that of the system. So In your MaterialApp:

MaterialApp(
      theme: lightTheme,
      darkTheme: darkTheme,
)

You set both dark and light, and Material will match the systems theme for you. This will solve the CardFormField problem, as your theme will always match the same as the Stripe package is using.

2.- Using the backgroundColor: Colors.transparent mentioned before in the style of the CardFormField. You can check what is the Current system theme mode/brightness like this:

final brightness = WidgetsBinding.instance.platformDispatcher.platformBrightness;

With this value you can choose what color do you want to give to your bottomSheet. So if the theme is "darkMode" you can show the bottom sheet with a darker background even if it is not inline with the current theme. At the end of the day it is only one element.

3.- You can migrate the system to use the new "CustomerSheet". This would change the flow in which a user manages its saved cards, but you can style it with your colors, set the "ThemeMode" independently from the current system mode and you can use it in your payment flow as well.

*This is the solution and end up going for as it looks the CardFormField is going to the "deprecated" soon. So I don't see them updating the form to set the "theme/brightness mode". If you choose to go this route and need a flow, let me know and i'll explain it here. Just tell me if you are switching to this system as it will take me some time to describe the whole process please ;)

VimarsSpaceo commented 3 months ago

@Flucadetena thank you so much for your help

I think the new customersheet will not going to fullfill the app flow and requirements so this is not suitable option to move forward

I have to check with alternative you provided

Also the issue in Android device which I am facing its being resolved

In this the conclusion is we have to show or visible the cardformfield until the card Added successfully

The mistake I am doing in this is I am closing the bottom sheet on add card button which is having cardformfield and then save card method called so getting error of card details is not complete

Once again @Flucadetena thank you so much for your support it's really appreciated

Flucadetena commented 3 months ago

@VimarsSpaceo Ohhh! that makes a lot of sense. I'm super happy you solved the problem and I was able to help in some way.

Let me know if the issue with the theme resolves before closing the issue ;)

VimarsSpaceo commented 3 months ago

Hi @Flucadetena

I have fixed the issue in ios device with changing its background color

Unfortunately there is no another way soo

Flucadetena commented 3 months ago

@VimarsSpaceo Yep. It is the only way.

Nevertheless happy you were able to solve it.

Don't forget to close the issue. ;)