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/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 ];
}
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 ] ) );
+ }
}
}
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-intent-controller.php b/includes/class-wc-stripe-intent-controller.php
index 3680f8e8b3..9859569b75 100644
--- a/includes/class-wc-stripe-intent-controller.php
+++ b/includes/class-wc-stripe-intent-controller.php
@@ -933,12 +933,12 @@ private function build_base_payment_intent_request_params( $payment_information
* @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', 'sofort' ], 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 );
}
/**
@@ -951,11 +951,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.
diff --git a/includes/class-wc-stripe-payment-tokens.php b/includes/class-wc-stripe-payment-tokens.php
index c70a3e900b..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.
*
@@ -230,7 +239,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 +250,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();
}
@@ -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() );
}
}
diff --git a/includes/compat/trait-wc-stripe-subscriptions.php b/includes/compat/trait-wc-stripe-subscriptions.php
index 0fc9e8392e..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.
@@ -386,8 +410,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 +432,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 +440,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 40df744c59..9ae13fb6b8 100644
--- a/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
+++ b/includes/payment-methods/class-wc-stripe-upe-payment-gateway.php
@@ -360,6 +360,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();
@@ -422,7 +423,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 ] ),
];
}
@@ -714,6 +715,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;
// Update saved payment method async to include billing details.
if ( $payment_information['is_using_saved_payment_method'] ) {
@@ -754,7 +756,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'],
@@ -766,7 +768,8 @@ private function process_payment_with_deferred_intent( int $order_id ) {
(object) [
'payment_method' => $payment_information['payment_method'],
'customer' => $payment_information['customer'],
- ]
+ ],
+ $this->get_upe_gateway_id_for_order( $upe_payment_method )
);
}
@@ -1305,7 +1308,12 @@ public function save_payment_method_to_order( $order, $payment_method ) {
$order->save();
}
- $this->maybe_update_source_on_subscription_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->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 );
}
/**
@@ -1487,10 +1495,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();
}
@@ -1897,6 +1905,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
}
@@ -2019,14 +2031,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 ordeer.
- $prepared_payment_method_object = $this->prepare_payment_method( $payment_method_object );
- $this->maybe_update_source_on_subscription_order( $order, $prepared_payment_method_object );
-
// 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->get_upe_gateway_id_for_order( $payment_method_instance ) );
+
do_action( 'woocommerce_stripe_add_payment_method', $user->ID, $payment_method_object );
}
@@ -2229,8 +2241,7 @@ private function get_return_url_for_redirect( $order, $save_payment_method ) {
);
}
- /**
- * Retrieves the (possible) existing payment intent for an order and payment method types.
+ /* Retrieves the (possible) existing payment intent for an order and payment method types.
*
* @param WC_Order $order The order.
* @param array $payment_method_types The payment method types.
@@ -2290,4 +2301,38 @@ private function get_payment_method_types_for_intent_creation( string $selected_
// Otherwise, return the selected payment method type.
return [ $selected_payment_type ];
}
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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_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;
+ }
}
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..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
@@ -22,6 +22,9 @@ public function __construct() {
$this->is_reusable = true;
$this->supported_currencies = [ 'EUR' ];
$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-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..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
@@ -22,6 +22,9 @@ public function __construct() {
$this->is_reusable = true;
$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.',
'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..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
@@ -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';
@@ -28,6 +29,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();
}
/**
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..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
@@ -22,6 +22,9 @@ public function __construct() {
$this->is_reusable = true;
$this->supported_currencies = [ 'EUR' ];
$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'
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..16e18a43e6 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,32 @@ 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' ];
+ }
+
+ /**
+ * 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( $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 ';
+ throw new \Error( $message . get_class( $this ) . '::' . $method );
+ }
}
/**
@@ -277,7 +303,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 );
@@ -376,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.
*
@@ -418,6 +463,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() );