diff --git a/changelog.txt b/changelog.txt index 50b07d61d..8a1a652f3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,6 +2,8 @@ = 8.9.0 - xxxx-xx-xx = * Tweak - Improve error message displayed when payment method creation fails in classic checkout. +* Dev - Replace two occurrences of payment method names with their constant equivalents. +* Fix - Hide express checkout when credit card payments are not enabled. * Fix - Fix issues when detaching payment methods on staging sites (with the new checkout experience enabled). * Fix - Display a notice if taxes vary by customer's billing address when checking out using the Stripe Express Checkout Element. * Tweak - Makes the new Stripe Express Checkout Element enabled by default. @@ -22,6 +24,8 @@ * Tweak - Remove the subscription order notes added each time a source wasn't migrated. * Tweak - Update ECE default button type. * Fix - Fix position of ECE button on shortcode cart page. +* Fix - Call ECE specific 'paymentFailed' function only when payment request fails. +* Fix - Fix issue in purchasing subscriptions when the store has no shipping options. = 8.8.2 - 2024-11-07 = * Fix - Prevent marking renewal orders as processing/completed multiple times due to handling the Stripe webhook in parallel. diff --git a/client/blocks/express-checkout/express-checkout-container.js b/client/blocks/express-checkout/express-checkout-container.js index ace659c7c..0d564e022 100644 --- a/client/blocks/express-checkout/express-checkout-container.js +++ b/client/blocks/express-checkout/express-checkout-container.js @@ -1,7 +1,11 @@ import React from 'react'; import { Elements } from '@stripe/react-stripe-js'; import ExpressCheckoutComponent from './express-checkout-component'; -import { getPaymentMethodTypesForExpressMethod } from 'wcstripe/express-checkout/utils'; +import { + getExpressCheckoutButtonAppearance, + getExpressCheckoutData, + getPaymentMethodTypesForExpressMethod, +} from 'wcstripe/express-checkout/utils'; export const ExpressCheckoutContainer = ( props ) => { const { stripe, billing, expressPaymentMethod } = props; @@ -13,6 +17,8 @@ export const ExpressCheckoutContainer = ( props ) => { paymentMethodTypes: getPaymentMethodTypesForExpressMethod( expressPaymentMethod ), + appearance: getExpressCheckoutButtonAppearance(), + locale: getExpressCheckoutData( 'stripe' )?.locale ?? 'en', }; return ( diff --git a/client/blocks/express-checkout/hooks.js b/client/blocks/express-checkout/hooks.js index 35c85615f..dd9f18dfb 100644 --- a/client/blocks/express-checkout/hooks.js +++ b/client/blocks/express-checkout/hooks.js @@ -39,8 +39,10 @@ export const useExpressCheckout = ( { window.location = redirectUrl; }; - const abortPayment = ( onConfirmEvent, message ) => { - onConfirmEvent.paymentFailed( { reason: 'fail' } ); + const abortPayment = ( onConfirmEvent, message, isOrderError = false ) => { + if ( ! isOrderError ) { + onConfirmEvent.paymentFailed( { reason: 'fail' } ); + } setExpressPaymentError( message ); onAbortPaymentHandler( onConfirmEvent, message ); }; diff --git a/client/blocks/express-checkout/index.js b/client/blocks/express-checkout/index.js index 27222319d..3f3607bed 100644 --- a/client/blocks/express-checkout/index.js +++ b/client/blocks/express-checkout/index.js @@ -25,6 +25,10 @@ const expressCheckoutElementsGooglePay = ( api ) => ( { ), edit: , canMakePayment: ( { cart } ) => { + if ( ! getBlocksConfiguration()?.shouldShowExpressCheckoutButton ) { + return false; + } + // eslint-disable-next-line camelcase if ( typeof wc_stripe_express_checkout_params === 'undefined' ) { return false; @@ -53,6 +57,10 @@ const expressCheckoutElementsApplePay = ( api ) => ( { ), edit: , canMakePayment: ( { cart } ) => { + if ( ! getBlocksConfiguration()?.shouldShowExpressCheckoutButton ) { + return false; + } + // eslint-disable-next-line camelcase if ( typeof wc_stripe_express_checkout_params === 'undefined' ) { return false; @@ -80,6 +88,10 @@ const expressCheckoutElementsStripeLink = ( api ) => ( { ), edit: , canMakePayment: ( { cart } ) => { + if ( ! getBlocksConfiguration()?.shouldShowExpressCheckoutButton ) { + return false; + } + // eslint-disable-next-line camelcase if ( typeof wc_stripe_express_checkout_params === 'undefined' ) { return false; diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js index a5d36aaaf..27a5d9e69 100644 --- a/client/entrypoints/express-checkout/index.js +++ b/client/entrypoints/express-checkout/index.js @@ -132,6 +132,7 @@ jQuery( function ( $ ) { currency: options.currency, paymentMethodCreation: 'manual', appearance: getExpressCheckoutButtonAppearance(), + locale: getExpressCheckoutData( 'stripe' )?.locale ?? 'en', paymentMethodTypes: getExpressPaymentMethodTypes(), } ); @@ -289,6 +290,7 @@ jQuery( function ( $ ) { currency: getExpressCheckoutData( 'checkout' ) .currency_code, appearance: getExpressCheckoutButtonAppearance(), + locale: getExpressCheckoutData( 'stripe' )?.locale ?? 'en', displayItems, order, } ); @@ -466,9 +468,12 @@ jQuery( function ( $ ) { * * @param {PaymentResponse} payment Payment response instance. * @param {string} message Error message to display. + * @param {boolean} isOrderError Whether the error is related to the order creation. */ - abortPayment: ( payment, message ) => { - payment.paymentFailed( { reason: 'fail' } ); + abortPayment: ( payment, message, isOrderError = false ) => { + if ( ! isOrderError ) { + payment.paymentFailed( { reason: 'fail' } ); + } onAbortPaymentHandler( payment, message ); displayExpressCheckoutNotice( message, 'error' ); diff --git a/client/express-checkout/__tests__/event-handler.test.js b/client/express-checkout/__tests__/event-handler.test.js index a6af507eb..801e5c347 100644 --- a/client/express-checkout/__tests__/event-handler.test.js +++ b/client/express-checkout/__tests__/event-handler.test.js @@ -340,7 +340,8 @@ describe( 'Express checkout event handlers', () => { ); expect( abortPayment ).toHaveBeenCalledWith( event, - 'Order creation error' + 'Order creation error', + true ); expect( completePayment ).not.toHaveBeenCalled(); } ); @@ -467,7 +468,8 @@ describe( 'Express checkout event handlers', () => { ); expect( abortPayment ).toHaveBeenCalledWith( event, - 'Order creation error' + 'Order creation error', + true ); expect( completePayment ).not.toHaveBeenCalled(); } ); diff --git a/client/express-checkout/event-handler.js b/client/express-checkout/event-handler.js index 79defef53..2caf8f317 100644 --- a/client/express-checkout/event-handler.js +++ b/client/express-checkout/event-handler.js @@ -92,7 +92,8 @@ export const onConfirmHandler = async ( if ( orderResponse.result !== 'success' ) { return abortPayment( event, - getErrorMessageFromNotice( orderResponse.messages ) + getErrorMessageFromNotice( orderResponse.messages ), + true ); } diff --git a/client/settings/payment-request-section/index.js b/client/settings/payment-request-section/index.js index 99f4b2ab1..7cde86b09 100644 --- a/client/settings/payment-request-section/index.js +++ b/client/settings/payment-request-section/index.js @@ -40,6 +40,7 @@ const PaymentRequestSection = () => { } }; + const displayExpressPaymentMethods = enabledMethodIds.includes( 'card' ); const displayLinkPaymentMethod = enabledMethodIds.includes( 'card' ) && availablePaymentMethodIds.includes( linkMethodID ); @@ -53,70 +54,83 @@ const PaymentRequestSection = () => {
    -
  • -
    - -
    -
    - -
    -
    -
    - { __( - 'Apple Pay / Google Pay', - 'woocommerce-gateway-stripe' - ) } + { ! displayExpressPaymentMethods && + ! displayLinkPaymentMethod && ( +
  • +
    + { __( + 'Credit card / debit card must be enabled as a payment method in order to use Express Checkout.', + 'woocommerce-gateway-stripe' + ) } +
    +
  • + ) } + { displayExpressPaymentMethods && ( +
  • +
    +
    -
    - { - /* eslint-disable jsx-a11y/anchor-has-content */ - interpolateComponents( { - mixedString: __( - 'Boost sales by offering a fast, simple, and secure checkout experience.' + - 'By enabling this feature, you agree to {{stripeLink}}Stripe{{/stripeLink}}, ' + - "{{appleLink}}Apple{{/appleLink}}, and {{googleLink}}Google{{/googleLink}}'s terms of use.", - 'woocommerce-gateway-stripe' - ), - components: { - stripeLink: ( - - ), - appleLink: ( - - ), - googleLink: ( - +
    + +
    +
    -
    - -
  • + + + ) } { displayLinkPaymentMethod && (
  • diff --git a/includes/class-wc-stripe-blocks-support.php b/includes/class-wc-stripe-blocks-support.php index fa93dcdda..0fd0a49bb 100644 --- a/includes/class-wc-stripe-blocks-support.php +++ b/includes/class-wc-stripe-blocks-support.php @@ -188,13 +188,14 @@ public function get_payment_method_data() { $js_params, // Blocks-specific options [ - 'icons' => $this->get_icons(), - 'supports' => $this->get_supported_features(), - 'showSavedCards' => $this->get_show_saved_cards(), - 'showSaveOption' => $this->get_show_save_option(), - 'isAdmin' => is_admin(), - 'shouldShowPaymentRequestButton' => $this->should_show_payment_request_button(), - 'button' => [ + 'icons' => $this->get_icons(), + 'supports' => $this->get_supported_features(), + 'showSavedCards' => $this->get_show_saved_cards(), + 'showSaveOption' => $this->get_show_save_option(), + 'isAdmin' => is_admin(), + 'shouldShowPaymentRequestButton' => $this->should_show_payment_request_button(), + 'shouldShowExpressCheckoutButton' => $this->should_show_express_checkout_button(), + 'button' => [ 'customLabel' => $this->payment_request_configuration->get_button_label(), ], ] @@ -255,10 +256,15 @@ private function should_show_payment_request_button() { * @return boolean True if ECEs should be displayed, false otherwise. */ private function should_show_express_checkout_button() { + // Don't show if ECEs are turned off in settings. + if ( ! $this->express_checkout_configuration->express_checkout_helper->is_express_checkout_enabled() ) { + return false; + } + // Don't show if ECEs are supposed to be hidden on the cart page. if ( has_block( 'woocommerce/cart' ) - && ! $this->express_checkout_configuration->express_checkout_helper->should_show_ece_on_cart_page()() + && ! $this->express_checkout_configuration->express_checkout_helper->should_show_ece_on_cart_page() ) { return false; } diff --git a/includes/class-wc-stripe-payment-tokens.php b/includes/class-wc-stripe-payment-tokens.php index fb8c8f1be..0615a2709 100644 --- a/includes/class-wc-stripe-payment-tokens.php +++ b/includes/class-wc-stripe-payment-tokens.php @@ -112,7 +112,7 @@ public static function get_token_from_request( array $request ) { * @return bool */ public static function customer_has_saved_methods( $customer_id ) { - $gateways = [ 'stripe', 'stripe_sepa' ]; + $gateways = [ WC_Gateway_Stripe::ID, WC_Gateway_Stripe_Sepa::ID ]; if ( empty( $customer_id ) ) { return false; @@ -166,7 +166,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom $stored_tokens[ $token->get_token() ] = $token; } - if ( 'stripe' === $gateway_id ) { + if ( WC_Gateway_Stripe::ID === $gateway_id ) { $stripe_customer = new WC_Stripe_Customer( $customer_id ); $stripe_sources = $stripe_customer->get_sources(); @@ -175,7 +175,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom if ( ! isset( $stored_tokens[ $source->id ] ) ) { $token = new WC_Payment_Token_CC(); $token->set_token( $source->id ); - $token->set_gateway_id( 'stripe' ); + $token->set_gateway_id( WC_Gateway_Stripe::ID ); if ( WC_Stripe_Helper::is_card_payment_method( $source ) ) { $token->set_card_type( strtolower( $source->card->brand ) ); @@ -194,7 +194,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom if ( ! isset( $stored_tokens[ $source->id ] ) && WC_Stripe_Payment_Methods::CARD === $source->object ) { $token = new WC_Payment_Token_CC(); $token->set_token( $source->id ); - $token->set_gateway_id( 'stripe' ); + $token->set_gateway_id( WC_Gateway_Stripe::ID ); $token->set_card_type( strtolower( $source->brand ) ); $token->set_last4( $source->last4 ); $token->set_expiry_month( $source->exp_month ); @@ -209,7 +209,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom } } - if ( 'stripe_sepa' === $gateway_id ) { + if ( WC_Gateway_Stripe_Sepa::ID === $gateway_id ) { $stripe_customer = new WC_Stripe_Customer( $customer_id ); $stripe_sources = $stripe_customer->get_sources(); @@ -218,7 +218,7 @@ public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $custom if ( ! isset( $stored_tokens[ $source->id ] ) ) { $token = new WC_Payment_Token_SEPA(); $token->set_token( $source->id ); - $token->set_gateway_id( 'stripe_sepa' ); + $token->set_gateway_id( WC_Gateway_Stripe_Sepa::ID ); $token->set_last4( $source->sepa_debit->last4 ); $token->set_user_id( $customer_id ); $token->save(); @@ -272,7 +272,7 @@ public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, // - APM tokens from before Split PE was in place. // - Non-credit card tokens using the sources API. Payments using these will fail with the PaymentMethods API. if ( - ( 'stripe' === $token->get_gateway_id() && WC_Stripe_Payment_Methods::SEPA === $token->get_type() ) || + ( WC_Gateway_Stripe::ID === $token->get_gateway_id() && WC_Stripe_Payment_Methods::SEPA === $token->get_type() ) || ! $this->is_valid_payment_method_id( $token->get_token(), $this->get_payment_method_type_from_token( $token ) ) ) { $deprecated_tokens[ $token->get_token() ] = $token; @@ -435,7 +435,7 @@ public function woocommerce_payment_token_deleted( $token_id, $token ) { $stripe_customer->detach_payment_method( $token->get_token() ); } else { - if ( 'stripe' === $token->get_gateway_id() || 'stripe_sepa' === $token->get_gateway_id() ) { + if ( WC_Gateway_Stripe::ID === $token->get_gateway_id() || WC_Gateway_Stripe_Sepa::ID === $token->get_gateway_id() ) { $stripe_customer->delete_source( $token->get_token() ); } } @@ -460,7 +460,7 @@ public function woocommerce_payment_token_set_default( $token_id ) { $stripe_customer->set_default_payment_method( $token->get_token() ); } } else { - if ( 'stripe' === $token->get_gateway_id() || 'stripe_sepa' === $token->get_gateway_id() ) { + if ( WC_Gateway_Stripe::ID === $token->get_gateway_id() || WC_Gateway_Stripe_Sepa::ID === $token->get_gateway_id() ) { $stripe_customer->set_default_source( $token->get_token() ); } } diff --git a/includes/compat/trait-wc-stripe-subscriptions.php b/includes/compat/trait-wc-stripe-subscriptions.php index bc032d379..91bc3bc54 100644 --- a/includes/compat/trait-wc-stripe-subscriptions.php +++ b/includes/compat/trait-wc-stripe-subscriptions.php @@ -682,7 +682,7 @@ public function add_subscription_payment_meta( $payment_meta, $subscription ) { ], '_stripe_source_id' => [ 'value' => $source_id, - 'label' => 'Stripe Source ID', + 'label' => 'Stripe Payment Method ID', ], ], ]; @@ -720,7 +720,7 @@ public function validate_subscription_payment_meta( $payment_method_id, $payment && 0 !== strpos( $payment_meta['post_meta']['_stripe_source_id']['value'], 'pm_' ) ) ) { - throw new Exception( __( 'Invalid source ID. A valid source "Stripe Source ID" must begin with "src_", "pm_", or "card_".', 'woocommerce-gateway-stripe' ) ); + throw new Exception( __( 'Invalid payment method ID. A valid "Stripe Payment Method ID" must begin with "src_", "pm_", or "card_".', 'woocommerce-gateway-stripe' ) ); } } } diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-element.php b/includes/payment-methods/class-wc-stripe-express-checkout-element.php index df7bd845c..f9286866d 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -99,6 +99,7 @@ public function init() { add_action( 'woocommerce_checkout_order_processed', [ $this, 'add_order_meta' ], 10, 2 ); add_filter( 'woocommerce_login_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); add_filter( 'woocommerce_registration_redirect', [ $this, 'get_login_redirect_url' ], 10, 3 ); + add_filter( 'woocommerce_cart_needs_shipping_address', [ $this, 'filter_cart_needs_shipping_address' ], 11, 1 ); add_action( 'before_woocommerce_pay_form', [ $this, 'localize_pay_for_order_page_scripts' ] ); } @@ -415,4 +416,17 @@ public function display_express_checkout_button_separator_html() { express_checkout_helper->has_subscription_product() && wc_get_shipping_method_count( true, true ) === 0 ) { + return false; + } + + return $needs_shipping_address; + } } diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-helper.php b/includes/payment-methods/class-wc-stripe-express-checkout-helper.php index 08886c016..1b7a0cfb2 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-helper.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-helper.php @@ -542,6 +542,11 @@ public function should_show_express_checkout_button() { return false; } + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + if ( ! isset( $available_gateways['stripe'] ) ) { + return false; + } + // Don't show if on the cart or checkout page, or if page contains the cart or checkout // shortcodes, with items in the cart that aren't supported. if ( @@ -561,7 +566,7 @@ public function should_show_express_checkout_button() { return false; } - // Don't show if product page PRB is disabled. + // Don't show if product page ECE is disabled. if ( $this->is_product() && ! $this->should_show_ece_on_product_pages() ) { return false; } diff --git a/includes/payment-methods/class-wc-stripe-payment-request.php b/includes/payment-methods/class-wc-stripe-payment-request.php index 660e6ea40..7bb83d579 100644 --- a/includes/payment-methods/class-wc-stripe-payment-request.php +++ b/includes/payment-methods/class-wc-stripe-payment-request.php @@ -967,6 +967,11 @@ public function should_show_payment_request_button() { return false; } + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + if ( ! isset( $available_gateways['stripe'] ) ) { + return false; + } + // Don't show if on the cart or checkout page, or if page contains the cart or checkout // shortcodes, with items in the cart that aren't supported. if ( diff --git a/readme.txt b/readme.txt index 1de69c57c..dd44beddd 100644 --- a/readme.txt +++ b/readme.txt @@ -112,6 +112,8 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o = 8.9.0 - xxxx-xx-xx = * Tweak - Improve error message displayed when payment method creation fails in classic checkout. +* Dev - Replace two occurrences of payment method names with their constant equivalents. +* Fix - Hide express checkout when credit card payments are not enabled. * Fix - Fix issues when detaching payment methods on staging sites (with the new checkout experience enabled). * Fix - Display a notice if taxes vary by customer's billing address when checking out using the Stripe Express Checkout Element. * Tweak - Makes the new Stripe Express Checkout Element enabled by default. @@ -132,5 +134,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Tweak - Remove the subscription order notes added each time a source wasn't migrated. * Tweak - Update ECE default button type. * Fix - Fix position of ECE button on shortcode cart page. +* Fix - Call ECE specific 'paymentFailed' function only when payment request fails. +* Fix - Fix issue in purchasing subscriptions when the store has no shipping options. [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). diff --git a/tests/phpunit/test-wc-stripe-express-checkout-helper.php b/tests/phpunit/test-wc-stripe-express-checkout-helper.php index a416a4b25..201097806 100644 --- a/tests/phpunit/test-wc-stripe-express-checkout-helper.php +++ b/tests/phpunit/test-wc-stripe-express-checkout-helper.php @@ -3,11 +3,11 @@ /** * These tests make assertions against class WC_Stripe_Express_Checkout_Helper. * - * @package WooCommerce_Stripe/Tests/WC_Stripe_Express_Checkout_Helper + * @package WooCommerce_Stripe/Tests/WC_Stripe_Express_Checkout_Helper_Test */ /** - * WC_Stripe_Express_Checkout_Helper class. + * WC_Stripe_Express_Checkout_Helper_Test class. */ class WC_Stripe_Express_Checkout_Helper_Test extends WP_UnitTestCase { public function set_up() { @@ -54,6 +54,10 @@ public function test_hides_ece_if_cannot_compute_taxes() { if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { define( 'WOOCOMMERCE_CHECKOUT', true ); } + $original_gateways = WC()->payment_gateways()->payment_gateways; + WC()->payment_gateways()->payment_gateways = [ + 'stripe' => new WC_Gateway_Stripe(), + ]; // Create virtual product and add to cart. $virtual_product = WC_Helper_Product::create_simple_product(); @@ -85,6 +89,47 @@ public function test_hides_ece_if_cannot_compute_taxes() { $shippable_product = WC_Helper_Product::create_simple_product(); WC()->cart->add_to_cart( $shippable_product->get_id(), 1 ); $this->assertTrue( $wc_stripe_ece_helper_mock->should_show_express_checkout_button() ); + + // Restore original gateways. + WC()->payment_gateways()->payment_gateways = $original_gateways; + } + + /** + * Test should_show_express_checkout_button, gateway logic. + */ + public function test_hides_ece_if_stripe_gateway_unavailable() { + $wc_stripe_ece_helper_mock = $this->createPartialMock( + WC_Stripe_Express_Checkout_Helper::class, + [ + 'is_product', + 'allowed_items_in_cart', + 'should_show_ece_on_cart_page', + 'should_show_ece_on_checkout_page', + ] + ); + $wc_stripe_ece_helper_mock->expects( $this->any() )->method( 'is_product' )->willReturn( false ); + $wc_stripe_ece_helper_mock->expects( $this->any() )->method( 'allowed_items_in_cart' )->willReturn( true ); + $wc_stripe_ece_helper_mock->expects( $this->any() )->method( 'should_show_ece_on_cart_page' )->willReturn( true ); + $wc_stripe_ece_helper_mock->expects( $this->any() )->method( 'should_show_ece_on_checkout_page' )->willReturn( true ); + $wc_stripe_ece_helper_mock->testmode = true; + if ( ! defined( 'WOOCOMMERCE_CHECKOUT' ) ) { + define( 'WOOCOMMERCE_CHECKOUT', true ); + } + $original_gateways = WC()->payment_gateways()->payment_gateways; + + // Hide if 'stripe' gateway is unavailable. + update_option( 'woocommerce_calc_taxes', 'no' ); + WC()->payment_gateways()->payment_gateways = [ + 'stripe' => new WC_Gateway_Stripe(), + 'stripe_alipay' => new WC_Gateway_Stripe_Alipay(), + ]; + $this->assertTrue( $wc_stripe_ece_helper_mock->should_show_express_checkout_button() ); + + unset( WC()->payment_gateways()->payment_gateways['stripe'] ); + $this->assertFalse( $wc_stripe_ece_helper_mock->should_show_express_checkout_button() ); + + // Restore original gateways. + WC()->payment_gateways()->payment_gateways = $original_gateways; } /**