Skip to content

Commit

Permalink
Fix creation of duplicate tokens (#3635)
Browse files Browse the repository at this point in the history
* Fix creation of duplicate tokens

* Fix creation of duplicate tokens

* Fix duplicate tokens bug when updateing existing ones

* Changelog and readme entries

* Removing duplicate method

* Reverting unnecessary change

* Fix duplicate tokens for the block checkout

* Readme and changelog updates

* Readme and changelog updates

* Not updating tokens if the new order is a subscription

* Block existing token update if any subscription is using it

* Replacing token update block with new method

* Removing subscription limitation

* Comparing fingerprint instead of card details + new fingerprint trait + moving tokens to a new folder

* Fix CC token class overitte

* Adding specific unit tests

* Adding specific unit tests

* Removing unnecessary trait usage

* Fix tests

* Adding specific unit tests

* Adding specific unit tests

* Fix tests

* New trait to simplify duplicate comparison logic

* Renaming trait + including it

* Specific unit tests

* Renaming the search method to get

* Transforming trait into interface

* Fix tests

---------

Co-authored-by: Diego Curbelo <[email protected]>
  • Loading branch information
wjrosa and diegocurbelo authored Jan 7, 2025
1 parent 0a93ce6 commit 060fa87
Show file tree
Hide file tree
Showing 20 changed files with 641 additions and 76 deletions.
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
*** Changelog ***

= 9.1.0 - xxxx-xx-xx =
* Fix - Prevents duplicated credit cards to be added to the customer's account through the My Account page, the shortcode checkout and the block checkout.
* Fix - Return to the correct page when redirect-based payment method fails.
* Fix - Show default recipient for Payment Authentication Requested email.
* Fix - Correctly handles IPP failed payments webhook calls by extracting the order ID from the payment intent metadata.
Expand Down
7 changes: 5 additions & 2 deletions includes/class-wc-stripe-customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -391,28 +391,31 @@ public function add_source( $source_id ) {
$wc_token->set_token( $response->id );
$wc_token->set_gateway_id( 'stripe_sepa' );
$wc_token->set_last4( $response->sepa_debit->last4 );
$wc_token->set_fingerprint( $response->sepa_debit->fingerprint );
break;
default:
if ( WC_Stripe_Helper::is_card_payment_method( $response ) ) {
$wc_token = new WC_Payment_Token_CC();
$wc_token = new WC_Stripe_Payment_Token_CC();
$wc_token->set_token( $response->id );
$wc_token->set_gateway_id( 'stripe' );
$wc_token->set_card_type( strtolower( $response->card->brand ) );
$wc_token->set_last4( $response->card->last4 );
$wc_token->set_expiry_month( $response->card->exp_month );
$wc_token->set_expiry_year( $response->card->exp_year );
$wc_token->set_fingerprint( $response->card->fingerprint );
}
break;
}
} else {
// Legacy.
$wc_token = new WC_Payment_Token_CC();
$wc_token = new WC_Stripe_Payment_Token_CC();
$wc_token->set_token( $response->id );
$wc_token->set_gateway_id( 'stripe' );
$wc_token->set_card_type( strtolower( $response->brand ) );
$wc_token->set_last4( $response->last4 );
$wc_token->set_expiry_month( $response->exp_month );
$wc_token->set_expiry_year( $response->exp_year );
$wc_token->set_fingerprint( $response->fingerprint );
}

$wc_token->set_user_id( $this->get_user_id() );
Expand Down
27 changes: 22 additions & 5 deletions includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -1474,7 +1474,7 @@ public function is_payment_needed( $order_id = null ) {

// Check if the cart contains a pre-order product. Ignore the cart if we're on the Pay for Order page.
if ( $this->is_pre_order_item_in_cart() && ! $is_pay_for_order_page ) {
$pre_order_product = $this->get_pre_order_product_from_cart();
$pre_order_product = $this->get_pre_order_product_from_cart();

// Only one pre-order product is allowed per cart,
// so we can return if it's charged upfront.
Expand Down Expand Up @@ -1802,7 +1802,7 @@ private function is_setup_intent_success_creation_redirection() {
* @param string $setup_intent_id ID of the setup intent.
* @param WP_User $user User to add token to.
*
* @return WC_Payment_Token_CC|WC_Payment_Token_WCPay_SEPA The added token.
* @return WC_Payment_Token The added token.
*
* @since 5.8.0
* @version 5.8.0
Expand Down Expand Up @@ -2216,8 +2216,16 @@ protected function handle_saving_payment_method( WC_Order $order, $payment_metho
$payment_method_instance = $this->payment_methods[ $payment_method_type ];
}

// Create a payment token for the user in the store.
$payment_method_instance->create_payment_token_for_user( $user->ID, $payment_method_object );
// Searches for an existing duplicate token to update.
$found_token = WC_Stripe_Payment_Tokens::get_duplicate_token( $payment_method_object, $customer->get_user_id(), $this->id );

if ( $found_token ) {
// Update the token with the new payment method ID.
$payment_method_instance->update_payment_token( $found_token, $payment_method_object->id );
} else {
// Create a payment token for the user in the store.
$payment_method_instance->create_payment_token_for_user( $user->ID, $payment_method_object );
}

// Add the payment method information to the order.
$prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object );
Expand Down Expand Up @@ -2363,7 +2371,16 @@ public function add_payment_method() {
$customer = new WC_Stripe_Customer( $user->ID );
$customer->clear_cache();

$token = $payment_method->create_payment_token_for_user( $user->ID, $payment_method_object );
// Check if a token with the same payment method details exist. If so, just updates the payment method ID and return.
$found_token = WC_Stripe_Payment_Tokens::get_duplicate_token( $payment_method_object, $user->ID, $this->id );

// If we have a token found, update it and return.
if ( $found_token ) {
$token = $payment_method->update_payment_token( $found_token, $payment_method_object->id );
} else {
// Create a new token if not.
$token = $payment_method->create_payment_token_for_user( $user->ID, $payment_method_object );
}

if ( ! is_a( $token, 'WC_Payment_Token' ) ) {
throw new WC_Stripe_Exception( sprintf( 'New payment token is not an instance of WC_Payment_Token. Token: %s.', print_r( $token, true ) ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,18 @@ public function get_retrievable_type() {
* @param string $user_id WP_User ID
* @param object $payment_method Stripe payment method object
*
* @return WC_Payment_Token_CC
* @return WC_Stripe_Payment_Token_CC
*/
public function create_payment_token_for_user( $user_id, $payment_method ) {
$token = new WC_Payment_Token_CC();
$token = new WC_Stripe_Payment_Token_CC();
$token->set_expiry_month( $payment_method->card->exp_month );
$token->set_expiry_year( $payment_method->card->exp_year );
$token->set_card_type( strtolower( $payment_method->card->display_brand ?? $payment_method->card->networks->preferred ?? $payment_method->card->brand ) );
$token->set_last4( $payment_method->card->last4 );
$token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID );
$token->set_token( $payment_method->id );
$token->set_user_id( $user_id );
$token->set_fingerprint( $payment_method->card->fingerprint );
$token->save();
return $token;
}
Expand Down
14 changes: 14 additions & 0 deletions includes/payment-methods/class-wc-stripe-upe-payment-method.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,20 @@ public function create_payment_token_for_user( $user_id, $payment_method ) {
$token->set_token( $payment_method->id );
$token->set_payment_method_type( $this->get_id() );
$token->set_user_id( $user_id );
$token->set_fingerprint( $payment_method->sepa_debit->fingerprint );
$token->save();
return $token;
}

/**
* Updates a payment token.
*
* @param WC_Payment_Token $token The token to update.
* @param string $payment_method_id The new payment method ID.
* @return WC_Payment_Token
*/
public function update_payment_token( $token, $payment_method_id ) {
$token->set_token( $payment_method_id );
$token->save();
return $token;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
// Exit if accessed directly
defined( 'ABSPATH' ) || exit;

class WC_Payment_Token_CashApp extends WC_Payment_Token {

class WC_Payment_Token_CashApp extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface {
/**
* Token Type.
*
Expand Down Expand Up @@ -62,6 +61,20 @@ public function get_cashtag() {
return $this->get_prop( 'cashtag' );
}

/**
* Checks if the payment method token is equal a provided payment method.
*
* @inheritDoc
*/
public function is_equal_payment_method( $payment_method ): bool {
if ( WC_Stripe_Payment_Methods::CASHAPP_PAY === $this->get_type()
&& ( $payment_method->cashapp->cashtag ?? null ) === $this->get_cashtag() ) {
return true;
}

return false;
}

/**
* Returns this token's hook prefix.
*
Expand Down
45 changes: 45 additions & 0 deletions includes/payment-tokens/class-wc-stripe-cc-payment-token.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* WooCommerce Stripe Credit Card Payment Token
*
* Representation of a payment token for Credit Card.
*
* @package WooCommerce_Stripe
* @since 9.9.0
*/

// phpcs:disable WordPress.Files.FileName

// Exit if accessed directly
defined( 'ABSPATH' ) || exit;

class WC_Stripe_Payment_Token_CC extends WC_Payment_Token_CC implements WC_Stripe_Payment_Method_Comparison_Interface {

use WC_Stripe_Fingerprint_Trait;

/**
* Constructor.
*
* @inheritDoc
*/
public function __construct( $token = '' ) {
// Add fingerprint to extra data to be persisted.
$this->extra_data['fingerprint'] = '';

parent::__construct( $token );
}

/**
* Checks if the payment method token is equal a provided payment method.
*
* @inheritDoc
*/
public function is_equal_payment_method( $payment_method ): bool {
if ( WC_Stripe_Payment_Methods::CARD === $payment_method->type
&& ( $payment_method->card->fingerprint ?? null ) === $this->get_fingerprint() ) {
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
*
* @class WC_Payment_Token_Link
*/
class WC_Payment_Token_Link extends WC_Payment_Token {

class WC_Payment_Token_Link extends WC_Payment_Token implements WC_Stripe_Payment_Method_Comparison_Interface {
/**
* Stores payment type.
*
Expand Down Expand Up @@ -92,4 +91,18 @@ public function get_email( $context = 'view' ) {
public function set_email( $email ) {
$this->set_prop( 'email', $email );
}

/**
* Checks if the payment method token is equal a provided payment method.
*
* @inheritDoc
*/
public function is_equal_payment_method( $payment_method ): bool {
if ( WC_Stripe_Payment_Methods::LINK === $payment_method->type
&& ( $payment_method->link->email ?? null ) === $this->get_email() ) {
return true;
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function __construct() {
add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item' ], 10, 2 );
add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_sepa_label' ] );
add_filter( 'woocommerce_payment_token_class', [ $this, 'woocommerce_payment_token_class' ], 10, 2 );
add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ] );
}
Expand Down Expand Up @@ -173,7 +174,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom
foreach ( $stripe_sources as $source ) {
if ( isset( $source->type ) && WC_Stripe_Payment_Methods::CARD === $source->type ) {
if ( ! isset( $stored_tokens[ $source->id ] ) ) {
$token = new WC_Payment_Token_CC();
$token = new WC_Stripe_Payment_Token_CC();
$token->set_token( $source->id );
$token->set_gateway_id( WC_Gateway_Stripe::ID );

Expand All @@ -184,6 +185,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom
$token->set_expiry_year( $source->card->exp_year );
}

$token->set_fingerprint( $source->fingerprint );
$token->set_user_id( $customer_id );
$token->save();
$tokens[ $token->get_id() ] = $token;
Expand All @@ -192,14 +194,15 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom
}
} else {
if ( ! isset( $stored_tokens[ $source->id ] ) && WC_Stripe_Payment_Methods::CARD === $source->object ) {
$token = new WC_Payment_Token_CC();
$token = new WC_Stripe_Payment_Token_CC();
$token->set_token( $source->id );
$token->set_gateway_id( WC_Gateway_Stripe::ID );
$token->set_card_type( strtolower( $source->brand ) );
$token->set_last4( $source->last4 );
$token->set_expiry_month( $source->exp_month );
$token->set_expiry_year( $source->exp_year );
$token->set_user_id( $customer_id );
$token->set_fingerprint( $source->fingerprint );
$token->save();
$tokens[ $token->get_id() ] = $token;
} else {
Expand All @@ -221,6 +224,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom
$token->set_gateway_id( WC_Gateway_Stripe_Sepa::ID );
$token->set_last4( $source->sepa_debit->last4 );
$token->set_user_id( $customer_id );
$token->set_fingerprint( $source->fingerprint );
$token->save();
$tokens[ $token->get_id() ] = $token;
} else {
Expand Down Expand Up @@ -492,9 +496,9 @@ private function is_valid_payment_method_type_for_gateway( $payment_method_type,
/**
* Creates and add a token to an user, based on the PaymentMethod object.
*
* @param array $payment_method Payment method to be added.
* @param WC_Stripe_Customer $user WC_Stripe_Customer we're processing the tokens for.
* @return WC_Payment_Token_CC|WC_Payment_Token_Link|WC_Payment_Token_SEPA The WC object for the payment token.
* @param object $payment_method Payment method to be added.
* @param WC_Stripe_Customer $customer WC_Stripe_Customer we're processing the tokens for.
* @return WC_Payment_Token The WC object for the payment token.
*/
private function add_token_to_user( $payment_method, WC_Stripe_Customer $customer ) {
// Clear cached payment methods.
Expand All @@ -503,13 +507,22 @@ private function add_token_to_user( $payment_method, WC_Stripe_Customer $custome
$payment_method_type = $this->get_original_payment_method_type( $payment_method );
$gateway_id = self::UPE_REUSABLE_GATEWAYS_BY_PAYMENT_METHOD[ $payment_method_type ];

$found_token = $this->get_duplicate_token( $payment_method, $customer->get_user_id(), $gateway_id );
if ( $found_token ) {
// Update the token with the new payment method ID.
$found_token->set_token( $payment_method->id );
$found_token->save();
return $found_token;
}

switch ( $payment_method_type ) {
case WC_Stripe_UPE_Payment_Method_CC::STRIPE_ID:
$token = new WC_Payment_Token_CC();
$token = new WC_Stripe_Payment_Token_CC();
$token->set_expiry_month( $payment_method->card->exp_month );
$token->set_expiry_year( $payment_method->card->exp_year );
$token->set_card_type( strtolower( $payment_method->card->display_brand ?? $payment_method->card->networks->preferred ?? $payment_method->card->brand ) );
$token->set_last4( $payment_method->card->last4 );
$token->set_fingerprint( $payment_method->card->fingerprint );
break;

case WC_Stripe_UPE_Payment_Method_Link::STRIPE_ID:
Expand All @@ -528,6 +541,7 @@ private function add_token_to_user( $payment_method, WC_Stripe_Customer $custome
$token = new WC_Payment_Token_SEPA();
$token->set_last4( $payment_method->sepa_debit->last4 );
$token->set_payment_method_type( $payment_method_type );
$token->set_fingerprint( $payment_method->sepa_debit->fingerprint );
}

$token->set_gateway_id( $gateway_id );
Expand Down Expand Up @@ -647,6 +661,50 @@ public function is_valid_payment_method_id( $payment_method_id, $payment_method_
return 0 === strpos( $payment_method_id, 'src_' ) && WC_Stripe_Payment_Methods::CARD === $payment_method_type;
}

/**
* Searches for a duplicate token in the user's saved payment methods and returns it.
*
* @param $payment_method stdClass The payment method object.
* @param $user_id int The user ID.
* @param $gateway_id string The gateway ID.
* @return WC_Payment_Token|null
*/
public static function get_duplicate_token( $payment_method, $user_id, $gateway_id ) {
// Using the base method instead of `WC_Payment_Tokens::get_customer_tokens` to avoid recursive calls to hooked filters and actions
$tokens = WC_Payment_Tokens::get_tokens(
[
'user_id' => $user_id,
'gateway_id' => $gateway_id,
'limit' => 100,
]
);
foreach ( $tokens as $token ) {
/**
* Token object.
*
* @var WC_Payment_Token_CashApp|WC_Stripe_Payment_Token_CC|WC_Payment_Token_Link|WC_Payment_Token_SEPA $token
*/
if ( $token->is_equal_payment_method( $payment_method ) ) {
return $token;
}
}
return null;
}

/**
* Filters the payment token class to override the credit card class with the extension's version.
*
* @param string $class Payment token class.
* @param string $type Token type.
* @return string
*/
public function woocommerce_payment_token_class( $class, $type ) {
if ( WC_Payment_Token_CC::class === $class ) {
return WC_Stripe_Payment_Token_CC::class;
}
return $class;
}

/**
* Controls the output for SEPA on the my account page.
*
Expand Down
Loading

0 comments on commit 060fa87

Please sign in to comment.