Skip to content
This repository has been archived by the owner on Apr 19, 2022. It is now read-only.

PaymentIntent - Same card is saved multiple times #45

Closed
yns01 opened this issue Feb 12, 2019 · 44 comments
Closed

PaymentIntent - Same card is saved multiple times #45

yns01 opened this issue Feb 12, 2019 · 44 comments
Assignees

Comments

@yns01
Copy link

yns01 commented Feb 12, 2019

Hey @thorsten-stripe, how have you been?

I'm playing around with the PaymentIntent API and I managed to attach a source to a customer. However, I noticed that if the user enters the same card he already saved, it will be attached twice as you can see here:
Example here:

{
  "id": "cus_BLPgxMRoUs4FYa",
  "object": "customer",
  "account_balance": 0,
  "created": 1504624833,
  "currency": null,
  "default_source": "src_1E1d3HC1oEzKtlmeA2H41jYh",
  "delinquent": false,
  "discount": null,
  "email": "[email protected]",
  "invoice_prefix": "99364FD",
  "invoice_settings": {
    "custom_fields": null,
    "footer": null,
    "supported_payment_methods": {
    }
  },
  "invoicing": {
  },
  "livemode": false,
  "metadata": {
  },
  "shipping": null,
  "sources": {
    "object": "list",
    "data": [
      {
        "id": "src_1E1d3HC1oEzKtlmeA2H41jYh",
        "object": "source",
        "amount": null,
        "card": {
          "address_line1_check": null,
          "address_zip_check": "unchecked",
          "brand": "Visa",
          "country": "US",
          "cvc_check": "unchecked",
          "dynamic_last4": null,
          "exp_month": 10,
          "exp_year": 2020,
          "fingerprint": "L5ePhLsSDFLcFTpi",
          "funding": "credit",
          "last4": "4242",
          "name": "Core_test2 Selenium",
          "tokenization_method": null
        },
        "client_secret": "src_client_secret_EUcHskjJmhzhaARK16hqneIc",
        "created": 1549646531,
        "currency": null,
        "customer": "cus_BLPgxMRoUs4FYa",
        "flow": "none",
        "livemode": false,
        "metadata": {
        },
        "owner": {
          "address": {
            "city": null,
            "country": null,
            "line1": null,
            "line2": null,
            "postal_code": "90210",
            "state": null
          },
          "email": null,
          "name": "Core_test2 Selenium",
          "phone": null,
          "verified_address": null,
          "verified_email": null,
          "verified_name": null,
          "verified_phone": null
        },
        "statement_descriptor": null,
        "status": "chargeable",
        "type": "card",
        "usage": "reusable"
      },
      {
        "id": "src_1E1eyEC1oEzKtlme6XcZqdkj",
        "object": "source",
        "amount": null,
        "card": {
          "address_line1_check": null,
          "address_zip_check": "unchecked",
          "brand": "Visa",
          "country": "US",
          "cvc_check": "unchecked",
          "dynamic_last4": null,
          "exp_month": 10,
          "exp_year": 2020,
          "fingerprint": "L5ePhLsSDFLcFTpi",
          "funding": "credit",
          "last4": "4242",
          "name": "Core_test2 Selenium",
          "tokenization_method": null
        },
        "client_secret": "src_client_secret_EUeGn9wV6rJdkzwEuvy8ZQkh",
        "created": 1549653906,
        "currency": null,
        "customer": "cus_BLPgxMRoUs4FYa",
        "flow": "none",
        "livemode": false,
        "metadata": {
        },
        "owner": {
          "address": {
            "city": null,
            "country": null,
            "line1": null,
            "line2": null,
            "postal_code": "90210",
            "state": null
          },
          "email": null,
          "name": "Core_test2 Selenium",
          "phone": null,
          "verified_address": null,
          "verified_email": null,
          "verified_name": null,
          "verified_phone": null
        },
        "statement_descriptor": null,
        "status": "chargeable",
        "type": "card",
        "usage": "reusable"
      },
    ],
    "has_more": false,
    "total_count": 6,
    "url": "/v1/customers/cus_BLPgxMRoUs4FYa/sources"
  },
  "subscriptions": {
    "object": "list",
    "data": [

    ],
    "has_more": false,
    "total_count": 0,
    "url": "/v1/customers/cus_BLPgxMRoUs4FYa/subscriptions"
  },
  "tax_info": null,
  "tax_info_verification": null
}


You can see that the fingerprint are exactly the same (which is normal as it’s the same card). How can we prevent that?

Regards,

Yanis.

@thorsten-stripe
Copy link
Contributor

@yns01 thanks for raising this! I'm discussing with the team and will get back to you.

Quick question on the use-case. Do you give the customer the option to select their stored card(s) instead of typing in the card number?

Therefore the scenario would be:

  1. Instead of selecting their stored card, they click on add new card
  2. they provide the same card details as the card they have already stored

Is that correct?

@thorsten-stripe thorsten-stripe self-assigned this Feb 13, 2019
@yns01
Copy link
Author

yns01 commented Feb 13, 2019

@thorsten-stripe We give the customer the option to select one of their stored credit card but also to enter a new one to pay and eventually store it if they chose to.

The scenario is correct, yes.

@thorsten-stripe
Copy link
Contributor

@yns01 thanks. We're looking at options for automatic deduping, will keep you posted here. As it stands today you have two options:

  1. Check the sources list on the customer and detach proactively yourself: https://stripe.com/docs/api/sources/detach

  2. Check the source fingerprint against the customer's source list before confirming the PaymentIntent:

  1. Client: createSource: https://stripe.com/docs/stripe-js/reference#stripe-create-source
  2. Server: check fingerprints
  3. Client: handleCardPayment with source and save_payment_method depending on the outcome of the check: https://stripe.com/docs/stripe-js/reference#stripe-handle-card-payment-no-element

Hope this works for you in the meantime.

@yns01
Copy link
Author

yns01 commented Feb 27, 2019

@thorsten-stripe Any new on a more robust and permanent solution?

@happygts
Copy link

Any updates on this guys ?

@thorsten-stripe
Copy link
Contributor

We're working on a more elegant solution, in the meantime, please use one of the deduplication approaches outlined above. You can find an example implementation here: dffb17f

@nticaric
Copy link

Any updates on this one?

@thorsten-stripe
Copy link
Contributor

@nticaric no automatic deduping yet, but you can utilise the manual confirmation integration flow[0] which allows you to check the PaymentMethod before creating/confirming the PaymentIntent.

[0] https://stripe.com/docs/payments/payment-intents/quickstart#manual-confirmation-flow

@thorsten-stripe
Copy link
Contributor

Not yet the deduping functionality you've been hoping for, but wanted to quickly follow-up that post-payment attachment of a payment method to a customer object is now available via the SCA off-session payment APIs[0], which might be helpful for some of you.

[0] https://stripe.com/docs/payments/cards/saving-cards#saving-card-after-payment

@thorsten-stripe
Copy link
Contributor

We've published a video that specifically looks at customer management / card-on-file with regard to SCA which you might find helpful if you landed here: https://youtu.be/52oinv6BZ34

@Pyo25
Copy link

Pyo25 commented Aug 21, 2019

For those who use PaymentMethod.attach method to add a payment method to a customer server-side, here is a solution (in Ruby, using stripe-ruby) that checks if the same card is already linked to the customer before attaching it:

found = false
Stripe::PaymentMethod.list(customer: stripe_customer_id, type: "card").each do |pm|
  if  pm.card.fingerprint == new_pm.card.fingerprint &&
      pm.card.exp_month == new_pm.card.exp_month &&
      pm.card.exp_year == new_pm.card.exp_year
    found = true
  end
end
unless found
  Stripe::PaymentMethod.attach(new_pm.id, {
    customer: stripe_customer_id,
  })
end

Does it make sense @thorsten-stripe ?

@thorsten-stripe
Copy link
Contributor

@Pyo25 Yes, that works, and good to call out that the fingerprint stays the same even if the card number gets a new expiration date. One quick note, in an ideal integration you'd be checking against the data in your own DB instead of listing all PMs for the customer.

@Pyo25
Copy link

Pyo25 commented Aug 28, 2019

Yeah indeed!

@MusabAkram
Copy link

I am using handleCardPayment method and i am saving card for future use in it. This methods add card every time weather the result is success or error. Can anyone help with it? I want to add it only when the payment succeeds.
Here's the code:
const { paymentIntent, error } = await stripe.handleCardPayment(
this.state.client_secret,
this.state.cardNumber,
{
setup_future_usage: 'off_session' // Save card details for future usage
}
);

@pasevin
Copy link

pasevin commented Sep 23, 2019

@Pyo25 Yes, that works, and good to call out that the fingerprint stays the same even if the card number gets a new expiration date. One quick note, in an ideal integration you'd be checking against the data in your own DB instead of listing all PMs for the customer.

I think fingerprint stays the same even when billing address and name details change (at least with test card it did), so need to check for that too and if it doesn't match, then update your DB and Stripe with new details.

@GrafGenerator
Copy link

Thanks @Pyo25!

@thorsten-stripe, I agree with your point about ideal integration way, but there is also point about service that allows you to install and use with almost no-thinking-required way. From that point of view I would say that having an option to perform idempotent operation on payment method is a must have. What if introduce this as option to payment method attach (both through PaymentMethod API or PaymentIntent API)? So if I want it, they will simply reuse existing payment method, as long as it's clearly distinguishable by (fingerprint, exp. month, exp. year).

@thomasbalsloev
Copy link

@GrafGenerator
Agree!

@lantanios
Copy link

I think Stripe API could just overwrite duplicated card with the newer one and save us trouble adding unnecessary "remove duplicates" code.
Is there any reason not to do so?

@howdoeyeknowyou-machine
Copy link

After integrating the stripe API into my app for a few hours I also came across this issue. Any updates on de-duping? Seems like something that should just work at the stripe level.

@leoplct
Copy link

leoplct commented Feb 4, 2020

+1 Having the same issue.

@sanjeet-iws
Copy link

Any news about this?

@aliusman
Copy link

A simple check to check duplicate card OR manual option to Allow/Disallow duplicate cards should do the trick, Why this has not been addressed? :)

@thorsten-stripe
Copy link
Contributor

I understand that you are looking for a way to automatically de-duplicate payment methods upon saving, however the experience for this is very specific to individual use cases. For example, why is the user entering the same card again instead of selecting the stored one? Are they trying to update their billing details? As you know your customers best, we recommend the approaches outlined above and adapting them to your customer's needs.

To make things easier, we have updated the saving behaviour to only attach payment methods to the customer if the payment was successful. This avoids the situation where potentially invalid payment methods are saved to the customer. You can learn more in our guide: https://stripe.com/docs/payments/save-during-payment

We've noted your desire for an option to automatically de-duplicate and are actively exploring this as we work to improve the ergonomics around payment method attachment.

For the time being, I'm going to close out this issue as it doesn't directly relate to the demo. If you have additional questions about this, please reach out to support using the form at https://support.stripe.com/ (preferred) or via email to [email protected].

Thanks everyone for the valuable feedback! To get updates vial email you can subscribe to our developer digest at the bottom right of docs: https://stripe.com/docs

@bootexe
Copy link

bootexe commented Apr 20, 2020

I have noticed that the number of duplicata card is equal to the number of time that the stripe form has been mount and unmount

I think is due to eventListener that has not been delete I think

@arnisjuraga
Copy link

This is working example for duplicate payment methods avoidance for PHP

// Response from frontent JS
$json_str = file_get_contents('php://input');
$body = json_decode($json_str);

// Retrieve confirmedpayment method
$paymentMethod = \Stripe\PaymentMethod::retrieve($body->payment_method);

// Get all customer CARD payment methods
$methods = \Stripe\PaymentMethod::all([
	'customer' => $this->customer->id, // $this->customer->id is running PHP object instance customer ID variable.
	'type'     => 'card',
]);


if ($methods) {
	foreach ($methods->data as $card) {
		if (
			$card->card->fingerprint == $paymentMethod->card->fingerprint &&
			$card->card->exp_month == $paymentMethod->card->exp_month &&
			$card->card->exp_year == $paymentMethod->card->exp_year
		) {
			// set matching payment method as current.
			$paymentMethod = $card;
			break;
		}
	}
}

// Attach to customer "again" (not needed?)
$paymentMethod->attach([
	'customer' => $this->customer->id,
]);

@tuanle07
Copy link

@thorsten-stripe : This issue is affecting the hosted invoice page by Stripe now. Every time a customer pays an invoice on the hosted invoice page, it keeps saving the same card every single time. I don't know how to solve this issue at all because it's out of my hand to control how the hosted invoice page works.

@GrafGenerator
Copy link

@thorsten-stripe,
My reply is quite late but anyway.
I think you're right that customers behavior is first-place factor to build payments pipeline. But

  1. It's really strong presumption that customers are aware of all features that payment gateways and payment features in products built on top of that payment gateways. This may be reasonable for some geekish or at least advanced-pc-users, but in general it's seems to be hard thing to learn for many.

  2. I can't get it why I need such denormalization for payment method, to me it seems that once payment method is identified by card's fingerprint and expiry date (in scope of one Stripe's customer) I can safely reuse it. Or there's some internal reason for this?

@thecatontheflat
Copy link

Same problem here

@akadebnath
Copy link

I am providing customers to use an already saved card, but the option to add new card too.
Although, for some people it may be pretty obvious, someone may not be so savy and type in the card details every time they want to make a purchase.

image

I don't know why it is so difficult for Stripe to detect a duplicate card and replace/update the old one. They are just ignoring their customer's voice and closing issues without resolving the issue at their end.

@devkdouglass
Copy link

Below is a PHP example that detaches all duplicated cards from a customer. This should be called after a successful transaction.

$stripe = new StripeClient('STRIPE_SECRET_KEY');

$cards = $stripe->paymentMethods->all(['customer' => 'CUSTOMER_ID', 'type' => 'card']);

$fingerprints = [];

foreach ($cards as $card) {
   $fingerprint = $card['card']['fingerprint'];

    if (in_array($fingerprint, $fingerprints, true)) {
      $stripe->paymentMethods->detach($card['id']);
   } else {
     $fingerprints[] = $fingerprint;
   }
}

@broglia
Copy link

broglia commented Oct 7, 2020

Hi, these solutions seem good for deduplicating cards returned by the PaymentMethod List API.
Although if the goal is to show the list of cards "saved for future payments" it might not be enough. I believe the List API returns all payment methods, even those which were not flagged for "future use".
Is there an easy way to retrieve only cards saved for future use?

@Skorpyon
Copy link

@thorsten-stripe Why you closed issue?

We've got the same:
image

@whyisusernamealwaystaken

@thorsten-stripe I'd also prefer a simple solution to this like setting an option in the PaymentMethod.attach method like { update: true } or something along this line which looks for the same fingerprint in customers attached cards and just updates changed information (like expiration date) if found and returns the PaymentMethod or just returns it if no changes need to be made, done.

I really feel that something like this should be part of the API since every dev is running into this kind of issue sooner or later.

@kg-2
Copy link

kg-2 commented Jul 14, 2021

I understand that stripe is a huge and complex ecosystem and that the developers want to be cautious to avoid introducing any unintended breaking changes but for the life of me I can't comprehend why anyone would ever want the same card attached to the same customer represented by multiple payment methods in the database. It's confusingly counterintuitive and places an unnecessary burden on thousands of developers to accidentally discover this odd behavior and then implement a fix for it. At a minimum the docs should include a warning about it.

@goriunov
Copy link

Same problem :( looks like this issue has been for a while...

@jemishmoradiya
Copy link

I've found a workaround.. you can follow these steps to avoid duplication of cards on stripe as well as on your database.

  1. Retrieve all card's Fingerprint in the array list
  2. Now attach your card to the customer even if this is duplicate
  3. Now you'll get a saved new card, now check if the new card's fingerprint exists in our array that we've created on the first step. if this exists then you can use delete card API from the stripe to delete the newly added card you can also write code if this is duplicated then don't save reference in your DB.

This approach will attach the duplicated cards to stripe customers but we're deleting them if we found them duplicated.
I've followed this approach in JAVA.

@osamaaftab
Copy link

fingerprint is null if livemode == false

@danielehrhardt
Copy link

Nothing to say here. Stripe WTF?!

@patrickbussmann
Copy link

The funny thing on my side is that I'm using PaymentIntents:

    'customer' => 'cus_xxxx',
    'setup_future_usage' => 'on_session'

And then I see the payment intent in the mobile app (iOS & Android) and whenever I pay I have the card duplicated.
This is very annoying 😐

@lifeofgurpreet
Copy link

common stripe. seriously?

@lifeofgurpreet
Copy link

Guys. After more digging they provided a video yesterday as well as some code. Here is the links. hope it will help you too.

https://glitch.com/edit/#!/stripe-tinydemos-deduplicate-cards?path=server.js%3A122%3A32

https://www.youtube.com/watch?v=MvMmOWWHjVo

@Skorpyon
Copy link

Skorpyon commented Nov 2, 2021

common stripe. seriously?

Developers first. Before IPO. After it cost $90B and fuck fucking developers.

@varsachendra
Copy link

varsachendra commented Dec 17, 2021

I am integrate Stripe Google pay Button for recurring payment(subscription). in that case I got below error
This PaymentMethod was previously used without being attached to a Customer or was detached from a Customer, and may not be used again

https://api.stripe.com/v1/payment_methods

{
"id": "pm_1K7G1gI29lnP5KBavFzpBZ8V",
"object": "payment_method",
"billing_details": {
"address": {
"city": "North Wales",
"country": "US",
"line1": "4105 ****",
"line2": null,
"postal_code": "12345",
"state": "PA"
},
"email": "[email protected]",
"name": "sachendra kumar",
"phone": null
},
"card": {
"brand": "visa",
"checks": {
"address_line1_check": null,
"address_postal_code_check": null,
"cvc_check": null
},
"country": "US",
"exp_month": 10,
"exp_year": 2023,
"funding": "credit",
"generated_from": null,
"last4": "XXXX",
"networks": {
"available": [
"visa"
],
"preferred": null
},
"three_d_secure_usage": {
"supported": true
},
"wallet": {
"dynamic_last4": "4242",
"google_pay": {
},
"type": "google_pay"
}
},
"created": 1639646004,
"customer": null,
"livemode": false,
"type": "card"
}

https://api.stripe.com/v1/payment_intents/pi_3K7G0wI29lnP5KBa14QrQFOg/confirm
{
"id": "pi_3K7G0wI29lnP5KBa14QrQFOg",
"object": "payment_intent",
"amount": 100,
"automatic_payment_methods": null,
"canceled_at": null,
"cancellation_reason": null,
"capture_method": "automatic",
"client_secret": "pi_3K7G0wI29lnP5KBa14QrQFOg_secret_UnXBp73shVKVjXtwGquYHM2Wp",
"confirmation_method": "automatic",
"created": 1639645958,
"currency": "usd",
"description": null,
"last_payment_error": null,
"livemode": false,
"next_action": null,
"payment_method": "pm_1K7G1gI29lnP5KBavFzpBZ8V",
"payment_method_types": [
"card"
],
"receipt_email": null,
"setup_future_usage": null,
"shipping": null,
"source": null,
"status": "succeeded"
}

//CONTROLLER REQUEST
Array
(
[result] => Array
(
[paymentIntent] => Array
(
[id] => pi_3K7G0wI29lnP5KBa14QrQFOg
[object] => payment_intent
[amount] => 100
[automatic_payment_methods] =>
[canceled_at] =>
[cancellation_reason] =>
[capture_method] => automatic
[client_secret] => pi_3K7G0wI29lnP5KBa14QrQFOg_secret_UnXBp73shVKVjXtwGquYHM2Wp
[confirmation_method] => automatic
[created] => 1639645958
[currency] => usd
[description] =>
[last_payment_error] =>
[livemode] => false
[next_action] =>
[payment_method] => pm_1K7G1gI29lnP5KBavFzpBZ8V
[payment_method_types] => Array
(
[0] => card
)

                [receipt_email] => 
                [setup_future_usage] => 
                [shipping] => 
                [source] => 
                [status] => succeeded
            )

    )

[payment_method] => pm_1K7G1gI29lnP5KBavFzpBZ8V
[token] => pi_3K7G0wI29lnP5KBa14QrQFOg
[name] => *******
[address] => ******
[city] => North Wales
[state] => PA
[zipcode] => 12345
[subscription_plan] => price_1InzeaI29lnP5KBaSE51rLIG

)
//CONTROLLER RESPONSE

{
"message": "This PaymentMethod was previously used without being attached to a Customer or was detached from a Customer, and may not be used again.",
"exception": "Stripe\Exception\InvalidRequestException",
}

@patrickbussmann
Copy link

patrickbussmann commented Dec 20, 2021

When someone need's an example for the PHP API:

$paymentMethods = $stripeClient
	->customers
	->allPaymentMethods($paymentIntent->customer->id, ['type' => 'card', 'limit' => 100]);
$fingerprints = [];
foreach ($paymentMethods as $method)
{
	$fingerprints[$method->card->fingerprint . '-' . $method->card->last4] = $method->id;
}
foreach ($paymentMethods as $method)
{
	if (!in_array($method->id, $fingerprints))
	{
		$stripeClient->paymentMethods->detach($method->id);
	}
}

Work's in testing mode as in live mode. 👍

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests