From bfa3f24c7806ca1b2ae0d55e2a0c951a6e9b1797 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 15:23:05 +1000 Subject: [PATCH 01/21] Add support flags to Stripe UPE methods --- .../class-wc-stripe-upe-payment-method-bancontact.php | 2 ++ .../class-wc-stripe-upe-payment-method-boleto.php | 1 + .../payment-methods/class-wc-stripe-upe-payment-method-cc.php | 2 ++ .../class-wc-stripe-upe-payment-method-ideal.php | 2 ++ .../payment-methods/class-wc-stripe-upe-payment-method-oxxo.php | 1 + .../payment-methods/class-wc-stripe-upe-payment-method-sepa.php | 2 ++ includes/payment-methods/class-wc-stripe-upe-payment-method.php | 1 + 7 files changed, 11 insertions(+) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php index 4c62db28e2..47d3facc78 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php @@ -22,6 +22,8 @@ public function __construct() { $this->is_reusable = true; $this->supported_currencies = [ 'EUR' ]; $this->label = __( 'Bancontact', 'woocommerce-gateway-stripe' ); + $this->supports[] = 'subscriptions'; + $this->supports[] = 'tokenization'; $this->description = __( 'Bancontact is the most popular online payment method in Belgium, with over 15 million cards in circulation.', 'woocommerce-gateway-stripe' diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php index db13ca7727..ef367fc660 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-boleto.php @@ -25,6 +25,7 @@ public function __construct() { $this->is_reusable = false; $this->supported_currencies = [ 'BRL' ]; $this->supported_countries = [ 'BR' ]; + $this->supports = [ 'products' ]; $this->label = __( 'Boleto', 'woocommerce-gateway-stripe' ); $this->description = __( 'Boleto is an official payment method in Brazil. Customers receive a voucher that can be paid at authorized agencies or banks, ATMs, or online bank portals.', diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php index 984becccaa..fc84a5f120 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-cc.php @@ -25,6 +25,8 @@ public function __construct() { $this->title = __( 'Credit / Debit Card', 'woocommerce-gateway-stripe' ); $this->is_reusable = true; $this->label = __( 'Credit / Debit Card', 'woocommerce-gateway-stripe' ); + $this->supports[] = 'subscriptions'; + $this->supports[] = 'tokenization'; $this->description = __( 'Let your customers pay with major credit and debit cards without leaving your store.', 'woocommerce-gateway-stripe' diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php index 68fa740786..a203f60c0c 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php @@ -22,6 +22,8 @@ public function __construct() { $this->is_reusable = true; $this->supported_currencies = [ 'EUR' ]; $this->label = __( 'iDEAL', 'woocommerce-gateway-stripe' ); + $this->supports[] = 'subscriptions'; + $this->supports[] = 'tokenization'; $this->description = __( 'iDEAL is a Netherlands-based payment method that allows customers to complete transactions online using their bank credentials.', 'woocommerce-gateway-stripe' diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php index efccbd5328..8569468807 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-oxxo.php @@ -25,6 +25,7 @@ public function __construct() { $this->is_reusable = false; $this->supported_currencies = [ 'MXN' ]; $this->supported_countries = [ 'MX' ]; + $this->supports = [ 'products' ]; $this->label = __( 'OXXO', 'woocommerce-gateway-stripe' ); $this->description = __( 'OXXO is a Mexican chain of convenience stores that allows customers to pay bills and online purchases in-store with cash.', diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php index 7a5ec9bceb..7b474ec170 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php @@ -24,6 +24,8 @@ public function __construct() { $this->is_reusable = true; $this->supported_currencies = [ 'EUR' ]; $this->label = __( 'SEPA Direct Debit', 'woocommerce-gateway-stripe' ); + $this->supports[] = 'subscriptions'; + $this->supports[] = 'tokenization'; $this->description = __( 'Reach 500 million customers and over 20 million businesses across the European Union.', 'woocommerce-gateway-stripe' diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index 2aa43e67d7..13be09b856 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -91,6 +91,7 @@ public function __construct() { $this->enabled = $is_stripe_enabled && in_array( static::STRIPE_ID, $this->get_option( 'upe_checkout_experience_accepted_payments', [ 'card' ] ), true ) ? 'yes' : 'no'; $this->id = WC_Gateway_Stripe::ID . '_' . static::STRIPE_ID; $this->testmode = ! empty( $main_settings['testmode'] ) && 'yes' === $main_settings['testmode']; + $this->supports = [ 'products', 'refunds' ]; } /** From 992ccece2414dc55ce0f9db2359d2c5a7117f25d Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 15:27:16 +1000 Subject: [PATCH 02/21] Set the wc-stripe-is-deferred-intent flag when processing payments via tokens on the block checkout --- includes/class-wc-stripe-blocks-support.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/includes/class-wc-stripe-blocks-support.php b/includes/class-wc-stripe-blocks-support.php index 44f29b91b5..d2ad050820 100644 --- a/includes/class-wc-stripe-blocks-support.php +++ b/includes/class-wc-stripe-blocks-support.php @@ -364,6 +364,14 @@ public function add_payment_request_order_meta( PaymentContext $context, Payment // Strip "Stripe_" from the payment method name to get the payment method type. $payment_method_type = substr( $context->payment_method, strlen( $this->name ) + 1 ); $is_stripe_payment_method = isset( $main_gateway->payment_methods[ $payment_method_type ] ); + + /** + * When using the block checkout and a saved token is being used, we need to set a flag + * to indicate that deferred intent should be used. + */ + if ( $is_stripe_payment_method && isset( $data['issavedtoken'] ) && $data['issavedtoken'] ) { + $context->set_payment_data( array_merge( $data, [ 'wc-stripe-is-deferred-intent' => true ] ) ); + } } } From e56c41839d68b58496a23b51f146ad552e73d0fa Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 15:27:56 +1000 Subject: [PATCH 03/21] Add bancontact and ideal to the list of gateways that require a mandate --- includes/class-wc-stripe-intent-controller.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index ecc703f478..65e2550984 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -893,12 +893,12 @@ private function get_payment_method_types_for_intent_creation( string $selected_ * @return bool True if a mandate must be shown and acknowledged by customer before deferred intent UPE payment can be processed, false otherwise. */ public function is_mandate_data_required( $selected_payment_type ) { - $gateway = $this->get_upe_gateway(); - $is_stripe_link_enabled = 'card' === $selected_payment_type && in_array( 'link', $gateway->get_upe_enabled_payment_method_ids(), true ); - $is_sepa_debit_payment = 'sepa_debit' === $selected_payment_type; + if ( in_array( $selected_payment_type, [ 'sepa_debit', 'bancontact', 'ideal' ], true ) ) { + return true; + } - return $is_stripe_link_enabled || $is_sepa_debit_payment; + return 'card' === $selected_payment_type && in_array( 'link', $this->get_upe_gateway()->get_upe_enabled_payment_method_ids(), true ); } /** @@ -911,11 +911,12 @@ public function is_mandate_data_required( $selected_payment_type ) { * @return array */ public function create_and_confirm_setup_intent( $payment_information ) { - $request = [ + $request = [ 'payment_method' => $payment_information['payment_method'], 'payment_method_types' => [ $payment_information['selected_payment_type'] ], 'customer' => $payment_information['customer'], 'confirm' => 'true', + 'return_url' => $payment_information['return_url'], ]; // SEPA setup intents require mandate data. From 6333f2a07a8d087ce6f1552e7e4f80cd014aa866 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 15:29:07 +1000 Subject: [PATCH 04/21] Set the subscriptions payment method ID when setting the payment token source --- includes/compat/trait-wc-stripe-subscriptions.php | 12 ++++++++++-- .../class-wc-stripe-upe-payment-gateway.php | 5 +++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/includes/compat/trait-wc-stripe-subscriptions.php b/includes/compat/trait-wc-stripe-subscriptions.php index 0fc9e8392e..c59294dfd9 100644 --- a/includes/compat/trait-wc-stripe-subscriptions.php +++ b/includes/compat/trait-wc-stripe-subscriptions.php @@ -386,8 +386,12 @@ public function process_subscription_payment( $amount, $renewal_order, $retry = * Updates other subscription sources. * * @since 5.6.0 + * + * @param WC_Order $order The order object. + * @param string $source_id The source ID. + * @param string $payment_gateway_id The payment method ID. eg 'stripe. */ - public function maybe_update_source_on_subscription_order( $order, $source ) { + public function maybe_update_source_on_subscription_order( $order, $source, $payment_gateway_id = '' ) { if ( ! $this->is_subscriptions_enabled() ) { return; } @@ -404,7 +408,6 @@ public function maybe_update_source_on_subscription_order( $order, $source ) { } foreach ( $subscriptions as $subscription ) { - $subscription_id = $subscription->get_id(); $subscription->update_meta_data( '_stripe_customer_id', $source->customer ); if ( ! empty( $source->payment_method ) ) { @@ -413,6 +416,11 @@ public function maybe_update_source_on_subscription_order( $order, $source ) { $subscription->update_meta_data( '_stripe_source_id', $source->source ); } + // Update the payment method. + if ( ! empty( $payment_gateway_id ) ) { + $subscription->set_payment_method( $payment_gateway_id ); + } + $subscription->save(); } } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index fd0a91ce09..5bafdf2a30 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -724,7 +724,8 @@ private function process_payment_with_deferred_intent( int $order_id ) { (object) [ 'payment_method' => $payment_information['payment_method'], 'customer' => $payment_information['customer'], - ] + ], + $this->id ); } @@ -1263,7 +1264,7 @@ public function save_payment_method_to_order( $order, $payment_method ) { $order->save(); } - $this->maybe_update_source_on_subscription_order( $order, $payment_method ); + $this->maybe_update_source_on_subscription_order( $order, $payment_method, $this->id ); } /** From 36fda590f05f154abd1f44f2315b18bf4fd72fbb Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 15:30:22 +1000 Subject: [PATCH 05/21] Fix saved tokens for APMs like iDEAL, Bancontact and SEPA --- includes/class-wc-stripe-helper.php | 18 ++++ includes/class-wc-stripe-payment-tokens.php | 86 +++++++++---------- .../class-wc-stripe-upe-payment-gateway.php | 6 +- .../class-wc-stripe-upe-payment-method.php | 6 +- 4 files changed, 71 insertions(+), 45 deletions(-) diff --git a/includes/class-wc-stripe-helper.php b/includes/class-wc-stripe-helper.php index ef8d6daf4b..7572654877 100644 --- a/includes/class-wc-stripe-helper.php +++ b/includes/class-wc-stripe-helper.php @@ -1105,4 +1105,22 @@ public static function get_intent_id_from_order( $order ) { return $intent_id ?? false; } + + /** + * Fetches a list of all Stripe gateway IDs. + * + * @return array An array of all Stripe gateway IDs. + */ + public static function get_stripe_gateway_ids() { + $main_gateway = WC_Stripe::get_instance()->get_main_stripe_gateway(); + $gateway_ids = [ 'stripe' => $main_gateway->id ]; + + if ( is_a( $main_gateway, 'WC_Stripe_UPE_Payment_Gateway' ) ) { + $gateways = $main_gateway->payment_methods; + } else { + $gateways = self::get_legacy_payment_methods(); + } + + return array_merge( $gateway_ids, wp_list_pluck( $gateways, 'id', 'id' ) ); + } } diff --git a/includes/class-wc-stripe-payment-tokens.php b/includes/class-wc-stripe-payment-tokens.php index c70a3e900b..cca914042a 100644 --- a/includes/class-wc-stripe-payment-tokens.php +++ b/includes/class-wc-stripe-payment-tokens.php @@ -230,7 +230,8 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom * @return array */ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, $gateway_id ) { - if ( ( ! empty( $gateway_id ) && WC_Stripe_UPE_Payment_Gateway::ID !== $gateway_id ) || ! is_user_logged_in() ) { + + if ( ! is_user_logged_in() || ( ! empty( $gateway_id ) && ! in_array( $gateway_id, WC_Stripe_Helper::get_stripe_gateway_ids(), true ) ) ) { return $tokens; } @@ -240,62 +241,61 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, return $tokens; } - $gateway = new WC_Stripe_UPE_Payment_Gateway(); - $reusable_payment_methods = array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] ); - $customer = new WC_Stripe_Customer( $user_id ); - $remaining_tokens = []; + $gateway = WC_Stripe::get_instance()->get_main_stripe_gateway(); + $upe_gateway = null; - foreach ( $tokens as $token ) { - if ( WC_Stripe_UPE_Payment_Gateway::ID === $token->get_gateway_id() ) { - $payment_method_type = $this->get_payment_method_type_from_token( $token ); - if ( ! in_array( $payment_method_type, $reusable_payment_methods, true ) ) { - // Remove saved token from list, if payment method is not enabled. - unset( $tokens[ $token->get_id() ] ); - } else { - // Store relevant existing tokens here. - // We will use this list to check whether these methods still exist on Stripe's side. - $remaining_tokens[ $token->get_token() ] = $token; - } + foreach ( $gateway->payment_methods as $payment_gateway ) { + if ( $payment_gateway->id === $gateway_id ) { + $upe_gateway = $payment_gateway; + break; } } - $retrievable_payment_method_types = []; - foreach ( $reusable_payment_methods as $payment_method_id ) { - $upe_payment_method = $gateway->payment_methods[ $payment_method_id ]; - if ( ! in_array( $upe_payment_method->get_retrievable_type(), $retrievable_payment_method_types, true ) ) { - $retrievable_payment_method_types[] = $upe_payment_method->get_retrievable_type(); - } + if ( ! $upe_gateway || ! $upe_gateway->is_reusable() ) { + return $tokens; + } + + $customer = new WC_Stripe_Customer( $user_id ); + $current_tokens = []; + + foreach ( $tokens as $token ) { + // Store relevant existing tokens here. + // We will use this list to check whether these methods still exist on Stripe's side. + $current_tokens[ $token->get_token() ] = $token; } try { - foreach ( $retrievable_payment_method_types as $payment_method_id ) { - $payment_methods = $customer->get_payment_methods( $payment_method_id ); - - // Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases. - remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); - foreach ( $payment_methods as $payment_method ) { - if ( ! isset( $remaining_tokens[ $payment_method->id ] ) ) { - $payment_method_type = $this->get_original_payment_method_type( $payment_method ); - if ( ! in_array( $payment_method_type, $reusable_payment_methods, true ) ) { - continue; - } - // Create new token for new payment method and add to list. - $upe_payment_method = $gateway->payment_methods[ $payment_method_type ]; - $token = $upe_payment_method->create_payment_token_for_user( $user_id, $payment_method ); - $tokens[ $token->get_id() ] = $token; - } else { - // Count that existing token for payment method is still present on Stripe. - // Remaining IDs in $remaining_tokens no longer exist with Stripe and will be eliminated. - unset( $remaining_tokens[ $payment_method->id ] ); + // If this UPE method uses a different payment method type as a token, we don't want to retrieve tokens for it. ie Bancontact uses SEPA tokens. + $payment_methods = $customer->get_payment_methods( $upe_gateway->get_retrievable_type() ); + + // Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases. + remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); + + foreach ( $payment_methods as $payment_method ) { + if ( ! isset( $current_tokens[ $payment_method->id ] ) ) { + $payment_method_type = $this->get_original_payment_method_type( $payment_method ); + + if ( $payment_method_type !== $upe_gateway::STRIPE_ID ) { + continue; } + + // Create new token for new payment method and add to list. + $token = $upe_gateway->create_payment_token_for_user( $user_id, $payment_method ); + $tokens[ $token->get_id() ] = $token; + } else { + // Count that existing token for payment method is still present on Stripe. + // Remaining IDs in $remaining_tokens no longer exist with Stripe and will be eliminated. + unset( $current_tokens[ $payment_method->id ] ); } - add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); } + add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 ); + // Eliminate remaining payment methods no longer known by Stripe. // Prevent unnecessary recursion, when deleting tokens. remove_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 ); - foreach ( $remaining_tokens as $token ) { + + foreach ( $current_tokens as $token ) { unset( $tokens[ $token->get_id() ] ); $token->delete(); } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 5bafdf2a30..ee0fbd5869 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -712,7 +712,7 @@ private function process_payment_with_deferred_intent( int $order_id ) { // Handle saving the payment method in the store. // It's already attached to the Stripe customer at this point. - if ( $payment_information['save_payment_method_to_store'] ) { + if ( $payment_information['save_payment_method_to_store'] && $upe_payment_method && $upe_payment_method->get_id() === $upe_payment_method->get_retrievable_type() ) { $this->handle_saving_payment_method( $order, $payment_information['payment_method'], @@ -1803,6 +1803,10 @@ private function prepare_payment_information_from_request( WC_Order $order ) { } $payment_method_id = $token->get_token(); + + if ( is_a( $token, 'WC_Payment_Token_SEPA' ) ) { + $selected_payment_type = WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID; + } } else { $payment_method_id = sanitize_text_field( wp_unslash( $_POST['wc-stripe-payment-method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index 13be09b856..793e85f256 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -278,7 +278,7 @@ public function get_retrievable_type() { public function create_payment_token_for_user( $user_id, $payment_method ) { $token = new WC_Payment_Token_SEPA(); $token->set_last4( $payment_method->sepa_debit->last4 ); - $token->set_gateway_id( WC_Stripe_UPE_Payment_Gateway::ID ); + $token->set_gateway_id( $this->id ); $token->set_token( $payment_method->id ); $token->set_payment_method_type( $this->get_id() ); $token->set_user_id( $user_id ); @@ -419,6 +419,10 @@ public function payment_fields() { $this->save_payment_method_checkbox( $force_save_payment ); } } + if ( $display_tokenization ) { + $this->tokenization_script(); + $this->saved_payment_methods(); + } } catch ( Exception $e ) { // Output the error message. WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() ); From 744f4d6c2406b58c861500cee485d7adb8ac96f8 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 15:31:24 +1000 Subject: [PATCH 06/21] fix typo and fetch the payment method object --- .../payment-methods/class-wc-stripe-upe-payment-gateway.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index ee0fbd5869..f011811255 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -684,6 +684,7 @@ private function process_payment_with_deferred_intent( int $order_id ) { $payment_needed = $this->is_payment_needed( $order->get_id() ); $payment_method_id = $payment_information['payment_method']; $selected_payment_type = $payment_information['selected_payment_type']; + $upe_payment_method = $this->payment_methods[ $selected_payment_type ] ?? null; // Make sure that we attach the payment method and the customer ID to the order meta data. $this->set_payment_method_id_for_order( $order, $payment_method_id ); @@ -1928,7 +1929,7 @@ private function handle_saving_payment_method( WC_Order $order, string $payment_ $customer = new WC_Stripe_Customer( $user->ID ); $customer->clear_cache(); - // Add the payment method information to the ordeer. + // Add the payment method information to the order. $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object ); $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object ); From c46f9e6dee816fb38951b19f7c164df90d9bc509 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 7 Feb 2024 17:22:49 +1000 Subject: [PATCH 07/21] Make sure to pass offsession flag to block payment elements if the cart contains a subs cription or saved tokens are enabled --- client/blocks/upe/index.js | 6 ++-- .../payment-elements.js | 35 +++++++++++-------- .../class-wc-stripe-upe-payment-gateway.php | 18 +++++++++- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/client/blocks/upe/index.js b/client/blocks/upe/index.js index a018f9728a..2652484830 100644 --- a/client/blocks/upe/index.js +++ b/client/blocks/upe/index.js @@ -32,13 +32,15 @@ Object.entries( getBlocksConfiguration()?.paymentMethodsConfig ) upeName, upeMethods, api, - upeConfig.testingInstructions + upeConfig.testingInstructions, + upeConfig.showSaveOption ?? false ), edit: getDeferredIntentCreationUPEFields( upeName, upeMethods, api, - upeConfig.testingInstructions + upeConfig.testingInstructions, + upeConfig.showSaveOption ?? false ), savedTokenComponent: , canMakePayment: () => !! api.getStripe(), diff --git a/client/blocks/upe/upe-deferred-intent-creation/payment-elements.js b/client/blocks/upe/upe-deferred-intent-creation/payment-elements.js index 0eb3bfe025..b43678a1fc 100644 --- a/client/blocks/upe/upe-deferred-intent-creation/payment-elements.js +++ b/client/blocks/upe/upe-deferred-intent-creation/payment-elements.js @@ -24,21 +24,25 @@ const PaymentElements = ( { api, ...props } ) => { const amount = Number( getBlocksConfiguration()?.cartTotal ); const currency = getBlocksConfiguration()?.currency.toLowerCase(); const appearance = initializeUPEAppearance(); + const options = { + mode: amount < 1 ? 'setup' : 'payment', + amount, + currency, + paymentMethodCreation: 'manual', + paymentMethodTypes: getPaymentMethodTypes( props.paymentMethodId ), + appearance, + }; + + // If the cart contains a subscription or the payment method supports saving, we need to use off_session setup so Stripe can display appropriate terms and conditions. + if ( + getBlocksConfiguration()?.cartContainsSubscription || + props.showSaveOption + ) { + options.setupFutureUsage = 'off_session'; + } return ( - + ); @@ -51,6 +55,7 @@ const PaymentElements = ( { api, ...props } ) => { * @param {Array} upeMethods * @param {WCStripeAPI} api * @param {string} testingInstructions + * @param {boolean} showSaveOption * * @return {JSX.Element} Rendered Payment elements. */ @@ -58,7 +63,8 @@ export const getDeferredIntentCreationUPEFields = ( paymentMethodId, upeMethods, api, - testingInstructions + testingInstructions, + showSaveOption ) => { return ( ); }; diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index f011811255..b5bfe5e203 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -330,6 +330,7 @@ public function javascript_params() { $stripe_params['accountDescriptor'] = $this->statement_descriptor; $stripe_params['addPaymentReturnURL'] = wc_get_account_endpoint_url( 'payment-methods' ); $stripe_params['enabledBillingFields'] = $enabled_billing_fields; + $stripe_params['cartContainsSubscription'] = $this->is_subscription_item_in_cart(); $cart_total = ( WC()->cart ? WC()->cart->get_total( '' ) : 0 ); $currency = get_woocommerce_currency(); @@ -392,7 +393,7 @@ private function get_enabled_payment_method_config() { 'isReusable' => $this->payment_methods[ $payment_method ]->is_reusable(), 'title' => $this->payment_methods[ $payment_method ]->get_title(), 'testingInstructions' => $this->payment_methods[ $payment_method ]->get_testing_instructions(), - 'showSaveOption' => $this->payment_methods[ $payment_method ]->should_show_save_option(), + 'showSaveOption' => $this->should_upe_payment_method_show_save_option( $this->payment_methods[ $payment_method ] ), ]; } @@ -2138,4 +2139,19 @@ private function get_return_url_for_redirect( $order, $save_payment_method ) { ) ); } + + /** + * Checks if the save option for a payment method should be displayed or not. + * + * @param WC_Stripe_UPE_Payment_Method $payment_method UPE Payment Method instance. + * @return bool - True if the payment method is reusable and the saved cards feature is enabled for the gateway and there is no subscription item in the cart, false otherwise. + */ + private function should_upe_payment_method_show_save_option( $payment_method ) { + if ( $payment_method->is_reusable() ) { + // If a subscription in the cart, it will be saved by default so no need to show the option. + return $this->is_saved_cards_enabled() && ! $this->is_subscription_item_in_cart(); + } + + return false; + } } From 0d84171d195cb9f3ab2bb28d80db9e9b48cb8e72 Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 9 Feb 2024 12:25:02 +1000 Subject: [PATCH 08/21] Set the payment method ID on the subscription when the payment method is updated --- .../payment-methods/class-wc-stripe-upe-payment-gateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index b5bfe5e203..ff047c22ea 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -1932,7 +1932,7 @@ private function handle_saving_payment_method( WC_Order $order, string $payment_ // Add the payment method information to the order. $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object ); - $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object ); + $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->id ); // Create a payment token for the user in the store. $payment_method_instance = $this->payment_methods[ $payment_method_type ]; From 80c0e81d871016b463fa5348283aedef6d030252 Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 9 Feb 2024 14:26:02 +1000 Subject: [PATCH 09/21] Set the order and subscription payment method to the upe gateway's ID --- .../class-wc-stripe-upe-payment-gateway.php | 17 +++++++++++------ .../class-wc-stripe-upe-payment-method-sepa.php | 4 ++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index ff047c22ea..b560a94355 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -727,7 +727,7 @@ private function process_payment_with_deferred_intent( int $order_id ) { 'payment_method' => $payment_information['payment_method'], 'customer' => $payment_information['customer'], ], - $this->id + $this->payment_methods[ $upe_payment_method->get_retrievable_type() ]->id ); } @@ -1266,7 +1266,12 @@ public function save_payment_method_to_order( $order, $payment_method ) { $order->save(); } - $this->maybe_update_source_on_subscription_order( $order, $payment_method, $this->id ); + // Fetch the payment method ID from the payment method object. + if ( isset( $this->payment_methods[ $payment_method->payment_method_object->type ] ) ) { + $payment_method_id = $this->payment_methods[ $payment_method->payment_method_object->type ]->id; + } + + $this->maybe_update_source_on_subscription_order( $order, $payment_method, $payment_method_id ?? $this->id ); } /** @@ -1930,14 +1935,14 @@ private function handle_saving_payment_method( WC_Order $order, string $payment_ $customer = new WC_Stripe_Customer( $user->ID ); $customer->clear_cache(); - // Add the payment method information to the order. - $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object ); - $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->id ); - // Create a payment token for the user in the store. $payment_method_instance = $this->payment_methods[ $payment_method_type ]; $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 ); + $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->payment_methods[ $payment_method_instance->get_retrievable_type() ]->id ); + do_action( 'woocommerce_stripe_add_payment_method', $user->ID, $payment_method_object ); } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php index 7b474ec170..0d80bd121e 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php @@ -7,6 +7,7 @@ * SEPA Payment Method class extending UPE base class */ class WC_Stripe_UPE_Payment_Method_Sepa extends WC_Stripe_UPE_Payment_Method { + use WC_Stripe_Subscriptions_Trait; const STRIPE_ID = 'sepa_debit'; @@ -30,6 +31,9 @@ public function __construct() { 'Reach 500 million customers and over 20 million businesses across the European Union.', 'woocommerce-gateway-stripe' ); + + // SEPA Direct Debit is the tokenization method for this method as well as Bancontact and iDEAL. Init subscription so it can process subscription payments. + $this->maybe_init_subscriptions(); } /** From 580f39999d827a8e88934d546b4bcfea47a17f5f Mon Sep 17 00:00:00 2001 From: James Allan Date: Fri, 9 Feb 2024 15:51:20 +1000 Subject: [PATCH 10/21] Map payment method to the gateway ID should be recorded on the subscription --- .../class-wc-stripe-upe-payment-gateway.php | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index b560a94355..11f09d7413 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -727,7 +727,7 @@ private function process_payment_with_deferred_intent( int $order_id ) { 'payment_method' => $payment_information['payment_method'], 'customer' => $payment_information['customer'], ], - $this->payment_methods[ $upe_payment_method->get_retrievable_type() ]->id + $this->get_upe_gateway_id_for_subscription_order( $upe_payment_method ) ); } @@ -1941,7 +1941,7 @@ private function handle_saving_payment_method( WC_Order $order, string $payment_ // Add the payment method information to the order. $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object ); - $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->payment_methods[ $payment_method_instance->get_retrievable_type() ]->id ); + $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->get_upe_gateway_id_for_subscription_order( $payment_method_instance ) ); do_action( 'woocommerce_stripe_add_payment_method', $user->ID, $payment_method_object ); } @@ -2159,4 +2159,23 @@ private function should_upe_payment_method_show_save_option( $payment_method ) { return false; } + + /** + * Determines the gateway ID to set as the subscription order's payment method. + * + * Some UPE payment methods use different gateway IDs to process their payments. eg Bancontact uses SEPA tokens, cards use 'stripe' etc. + * This function will return the correct gateway ID which should be recorded on the subscription so that the correct payment method is used to process future payments. + * + * @param WC_Stripe_UPE_Payment_Method $payment_method The UPE payment method instance. + * @return string The gateway ID to set on the subscription/order. + */ + private function get_upe_gateway_id_for_subscription_order( $payment_method ) { + $token_gateway_type = $payment_method->get_retrievable_type(); + + if ( 'card' !== $token_gateway_type ) { + return $this->payment_methods[ $token_gateway_type ]->id; + } + + return $this->id; + } } From 5d92694b5510602fdb60bdd7a2322ddcc85388f6 Mon Sep 17 00:00:00 2001 From: James Allan Date: Tue, 13 Feb 2024 12:01:28 +1000 Subject: [PATCH 11/21] Set payment method title and ID to the APM used --- .../class-wc-stripe-upe-payment-gateway.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 11f09d7413..04bb56ba43 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -727,7 +727,7 @@ private function process_payment_with_deferred_intent( int $order_id ) { 'payment_method' => $payment_information['payment_method'], 'customer' => $payment_information['customer'], ], - $this->get_upe_gateway_id_for_subscription_order( $upe_payment_method ) + $this->get_upe_gateway_id_for_order( $upe_payment_method ) ); } @@ -1453,10 +1453,10 @@ public function set_payment_method_title_for_order( $order, $payment_method_type if ( ! isset( $this->payment_methods[ $payment_method_type ] ) ) { return; } + $payment_method = $this->payment_methods[ $payment_method_type ]; + $payment_method_title = $payment_method->get_label(); - $payment_method_title = $this->payment_methods[ $payment_method_type ]->get_label(); - - $order->set_payment_method( self::ID ); + $order->set_payment_method( $this->get_upe_gateway_id_for_order( $payment_method ) ); $order->set_payment_method_title( $payment_method_title ); $order->save(); } @@ -1941,7 +1941,7 @@ private function handle_saving_payment_method( WC_Order $order, string $payment_ // Add the payment method information to the order. $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object ); - $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->get_upe_gateway_id_for_subscription_order( $payment_method_instance ) ); + $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object, $this->get_upe_gateway_id_for_order( $payment_method_instance ) ); do_action( 'woocommerce_stripe_add_payment_method', $user->ID, $payment_method_object ); } @@ -2169,7 +2169,7 @@ private function should_upe_payment_method_show_save_option( $payment_method ) { * @param WC_Stripe_UPE_Payment_Method $payment_method The UPE payment method instance. * @return string The gateway ID to set on the subscription/order. */ - private function get_upe_gateway_id_for_subscription_order( $payment_method ) { + private function get_upe_gateway_id_for_order( $payment_method ) { $token_gateway_type = $payment_method->get_retrievable_type(); if ( 'card' !== $token_gateway_type ) { From 5f763772fd8c1e682346bb3e4b6c1512ac5365bd Mon Sep 17 00:00:00 2001 From: James Allan Date: Tue, 13 Feb 2024 16:50:43 +1000 Subject: [PATCH 12/21] Make sure UPE methods can call methods on the main instance of the UPE gateway --- .../class-wc-stripe-upe-payment-method.php | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index 793e85f256..c70a834d25 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -94,6 +94,31 @@ public function __construct() { $this->supports = [ 'products', 'refunds' ]; } + /** + * Magic method to call methods from the main UPE Stripe gateway. + * + * Calling methods on the UPE method instance should forward the call to the main UPE Stripe gateway. + * Because the UPE methods are not actual gateways, they don't have the methods to handle payments, so we need to forward the calls to + * the main UPE Stripe gateway. + * + * That would suggest we should use a class inheritance structure, however, we don't want to extend the UPE Stripe gateway class + * because we don't want the UPE method instance of the gateway to process those calls, we want the actual main instance of the + * gateway to process them. + * + * @param string $method The method name. + * @param array $arguments The method arguments. + */ + public function __call( $method, $arguments ) { + $upe_gateway_instance = WC_Stripe::get_instance()->get_main_stripe_gateway(); + + if ( in_array( $name, get_class_methods( $upe_gateway_instance ) ) ) { + return call_user_func_array( [ $upe_gateway_instance, $method ], $arguments ); + } else { + $message = method_exists( $upe_gateway_instance, $method ) ? 'Call to private method ' : 'Call to undefined method '; + throw new \Error( $message . get_class( $this ) . '::' . $method ); + } + } + /** * Returns payment method ID * @@ -365,18 +390,6 @@ public function get_testing_instructions() { return ''; } - /** - * Processes an order payment. - * - * UPE Payment methods use the WC_Stripe_UPE_Payment_Gateway::process_payment() function. - * - * @param int $order_id The order ID to process. - * @return array The payment result. - */ - public function process_payment( $order_id ) { - return WC_Stripe::get_instance()->get_main_stripe_gateway()->process_payment( $order_id ); - } - /** * Determines if the Stripe Account country supports this UPE method. * From 7e0569d913c67f32f10c848630f4278b091490ce Mon Sep 17 00:00:00 2001 From: James Allan Date: Tue, 13 Feb 2024 17:01:48 +1000 Subject: [PATCH 13/21] Fix undefined $name error --- includes/payment-methods/class-wc-stripe-upe-payment-method.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index c70a834d25..bcf89aab62 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -111,7 +111,7 @@ public function __construct() { public function __call( $method, $arguments ) { $upe_gateway_instance = WC_Stripe::get_instance()->get_main_stripe_gateway(); - if ( in_array( $name, get_class_methods( $upe_gateway_instance ) ) ) { + if ( in_array( $method, get_class_methods( $upe_gateway_instance ) ) ) { return call_user_func_array( [ $upe_gateway_instance, $method ], $arguments ); } else { $message = method_exists( $upe_gateway_instance, $method ) ? 'Call to private method ' : 'Call to undefined method '; From 6e063604929b66ce81bccb4ff9cfcbe63d279b5b Mon Sep 17 00:00:00 2001 From: James Allan Date: Tue, 13 Feb 2024 17:46:47 +1000 Subject: [PATCH 14/21] Reinstate the process payment function for upe methods --- .../class-wc-stripe-upe-payment-method.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index bcf89aab62..78e16ac963 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -390,6 +390,18 @@ public function get_testing_instructions() { return ''; } + /** + * Processes an order payment. + * + * UPE Payment methods use the WC_Stripe_UPE_Payment_Gateway::process_payment() function. + * + * @param int $order_id The order ID to process. + * @return array The payment result. + */ + public function process_payment( $order_id ) { + return WC_Stripe::get_instance()->get_main_stripe_gateway()->process_payment( $order_id ); + } + /** * Determines if the Stripe Account country supports this UPE method. * From 691137857d82db0e0ba3c27b991e981b20d438d7 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 14 Feb 2024 11:18:16 +1000 Subject: [PATCH 15/21] Only attach hooks in the subscriptions trait once --- .../compat/trait-wc-stripe-subscriptions.php | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/includes/compat/trait-wc-stripe-subscriptions.php b/includes/compat/trait-wc-stripe-subscriptions.php index c59294dfd9..affd8a174d 100644 --- a/includes/compat/trait-wc-stripe-subscriptions.php +++ b/includes/compat/trait-wc-stripe-subscriptions.php @@ -10,6 +10,15 @@ trait WC_Stripe_Subscriptions_Trait { use WC_Stripe_Subscriptions_Utilities_Trait; + /** + * Stores a flag to indicate if the subscription integration hooks have been attached. + * + * The callbacks attached as part of maybe_init_subscriptions() only need to be attached once to avoid duplication. + * + * @var bool False by default, true once the callbacks have been attached. + */ + private static $has_attached_integration_hooks = false; + /** * Initialize subscription support and hooks. * @@ -38,18 +47,33 @@ public function maybe_init_subscriptions() { add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, [ $this, 'scheduled_subscription_payment' ], 10, 2 ); add_action( 'woocommerce_subscription_failing_payment_method_updated_' . $this->id, [ $this, 'update_failing_payment_method' ], 10, 2 ); - add_action( 'wcs_resubscribe_order_created', [ $this, 'delete_resubscribe_meta' ], 10 ); - add_action( 'wcs_renewal_order_created', [ $this, 'delete_renewal_meta' ], 10 ); + add_action( 'wc_stripe_payment_fields_' . $this->id, [ $this, 'display_update_subs_payment_checkout' ] ); add_action( 'wc_stripe_add_payment_method_' . $this->id . '_success', [ $this, 'handle_add_payment_method_success' ], 10, 2 ); - add_action( 'woocommerce_subscriptions_change_payment_before_submit', [ $this, 'differentiate_change_payment_method_form' ] ); // Display the payment method used for a subscription in the "My Subscriptions" table. add_filter( 'woocommerce_my_subscriptions_payment_method', [ $this, 'maybe_render_subscription_payment_method' ], 10, 2 ); // Allow store managers to manually set Stripe as the payment method on a subscription. add_filter( 'woocommerce_subscription_payment_meta', [ $this, 'add_subscription_payment_meta' ], 10, 2 ); + + // Validate the payment method meta data set on a subscription. add_filter( 'woocommerce_subscription_validate_payment_meta', [ $this, 'validate_subscription_payment_meta' ], 10, 2 ); + + /** + * The callbacks attached below only need to be attached once. We don't need each gateway instance to have its own callback. + * Therefore we only attach them once on the main `stripe` gateway and store a flag to indicate that they have been attached. + */ + if ( self::$has_attached_integration_hooks || WC_Gateway_Stripe::ID !== $this->id ) { + return; + } + + self::$has_attached_integration_hooks = true; + + add_action( 'woocommerce_subscriptions_change_payment_before_submit', [ $this, 'differentiate_change_payment_method_form' ] ); + add_action( 'wcs_resubscribe_order_created', [ $this, 'delete_resubscribe_meta' ], 10 ); + add_action( 'wcs_renewal_order_created', [ $this, 'delete_renewal_meta' ], 10 ); + add_filter( 'wc_stripe_display_save_payment_method_checkbox', [ $this, 'display_save_payment_method_checkbox' ] ); // Add the necessary information to create a mandate to the payment intent. From b13c1624f3e62a8f0ca758d477799f4d2bc67143 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 14 Feb 2024 16:39:12 +1000 Subject: [PATCH 16/21] Process UPE payment method refunds via the UPE gateway --- .../class-wc-stripe-upe-payment-method.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method.php b/includes/payment-methods/class-wc-stripe-upe-payment-method.php index 78e16ac963..16e18a43e6 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method.php @@ -402,6 +402,25 @@ public function process_payment( $order_id ) { return WC_Stripe::get_instance()->get_main_stripe_gateway()->process_payment( $order_id ); } + /** + * Process a refund. + * + * UPE Payment methods use the WC_Stripe_UPE_Payment_Gateway::process_payment() function. + * + * @param int $order_id Order ID. + * @param float|null $amount Refund amount. + * @param string $reason Refund reason. + * + * @return bool|\WP_Error True or false based on success, or a WP_Error object. + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + if ( ! $this->can_refund_via_stripe() ) { + return false; + } + + return WC_Stripe::get_instance()->get_main_stripe_gateway()->process_refund( $order_id, $amount, $reason ); + } + /** * Determines if the Stripe Account country supports this UPE method. * From b99580b28551efcb05fae659236f863027e6eb8c Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 14 Feb 2024 16:40:36 +1000 Subject: [PATCH 17/21] Always use the source object's payment type when processing off session payments --- includes/abstracts/abstract-wc-stripe-payment-gateway.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/includes/abstracts/abstract-wc-stripe-payment-gateway.php b/includes/abstracts/abstract-wc-stripe-payment-gateway.php index 36aed5c23d..d9aa7271af 100644 --- a/includes/abstracts/abstract-wc-stripe-payment-gateway.php +++ b/includes/abstracts/abstract-wc-stripe-payment-gateway.php @@ -1709,9 +1709,8 @@ public function create_and_confirm_intent_for_off_session( $order, $prepared_sou $full_request = $this->generate_payment_request( $order, $prepared_source ); $payment_method_types = [ 'card' ]; - if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) { - $payment_method_types = $this->get_upe_enabled_at_checkout_payment_method_ids(); - } elseif ( isset( $prepared_source->source_object->type ) ) { + + if ( isset( $prepared_source->source_object->type ) ) { $payment_method_types = [ $prepared_source->source_object->type ]; } From d1f75410372b72eda0c6d1916356f9b60dad66b3 Mon Sep 17 00:00:00 2001 From: James Allan Date: Wed, 14 Feb 2024 17:01:27 +1000 Subject: [PATCH 18/21] Use new helper function for determining the payment method id of a upe method object --- .../payment-methods/class-wc-stripe-upe-payment-gateway.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php index 1fa3fdfbe1..9ae13fb6b8 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php @@ -1310,7 +1310,7 @@ public function save_payment_method_to_order( $order, $payment_method ) { // Fetch the payment method ID from the payment method object. if ( isset( $this->payment_methods[ $payment_method->payment_method_object->type ] ) ) { - $payment_method_id = $this->payment_methods[ $payment_method->payment_method_object->type ]->id; + $payment_method_id = $this->get_upe_gateway_id_for_order( $this->payment_methods[ $payment_method->payment_method_object->type ] ); } $this->maybe_update_source_on_subscription_order( $order, $payment_method, $payment_method_id ?? $this->id ); From 9516976175dc035b36f3cac22d66762694c97dc9 Mon Sep 17 00:00:00 2001 From: Danae Millan <41606954+a-danae@users.noreply.github.com> Date: Thu, 15 Feb 2024 02:18:58 +0100 Subject: [PATCH 19/21] Remove APM payment methods in Stripe when their tokens are deleted locally (#2902) * Check whether the deleted token belongs to a reusable gateway Before, we were only checking it belonged to the main gateway. Since Split PE, APMs have their own gateway so we needed to change this check * Move the try/catch blocks up in the method * Add Bancontact, Ideal, and Sofort to the static list of reusable gateways --- includes/class-wc-stripe-payment-tokens.php | 27 ++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/includes/class-wc-stripe-payment-tokens.php b/includes/class-wc-stripe-payment-tokens.php index cca914042a..300ad22610 100644 --- a/includes/class-wc-stripe-payment-tokens.php +++ b/includes/class-wc-stripe-payment-tokens.php @@ -12,6 +12,15 @@ class WC_Stripe_Payment_Tokens { private static $_this; + const UPE_REUSABLE_GATEWAYS = [ + // Link payment methods are saved under the main Stripe gateway. + WC_Stripe_UPE_Payment_Gateway::ID, + WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_Bancontact::STRIPE_ID, + WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_Ideal::STRIPE_ID, + WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID, + WC_Stripe_UPE_Payment_Gateway::ID . '_' . WC_Stripe_UPE_Payment_Method_Sofort::STRIPE_ID, + ]; + /** * Constructor. * @@ -373,22 +382,18 @@ public function get_account_saved_payment_methods_list_item_sepa( $item, $paymen */ public function woocommerce_payment_token_deleted( $token_id, $token ) { $stripe_customer = new WC_Stripe_Customer( get_current_user_id() ); - if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) { - if ( WC_Stripe_UPE_Payment_Gateway::ID === $token->get_gateway_id() ) { - try { + try { + if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) { + if ( in_array( $token->get_gateway_id(), self::UPE_REUSABLE_GATEWAYS, true ) ) { $stripe_customer->detach_payment_method( $token->get_token() ); - } catch ( WC_Stripe_Exception $e ) { - WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() ); } - } - } else { - if ( 'stripe' === $token->get_gateway_id() || 'stripe_sepa' === $token->get_gateway_id() ) { - try { + } else { + if ( 'stripe' === $token->get_gateway_id() || 'stripe_sepa' === $token->get_gateway_id() ) { $stripe_customer->delete_source( $token->get_token() ); - } catch ( WC_Stripe_Exception $e ) { - WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() ); } } + } catch ( WC_Stripe_Exception $e ) { + WC_Stripe_Logger::log( 'Error: ' . $e->getMessage() ); } } From d6690c95279036d15a5a7a82b2286fe403ae2232 Mon Sep 17 00:00:00 2001 From: James Allan Date: Thu, 15 Feb 2024 12:48:12 +1000 Subject: [PATCH 20/21] Add subscription support for Sofort --- includes/class-wc-stripe-intent-controller.php | 2 +- .../class-wc-stripe-upe-payment-method-sofort.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-stripe-intent-controller.php b/includes/class-wc-stripe-intent-controller.php index 2493a2816f..9859569b75 100644 --- a/includes/class-wc-stripe-intent-controller.php +++ b/includes/class-wc-stripe-intent-controller.php @@ -934,7 +934,7 @@ private function build_base_payment_intent_request_params( $payment_information */ public function is_mandate_data_required( $selected_payment_type ) { - if ( in_array( $selected_payment_type, [ 'sepa_debit', 'bancontact', 'ideal' ], true ) ) { + if ( in_array( $selected_payment_type, [ 'sepa_debit', 'bancontact', 'ideal', 'sofort' ], true ) ) { return true; } diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php index 3b19026a85..bc4ebf68cf 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php @@ -22,6 +22,8 @@ public function __construct() { $this->is_reusable = true; $this->supported_currencies = [ 'EUR' ]; $this->label = __( 'Sofort', 'woocommerce-gateway-stripe' ); + $this->supports[] = 'subscriptions'; + $this->supports[] = 'tokenization'; $this->description = __( 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.', 'woocommerce-gateway-stripe' From e1e3926a932971d2bbb4f1950375843d1c329293 Mon Sep 17 00:00:00 2001 From: James Allan Date: Thu, 15 Feb 2024 16:33:12 +1000 Subject: [PATCH 21/21] Add multiple subscription support flags to Bancontact, iDEAL, Sofort methods --- .../class-wc-stripe-upe-payment-method-bancontact.php | 1 + .../class-wc-stripe-upe-payment-method-ideal.php | 1 + .../payment-methods/class-wc-stripe-upe-payment-method-sepa.php | 2 -- .../class-wc-stripe-upe-payment-method-sofort.php | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php index 47d3facc78..91b6c544e7 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-bancontact.php @@ -24,6 +24,7 @@ public function __construct() { $this->label = __( 'Bancontact', 'woocommerce-gateway-stripe' ); $this->supports[] = 'subscriptions'; $this->supports[] = 'tokenization'; + $this->supports[] = 'multiple_subscriptions'; $this->description = __( 'Bancontact is the most popular online payment method in Belgium, with over 15 million cards in circulation.', 'woocommerce-gateway-stripe' diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php index a203f60c0c..d66c2bac93 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-ideal.php @@ -23,6 +23,7 @@ public function __construct() { $this->supported_currencies = [ 'EUR' ]; $this->label = __( 'iDEAL', 'woocommerce-gateway-stripe' ); $this->supports[] = 'subscriptions'; + $this->supports[] = 'multiple_subscriptions'; $this->supports[] = 'tokenization'; $this->description = __( 'iDEAL is a Netherlands-based payment method that allows customers to complete transactions online using their bank credentials.', diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php index 0d80bd121e..4eaea04dee 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sepa.php @@ -25,8 +25,6 @@ public function __construct() { $this->is_reusable = true; $this->supported_currencies = [ 'EUR' ]; $this->label = __( 'SEPA Direct Debit', 'woocommerce-gateway-stripe' ); - $this->supports[] = 'subscriptions'; - $this->supports[] = 'tokenization'; $this->description = __( 'Reach 500 million customers and over 20 million businesses across the European Union.', 'woocommerce-gateway-stripe' diff --git a/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php b/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php index bc4ebf68cf..f7708ce679 100644 --- a/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php +++ b/includes/payment-methods/class-wc-stripe-upe-payment-method-sofort.php @@ -24,6 +24,7 @@ public function __construct() { $this->label = __( 'Sofort', 'woocommerce-gateway-stripe' ); $this->supports[] = 'subscriptions'; $this->supports[] = 'tokenization'; + $this->supports[] = 'multiple_subscriptions'; $this->description = __( 'Accept secure bank transfers from Austria, Belgium, Germany, Italy, Netherlands, and Spain.', 'woocommerce-gateway-stripe'