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() );