diff --git a/changelog.txt b/changelog.txt index 7267bc461a5..1db69a2c48b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ *** WooPayments Changelog *** += 8.0.2 - 2024-08-07 = +* Fix - Add opt-in checks to prevent blocking customers using other payment methods. +* Fix - Fix error in Express Checkout Element use with coupons. +* Fix - Only enable Direct Checkout when WooPayments gateway is enabled. + = 8.0.1 - 2024-07-31 = * Fix - Reverts changes related to Direct Checkout that broke the PayPal extension. diff --git a/client/components/woopay/save-user/checkout-page-save-user.js b/client/components/woopay/save-user/checkout-page-save-user.js index 91443f3b988..1db75af335b 100644 --- a/client/components/woopay/save-user/checkout-page-save-user.js +++ b/client/components/woopay/save-user/checkout-page-save-user.js @@ -1,4 +1,5 @@ /* eslint-disable max-len */ +/* global jQuery */ /** * External dependencies */ @@ -131,55 +132,51 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { }, [ isPhoneValid ] ); useEffect( () => { - const formSubmitButton = isBlocksCheckout - ? document.querySelector( - 'button.wc-block-components-checkout-place-order-button' - ) - : document.querySelector( - 'form.woocommerce-checkout button[type="submit"]' - ); + const checkoutForm = jQuery( 'form.woocommerce-checkout' ); - if ( ! formSubmitButton ) { + checkoutForm.on( 'checkout_place_order', function () { + jQuery( '#validate-error-invalid-woopay-phone-number' ).show(); + } ); + }, [] ); + + const updatePhoneNumberValidationError = useCallback( () => { + if ( ! isSaveDetailsChecked ) { + clearValidationError( errorId ); + if ( isPhoneValid !== null ) { + onPhoneValidationChange( null ); + } return; } - const updateFormSubmitButton = () => { - if ( isSaveDetailsChecked && isPhoneValid ) { - clearValidationError( errorId ); - - // Set extension data if checkbox is selected and phone number is valid in blocks checkout. - if ( isBlocksCheckout ) { - sendExtensionData( false ); - } - } + if ( isSaveDetailsChecked && isPhoneValid ) { + clearValidationError( errorId ); - if ( isSaveDetailsChecked && ! isPhoneValid ) { - setValidationErrors( { - [ errorId ]: { - message: __( - 'Please enter a valid mobile phone number.', - 'woocommerce-payments' - ), - // Hides errors when the number has not been typed yet but shows when trying to place the order. - hidden: isPhoneValid === null, - }, - } ); + // Set extension data if checkbox is selected and phone number is valid in blocks checkout. + if ( isBlocksCheckout ) { + sendExtensionData( false ); } - }; - - updateFormSubmitButton(); + return; + } - return () => { - clearValidationError( errorId ); - }; + if ( isSaveDetailsChecked && ! isPhoneValid ) { + setValidationErrors( { + [ errorId ]: { + message: __( + 'Please enter a valid mobile phone number.', + 'woocommerce-payments' + ), + // Hides errors when the number has not been typed yet but shows when trying to place the order. + hidden: isPhoneValid === null, + }, + } ); + } }, [ - setValidationErrors, - errorId, clearValidationError, isBlocksCheckout, isPhoneValid, isSaveDetailsChecked, sendExtensionData, + setValidationErrors, ] ); // In classic checkout the saved tokens are under WCPay, so we need to check if new token is selected or not, @@ -198,9 +195,12 @@ const CheckoutPageSaveUser = ( { isBlocksCheckout } ) => { if ( isBlocksCheckout && userDataSent ) { sendExtensionData( true ); } + clearValidationError( errorId ); return null; } + updatePhoneNumberValidationError(); + return ( { name="woopay_viewport" value={ `${ viewportWidth }x${ viewportHeight }` } /> -
+
{ isBlocksCheckout={ isBlocksCheckout } />
- + { isBlocksCheckout && ( + + ) } + { ! isBlocksCheckout && ! isPhoneValid && ( + + ) }
diff --git a/client/components/woopay/save-user/test/checkout-page-save-user.test.js b/client/components/woopay/save-user/test/checkout-page-save-user.test.js index e30a1bd7f16..e14f71509d3 100644 --- a/client/components/woopay/save-user/test/checkout-page-save-user.test.js +++ b/client/components/woopay/save-user/test/checkout-page-save-user.test.js @@ -6,6 +6,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; // eslint-disable-next-line import/no-unresolved import { extensionCartUpdate } from '@woocommerce/blocks-checkout'; +import { addAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -16,6 +17,30 @@ import useSelectedPaymentMethod from '../../hooks/use-selected-payment-method'; import { getConfig } from 'utils/checkout'; import { useDispatch } from '@wordpress/data'; +const jQueryMock = ( selector ) => { + if ( typeof selector === 'function' ) { + return selector( jQueryMock ); + } + + return { + on: ( event, callbackOrSelector, callback2 ) => + addAction( + `payment-request-test.jquery-event.${ selector }${ + typeof callbackOrSelector === 'string' + ? `.${ callbackOrSelector }` + : '' + }.${ event }`, + 'tests', + typeof callbackOrSelector === 'string' + ? callback2 + : callbackOrSelector + ), + val: () => null, + is: () => null, + remove: () => null, + }; +}; + jest.mock( '../../hooks/use-woopay-user', () => jest.fn() ); jest.mock( '../../hooks/use-selected-payment-method', () => jest.fn() ); jest.mock( 'utils/checkout', () => ( { @@ -79,6 +104,8 @@ const BlocksCheckoutEnvironmentMock = ( { children } ) => ( describe( 'CheckoutPageSaveUser', () => { beforeEach( () => { + global.$ = jQueryMock; + global.jQuery = jQueryMock; useDispatch.mockImplementation( () => { return { setValidationErrors: jest.fn(), diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index a5ab114fffc..e55a44bfee9 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -7,14 +7,17 @@ * @return {Array} An array of PaymentItems */ export const normalizeLineItems = ( displayItems ) => { - return displayItems.map( ( displayItem ) => - // The amount prop is already present on the item. - ( { - ...displayItem, + return displayItems.map( ( displayItem ) => { + let amount = displayItem?.amount ?? displayItem?.value; + if ( displayItem.key === 'total_discount' ) { + amount = -amount; + } + + return { name: displayItem.label, - amount: displayItem?.amount ?? displayItem?.value, - } ) - ); + amount, + }; + } ); }; /** diff --git a/client/express-checkout/utils/test/normalize.js b/client/express-checkout/utils/test/normalize.js index aaa15dee135..e963a356911 100644 --- a/client/express-checkout/utils/test/normalize.js +++ b/client/express-checkout/utils/test/normalize.js @@ -31,22 +31,15 @@ describe( 'Express checkout normalization', () => { const expected = [ { name: 'Item 1', - label: 'Item 1', amount: 100, - value: 100, }, { name: 'Item 2', - label: 'Item 2', amount: 200, - value: 200, }, { name: 'Item 3', - label: 'Item 3', amount: 200, - valueWithTax: 300, - value: 200, }, ]; @@ -74,19 +67,61 @@ describe( 'Express checkout normalization', () => { const expected = [ { name: 'Item 1', - label: 'Item 1', amount: 100, }, { name: 'Item 2', - label: 'Item 2', amount: 200, }, { name: 'Item 3', + amount: 300, + }, + ]; + + expect( normalizeLineItems( displayItems ) ).toStrictEqual( + expected + ); + } ); + + test( 'normalizes discount line item properly', () => { + const displayItems = [ + { + label: 'Item 1', + amount: 100, + }, + { + label: 'Item 2', + amount: 200, + }, + { label: 'Item 3', amount: 300, }, + { + key: 'total_discount', + label: 'Discount', + amount: 50, + }, + ]; + + const expected = [ + { + name: 'Item 1', + amount: 100, + }, + { + name: 'Item 2', + amount: 200, + }, + { + name: 'Item 3', + amount: 300, + }, + { + name: 'Discount', + amount: -50, + }, ]; expect( normalizeLineItems( displayItems ) ).toStrictEqual( diff --git a/includes/class-wc-payments-features.php b/includes/class-wc-payments-features.php index 2be033177ca..7d1a2316b19 100644 --- a/includes/class-wc-payments-features.php +++ b/includes/class-wc-payments-features.php @@ -270,7 +270,7 @@ public static function is_woopay_direct_checkout_enabled() { $is_direct_checkout_eligible = is_array( $account_cache ) && ( $account_cache['platform_direct_checkout_eligible'] ?? false ); $is_direct_checkout_flag_enabled = '1' === get_option( self::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); - return $is_direct_checkout_eligible && $is_direct_checkout_flag_enabled && self::is_woopay_enabled(); + return $is_direct_checkout_eligible && $is_direct_checkout_flag_enabled && self::is_woopayments_gateway_enabled() && self::is_woopay_enabled(); } /** @@ -392,4 +392,16 @@ public static function to_array() { ] ); } + + /** + * Checks if WooCommerce Payments gateway is enabled. + * + * @return bool True if WooCommerce Payments gateway is enabled, false otherwise. + */ + private static function is_woopayments_gateway_enabled() { + $woopayments_settings = get_option( 'woocommerce_woocommerce_payments_settings' ); + $woopayments_enabled_setting = $woopayments_settings['enabled'] ?? 'no'; + + return 'yes' === $woopayments_enabled_setting; + } } diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index 4f3edb6ad91..da1f6de2664 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -1799,6 +1799,7 @@ public static function init_woopay() { // Update email field location. add_action( 'woocommerce_checkout_billing', [ __CLASS__, 'woopay_fields_before_billing_details' ], -50 ); add_filter( 'woocommerce_form_field_email', [ __CLASS__, 'filter_woocommerce_form_field_woopay_email' ], 20, 4 ); + add_action( 'woocommerce_checkout_process', [ __CLASS__, 'maybe_show_woopay_phone_number_error' ] ); include_once __DIR__ . '/woopay-user/class-woopay-save-user.php'; @@ -2027,4 +2028,17 @@ public static function maybe_disable_wcpay_subscriptions_on_update() { update_option( WC_Payments_Features::WCPAY_SUBSCRIPTIONS_FLAG_NAME, '0' ); } } + + /** + * Show error when WooPay opt-in is checked but no phone number was typed. + */ + public static function maybe_show_woopay_phone_number_error() { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( isset( $_POST['save_user_in_woopay'] ) && 'true' === $_POST['save_user_in_woopay'] ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( ! isset( $_POST['woopay_user_phone_field'] ) || ! isset( $_POST['woopay_user_phone_field']['no-country-code'] ) || empty( $_POST['woopay_user_phone_field']['no-country-code'] ) ) { + wc_add_notice( '' . __( 'Mobile Number', 'woocommerce-payments' ) . ' ' . __( 'is required to create an WooPay account.', 'woocommerce-payments' ), 'error' ); + } + } + } } diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 57b8f133b84..460cd5f27dc 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -124,6 +124,7 @@ public function build_display_items( $itemized_display_items = false ) { if ( WC()->cart->has_discount() ) { $items[] = [ + 'key' => 'total_discount', 'label' => esc_html( __( 'Discount', 'woocommerce-payments' ) ), 'amount' => WC_Payments_Utils::prepare_amount( $discounts, $currency ), ]; diff --git a/package-lock.json b/package-lock.json index fe60e78af6c..635cdb79740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "woocommerce-payments", - "version": "8.0.1", + "version": "8.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "woocommerce-payments", - "version": "8.0.1", + "version": "8.0.2", "hasInstallScript": true, "license": "GPL-3.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 17c364d8db3..f5af812b62c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "woocommerce-payments", - "version": "8.0.1", + "version": "8.0.2", "main": "webpack.config.js", "author": "Automattic", "license": "GPL-3.0-or-later", diff --git a/readme.txt b/readme.txt index e36bf85c21c..829e48f925e 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment Requires at least: 6.0 Tested up to: 6.6 Requires PHP: 7.3 -Stable tag: 8.0.1 +Stable tag: 8.0.2 License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -94,6 +94,12 @@ Please note that our support for the checkout block is still experimental and th == Changelog == += 8.0.2 - 2024-08-07 = +* Fix - Add opt-in checks to prevent blocking customers using other payment methods. +* Fix - Fix error in Express Checkout Element use with coupons. +* Fix - Only enable Direct Checkout when WooPayments gateway is enabled. + + = 8.0.1 - 2024-07-31 = * Fix - Reverts changes related to Direct Checkout that broke the PayPal extension. diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php index 0229e014001..217bb7be513 100644 --- a/tests/unit/test-class-wc-payments-features.php +++ b/tests/unit/test-class-wc-payments-features.php @@ -226,7 +226,8 @@ public function test_is_woopay_express_checkout_enabled_returns_false_when_woopa $this->assertFalse( WC_Payments_Features::is_woopay_express_checkout_enabled() ); } - public function test_is_woopay_direct_checkout_enabled_returns_true() { + public function test_is_woopay_direct_checkout_enabled_returns_true_when_woopayments_gateway_enabled() { + update_option( 'woocommerce_woocommerce_payments_settings', [ 'enabled' => 'yes' ] ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); $this->mock_cache->method( 'get' )->willReturn( @@ -238,6 +239,19 @@ public function test_is_woopay_direct_checkout_enabled_returns_true() { $this->assertTrue( WC_Payments_Features::is_woopay_direct_checkout_enabled() ); } + public function test_is_woopay_direct_checkout_enabled_returns_false_when_woopayments_gateway_disabled() { + update_option( 'woocommerce_woocommerce_payments_settings', [ 'enabled' => 'no' ] ); + $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); + $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); + $this->mock_cache->method( 'get' )->willReturn( + [ + 'platform_checkout_eligible' => true, + 'platform_direct_checkout_eligible' => true, + ] + ); + $this->assertFalse( WC_Payments_Features::is_woopay_direct_checkout_enabled() ); + } + public function test_is_woopay_direct_checkout_enabled_returns_false_when_flag_is_false() { $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '0' ); @@ -263,6 +277,7 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_woopay } public function test_is_woopay_direct_checkout_enabled_returns_true_when_first_party_auth_is_disabled() { + update_option( 'woocommerce_woocommerce_payments_settings', [ 'enabled' => 'yes' ] ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '0' ); $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' ); diff --git a/woocommerce-payments.php b/woocommerce-payments.php index cbf816f0e5a..e62a16e7948 100644 --- a/woocommerce-payments.php +++ b/woocommerce-payments.php @@ -11,7 +11,7 @@ * WC tested up to: 9.1.2 * Requires at least: 6.0 * Requires PHP: 7.3 - * Version: 8.0.1 + * Version: 8.0.2 * Requires Plugins: woocommerce * * @package WooCommerce\Payments