From 0c7422e78b84f23247d56db031aa0fde5b49a0e9 Mon Sep 17 00:00:00 2001 From: Timur Karimov Date: Tue, 17 Dec 2024 17:19:13 +0100 Subject: [PATCH 1/4] Move Promise instantiation into memoized function to ensure caching consistency (#9927) Co-authored-by: Timur Karimov --- changelog/fix-unhandled-promises | 4 + client/express-checkout/blocks/index.js | 8 +- .../utils/checkPaymentMethodIsAvailable.js | 122 +++++++++--------- 3 files changed, 69 insertions(+), 65 deletions(-) create mode 100644 changelog/fix-unhandled-promises diff --git a/changelog/fix-unhandled-promises b/changelog/fix-unhandled-promises new file mode 100644 index 00000000000..a4d1a679405 --- /dev/null +++ b/changelog/fix-unhandled-promises @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Fix payment method filtering when billing country changes in Blocks checkout. diff --git a/client/express-checkout/blocks/index.js b/client/express-checkout/blocks/index.js index 764dc2292cd..a46ef99e82b 100644 --- a/client/express-checkout/blocks/index.js +++ b/client/express-checkout/blocks/index.js @@ -39,9 +39,7 @@ const expressCheckoutElementApplePay = ( api ) => ( { return false; } - return new Promise( ( resolve ) => { - checkPaymentMethodIsAvailable( 'applePay', cart, resolve ); - } ); + return checkPaymentMethodIsAvailable( 'applePay', cart ); }, } ); @@ -77,9 +75,7 @@ const expressCheckoutElementGooglePay = ( api ) => { return false; } - return new Promise( ( resolve ) => { - checkPaymentMethodIsAvailable( 'googlePay', cart, resolve ); - } ); + return checkPaymentMethodIsAvailable( 'googlePay', cart ); }, }; }; diff --git a/client/express-checkout/utils/checkPaymentMethodIsAvailable.js b/client/express-checkout/utils/checkPaymentMethodIsAvailable.js index b592169da22..5beb7e32942 100644 --- a/client/express-checkout/utils/checkPaymentMethodIsAvailable.js +++ b/client/express-checkout/utils/checkPaymentMethodIsAvailable.js @@ -14,71 +14,75 @@ import WCPayAPI from 'wcpay/checkout/api'; import { getUPEConfig } from 'wcpay/utils/checkout'; export const checkPaymentMethodIsAvailable = memoize( - ( paymentMethod, cart, resolve ) => { - // Create the DIV container on the fly - const containerEl = document.createElement( 'div' ); + ( paymentMethod, cart ) => { + return new Promise( ( resolve ) => { + // Create the DIV container on the fly + const containerEl = document.createElement( 'div' ); - // Ensure the element is hidden and doesn’t interfere with the page layout. - containerEl.style.display = 'none'; + // Ensure the element is hidden and doesn’t interfere with the page layout. + containerEl.style.display = 'none'; - document.querySelector( 'body' ).appendChild( containerEl ); + document.querySelector( 'body' ).appendChild( containerEl ); - const root = ReactDOM.createRoot( containerEl ); + const root = ReactDOM.createRoot( containerEl ); - const api = new WCPayAPI( - { - publishableKey: getUPEConfig( 'publishableKey' ), - accountId: getUPEConfig( 'accountId' ), - forceNetworkSavedCards: getUPEConfig( - 'forceNetworkSavedCards' - ), - locale: getUPEConfig( 'locale' ), - isStripeLinkEnabled: isLinkEnabled( - getUPEConfig( 'paymentMethodsConfig' ) - ), - }, - request - ); + const api = new WCPayAPI( + { + publishableKey: getUPEConfig( 'publishableKey' ), + accountId: getUPEConfig( 'accountId' ), + forceNetworkSavedCards: getUPEConfig( + 'forceNetworkSavedCards' + ), + locale: getUPEConfig( 'locale' ), + isStripeLinkEnabled: isLinkEnabled( + getUPEConfig( 'paymentMethodsConfig' ) + ), + }, + request + ); - root.render( - - resolve( false ) } + root.render( + { - let canMakePayment = false; - if ( event.availablePaymentMethods ) { - canMakePayment = - event.availablePaymentMethods[ paymentMethod ]; - } - resolve( canMakePayment ); - root.unmount(); - containerEl.remove(); - } } - /> - - ); + > + resolve( false ) } + options={ { + paymentMethods: { + amazonPay: 'never', + applePay: + paymentMethod === 'applePay' + ? 'always' + : 'never', + googlePay: + paymentMethod === 'googlePay' + ? 'always' + : 'never', + link: 'never', + paypal: 'never', + }, + } } + onReady={ ( event ) => { + let canMakePayment = false; + if ( event.availablePaymentMethods ) { + canMakePayment = + event.availablePaymentMethods[ + paymentMethod + ]; + } + resolve( canMakePayment ); + root.unmount(); + containerEl.remove(); + } } + /> + + ); + } ); } ); From 129fe05703a44bb4085b746e540865e1e6a18995 Mon Sep 17 00:00:00 2001 From: Rafael Zaleski Date: Tue, 17 Dec 2024 15:45:40 -0300 Subject: [PATCH 2/4] Refresh page when ECE is dismissed (#9888) --- .../fix-9794-refresh-page-when-ece-dismissed | 4 + client/express-checkout/event-handlers.js | 10 ++ client/express-checkout/utils/index.ts | 1 + .../express-checkout/utils/shipping-fields.js | 131 ++++++++++++++++++ .../event-handlers.js | 14 +- .../tokenized-express-checkout/utils/index.ts | 1 + .../utils/shipping-fields.js | 131 ++++++++++++++++++ 7 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 changelog/fix-9794-refresh-page-when-ece-dismissed create mode 100644 client/express-checkout/utils/shipping-fields.js create mode 100644 client/tokenized-express-checkout/utils/shipping-fields.js diff --git a/changelog/fix-9794-refresh-page-when-ece-dismissed b/changelog/fix-9794-refresh-page-when-ece-dismissed new file mode 100644 index 00000000000..7ec81b4760e --- /dev/null +++ b/changelog/fix-9794-refresh-page-when-ece-dismissed @@ -0,0 +1,4 @@ +Significance: patch +Type: add + +Refresh the cart and checkout pages when ECE is dismissed and the shipping options were modified in the payment sheet. diff --git a/client/express-checkout/event-handlers.js b/client/express-checkout/event-handlers.js index 2d1345ff752..3c59d456251 100644 --- a/client/express-checkout/event-handlers.js +++ b/client/express-checkout/event-handlers.js @@ -13,12 +13,15 @@ import { normalizeShippingAddress, normalizeLineItems, getExpressCheckoutData, + updateShippingAddressUI, } from './utils'; import { trackExpressCheckoutButtonClick, trackExpressCheckoutButtonLoad, } from './tracking'; +let lastSelectedAddress = null; + export const shippingAddressChangeHandler = async ( api, event, elements ) => { try { const response = await api.expressCheckoutECECalculateShippingOptions( @@ -29,6 +32,9 @@ export const shippingAddressChangeHandler = async ( api, event, elements ) => { elements.update( { amount: response.total.amount, } ); + + lastSelectedAddress = event.address; + event.resolve( { shippingRates: response.shipping_options, lineItems: normalizeLineItems( response.displayItems ), @@ -171,5 +177,9 @@ export const onCompletePaymentHandler = () => { }; export const onCancelHandler = () => { + if ( lastSelectedAddress ) { + updateShippingAddressUI( lastSelectedAddress ); + } + lastSelectedAddress = null; unblockUI(); }; diff --git a/client/express-checkout/utils/index.ts b/client/express-checkout/utils/index.ts index 7e6c4bf2d09..3fcb6286071 100644 --- a/client/express-checkout/utils/index.ts +++ b/client/express-checkout/utils/index.ts @@ -2,6 +2,7 @@ * Internal dependencies */ export * from './normalize'; +export * from './shipping-fields'; import { getDefaultBorderRadius } from 'wcpay/utils/express-checkout'; interface MyWindow extends Window { diff --git a/client/express-checkout/utils/shipping-fields.js b/client/express-checkout/utils/shipping-fields.js new file mode 100644 index 00000000000..f097b1eca59 --- /dev/null +++ b/client/express-checkout/utils/shipping-fields.js @@ -0,0 +1,131 @@ +/* global jQuery */ +/** + * Internal dependencies + */ +import { normalizeShippingAddress, getExpressCheckoutData } from '.'; + +/** + * Checks if the intermediate address is redacted for the given country. + * CA and GB addresses are redacted and are causing errors until WooCommerce is able to + * handle redacted addresses. + * https://developers.google.com/pay/api/web/reference/response-objects#IntermediateAddress + * + * @param {string} country - The country code. + * + * @return {boolean} True if the postcode is redacted for the country, false otherwise. + */ +const isPostcodeRedactedForCountry = ( country ) => { + return [ 'CA', 'GB' ].includes( country ); +}; + +/* + * Updates a field in a form with a new value. + * + * @param {String} formSelector - The selector for the form containing the field. + * @param {Object} fieldName - The name of the field to update. + * @param {Object} value - The new value for the field. + */ +const updateShortcodeField = ( formSelector, fieldName, value ) => { + const field = document.querySelector( + `${ formSelector } [name="${ fieldName }"]` + ); + + if ( ! field ) return; + + // Check if the field is a dropdown (country/state). + if ( field.tagName === 'SELECT' && /country|state/.test( fieldName ) ) { + const options = Array.from( field.options ); + const match = options.find( + ( opt ) => + opt.value === value || + opt.textContent.trim().toLowerCase() === value.toLowerCase() + ); + + if ( match ) { + field.value = match.value; + jQuery( field ).trigger( 'change' ).trigger( 'close' ); + } + } else { + // Default behavior for text inputs. + field.value = value; + jQuery( field ).trigger( 'change' ); + } +}; + +/** + * Updates the WooCommerce Blocks shipping UI to reflect a new shipping address. + * + * @param {Object} eventAddress - The shipping address returned by the payment event. + */ +const updateBlocksShippingUI = ( eventAddress ) => { + wp?.data + ?.dispatch( 'wc/store/cart' ) + ?.setShippingAddress( normalizeShippingAddress( eventAddress ) ); +}; + +/** + * Updates the WooCommerce shortcode cart/checkout shipping UI to reflect a new shipping address. + * + * @param {Object} eventAddress - The shipping address returned by the payment event. + */ +const updateShortcodeShippingUI = ( eventAddress ) => { + const context = getExpressCheckoutData( 'button_context' ); + const address = normalizeShippingAddress( eventAddress ); + + const keys = [ 'country', 'state', 'city', 'postcode' ]; + + if ( context === 'cart' ) { + keys.forEach( ( key ) => { + if ( address[ key ] ) { + updateShortcodeField( + 'form.woocommerce-shipping-calculator', + `calc_shipping_${ key }`, + address[ key ] + ); + } + } ); + document + .querySelector( + 'form.woocommerce-shipping-calculator [name="calc_shipping"]' + ) + ?.click(); + } else if ( context === 'checkout' ) { + keys.forEach( ( key ) => { + if ( address[ key ] ) { + updateShortcodeField( + 'form.woocommerce-checkout', + `billing_${ key }`, + address[ key ] + ); + } + } ); + } +}; + +/** + * Updates the WooCommerce shipping UI to reflect a new shipping address. + * + * Determines the current context (cart or checkout) and updates either + * WooCommerce Blocks or shortcode-based shipping forms, if applicable. + * + * @param {Object} newAddress - The new shipping address object returned by the payment event. + * @param {string} newAddress.country - The country code of the shipping address. + * @param {string} [newAddress.state] - The state/province of the shipping address. + * @param {string} [newAddress.city] - The city of the shipping address. + * @param {string} [newAddress.postcode] - The postal/ZIP code of the shipping address. + */ +export const updateShippingAddressUI = ( newAddress ) => { + const context = getExpressCheckoutData( 'button_context' ); + const isBlocks = getExpressCheckoutData( 'has_block' ); + + if ( + [ 'cart', 'checkout' ].includes( context ) && + ! isPostcodeRedactedForCountry( newAddress.country ) + ) { + if ( isBlocks ) { + updateBlocksShippingUI( newAddress ); + } else { + updateShortcodeShippingUI( newAddress ); + } + } +}; diff --git a/client/tokenized-express-checkout/event-handlers.js b/client/tokenized-express-checkout/event-handlers.js index 12ec2513cc7..db2bc4c2c3e 100644 --- a/client/tokenized-express-checkout/event-handlers.js +++ b/client/tokenized-express-checkout/event-handlers.js @@ -8,7 +8,11 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import { getErrorMessageFromNotice, getExpressCheckoutData } from './utils'; +import { + getErrorMessageFromNotice, + getExpressCheckoutData, + updateShippingAddressUI, +} from './utils'; import { trackExpressCheckoutButtonClick, trackExpressCheckoutButtonLoad, @@ -24,6 +28,7 @@ import { transformPrice, } from './transformers/wc-to-stripe'; +let lastSelectedAddress = null; let cartApi = new ExpressCheckoutCartApi(); export const setCartApiHandler = ( handler ) => ( cartApi = handler ); export const getCartApiHandler = () => cartApi; @@ -56,6 +61,9 @@ export const shippingAddressChangeHandler = async ( event, elements ) => { cartData.totals ), } ); + + lastSelectedAddress = event.address; + event.resolve( { shippingRates: transformCartDataForShippingRates( cartData ), lineItems: transformCartDataForDisplayItems( cartData ), @@ -216,5 +224,9 @@ export const onCompletePaymentHandler = () => { }; export const onCancelHandler = () => { + if ( lastSelectedAddress ) { + updateShippingAddressUI( lastSelectedAddress ); + } + lastSelectedAddress = null; unblockUI(); }; diff --git a/client/tokenized-express-checkout/utils/index.ts b/client/tokenized-express-checkout/utils/index.ts index 98ee8b90091..bae962363b2 100644 --- a/client/tokenized-express-checkout/utils/index.ts +++ b/client/tokenized-express-checkout/utils/index.ts @@ -3,6 +3,7 @@ */ import { WCPayExpressCheckoutParams } from 'wcpay/express-checkout/utils'; export * from './normalize'; +export * from './shipping-fields'; import { getDefaultBorderRadius } from 'wcpay/utils/express-checkout'; export const getExpressCheckoutData = < diff --git a/client/tokenized-express-checkout/utils/shipping-fields.js b/client/tokenized-express-checkout/utils/shipping-fields.js new file mode 100644 index 00000000000..f097b1eca59 --- /dev/null +++ b/client/tokenized-express-checkout/utils/shipping-fields.js @@ -0,0 +1,131 @@ +/* global jQuery */ +/** + * Internal dependencies + */ +import { normalizeShippingAddress, getExpressCheckoutData } from '.'; + +/** + * Checks if the intermediate address is redacted for the given country. + * CA and GB addresses are redacted and are causing errors until WooCommerce is able to + * handle redacted addresses. + * https://developers.google.com/pay/api/web/reference/response-objects#IntermediateAddress + * + * @param {string} country - The country code. + * + * @return {boolean} True if the postcode is redacted for the country, false otherwise. + */ +const isPostcodeRedactedForCountry = ( country ) => { + return [ 'CA', 'GB' ].includes( country ); +}; + +/* + * Updates a field in a form with a new value. + * + * @param {String} formSelector - The selector for the form containing the field. + * @param {Object} fieldName - The name of the field to update. + * @param {Object} value - The new value for the field. + */ +const updateShortcodeField = ( formSelector, fieldName, value ) => { + const field = document.querySelector( + `${ formSelector } [name="${ fieldName }"]` + ); + + if ( ! field ) return; + + // Check if the field is a dropdown (country/state). + if ( field.tagName === 'SELECT' && /country|state/.test( fieldName ) ) { + const options = Array.from( field.options ); + const match = options.find( + ( opt ) => + opt.value === value || + opt.textContent.trim().toLowerCase() === value.toLowerCase() + ); + + if ( match ) { + field.value = match.value; + jQuery( field ).trigger( 'change' ).trigger( 'close' ); + } + } else { + // Default behavior for text inputs. + field.value = value; + jQuery( field ).trigger( 'change' ); + } +}; + +/** + * Updates the WooCommerce Blocks shipping UI to reflect a new shipping address. + * + * @param {Object} eventAddress - The shipping address returned by the payment event. + */ +const updateBlocksShippingUI = ( eventAddress ) => { + wp?.data + ?.dispatch( 'wc/store/cart' ) + ?.setShippingAddress( normalizeShippingAddress( eventAddress ) ); +}; + +/** + * Updates the WooCommerce shortcode cart/checkout shipping UI to reflect a new shipping address. + * + * @param {Object} eventAddress - The shipping address returned by the payment event. + */ +const updateShortcodeShippingUI = ( eventAddress ) => { + const context = getExpressCheckoutData( 'button_context' ); + const address = normalizeShippingAddress( eventAddress ); + + const keys = [ 'country', 'state', 'city', 'postcode' ]; + + if ( context === 'cart' ) { + keys.forEach( ( key ) => { + if ( address[ key ] ) { + updateShortcodeField( + 'form.woocommerce-shipping-calculator', + `calc_shipping_${ key }`, + address[ key ] + ); + } + } ); + document + .querySelector( + 'form.woocommerce-shipping-calculator [name="calc_shipping"]' + ) + ?.click(); + } else if ( context === 'checkout' ) { + keys.forEach( ( key ) => { + if ( address[ key ] ) { + updateShortcodeField( + 'form.woocommerce-checkout', + `billing_${ key }`, + address[ key ] + ); + } + } ); + } +}; + +/** + * Updates the WooCommerce shipping UI to reflect a new shipping address. + * + * Determines the current context (cart or checkout) and updates either + * WooCommerce Blocks or shortcode-based shipping forms, if applicable. + * + * @param {Object} newAddress - The new shipping address object returned by the payment event. + * @param {string} newAddress.country - The country code of the shipping address. + * @param {string} [newAddress.state] - The state/province of the shipping address. + * @param {string} [newAddress.city] - The city of the shipping address. + * @param {string} [newAddress.postcode] - The postal/ZIP code of the shipping address. + */ +export const updateShippingAddressUI = ( newAddress ) => { + const context = getExpressCheckoutData( 'button_context' ); + const isBlocks = getExpressCheckoutData( 'has_block' ); + + if ( + [ 'cart', 'checkout' ].includes( context ) && + ! isPostcodeRedactedForCountry( newAddress.country ) + ) { + if ( isBlocks ) { + updateBlocksShippingUI( newAddress ); + } else { + updateShortcodeShippingUI( newAddress ); + } + } +}; From cf6d2c1f5de1df9d60c9f040757fd35a79525b35 Mon Sep 17 00:00:00 2001 From: bruce aldridge Date: Wed, 18 Dec 2024 11:09:21 +1300 Subject: [PATCH 3/4] Performance improvements for disputes reminder task list (#9906) --- changelog/fix-9716-disputes-api-requests | 4 ++ includes/admin/class-wc-payments-admin.php | 1 + .../tasks/class-wc-payments-task-disputes.php | 42 ++++++++++++++----- includes/class-wc-payments-tasks.php | 10 ++++- .../test-class-wc-payments-task-disputes.php | 1 + 5 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 changelog/fix-9716-disputes-api-requests diff --git a/changelog/fix-9716-disputes-api-requests b/changelog/fix-9716-disputes-api-requests new file mode 100644 index 00000000000..10f5387c9b4 --- /dev/null +++ b/changelog/fix-9716-disputes-api-requests @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Performance improvements for Disputes Needing Response task shown in WooCommerce admin. diff --git a/includes/admin/class-wc-payments-admin.php b/includes/admin/class-wc-payments-admin.php index 6509cc90ecb..e7ad01fe210 100644 --- a/includes/admin/class-wc-payments-admin.php +++ b/includes/admin/class-wc-payments-admin.php @@ -1336,6 +1336,7 @@ public function add_transactions_notification_badge() { /** * Gets the number of disputes which need a response. ie have a 'needs_response' or 'warning_needs_response' status. + * Used to display a notification badge on the Payments > Disputes menu item. * * @return int The number of disputes which need a response. */ diff --git a/includes/admin/tasks/class-wc-payments-task-disputes.php b/includes/admin/tasks/class-wc-payments-task-disputes.php index b7212ec7623..7d5ac82faf4 100644 --- a/includes/admin/tasks/class-wc-payments-task-disputes.php +++ b/includes/admin/tasks/class-wc-payments-task-disputes.php @@ -49,6 +49,13 @@ class WC_Payments_Task_Disputes extends Task { */ private $disputes_due_within_1d; + /** + * A memory cache of all disputes needing response. + * + * @var array|null + */ + private $disputes_needing_response = null; + /** * WC_Payments_Task_Disputes constructor. */ @@ -57,13 +64,12 @@ public function __construct() { $this->api_client = \WC_Payments::get_payments_api_client(); $this->database_cache = \WC_Payments::get_database_cache(); parent::__construct(); - $this->init(); } /** * Initialize the task. */ - private function init() { + private function fetch_relevant_disputes() { $this->disputes_due_within_7d = $this->get_disputes_needing_response_within_days( 7 ); $this->disputes_due_within_1d = $this->get_disputes_needing_response_within_days( 1 ); } @@ -83,6 +89,9 @@ public function get_id() { * @return string */ public function get_title() { + if ( null === $this->disputes_needing_response ) { + $this->fetch_relevant_disputes(); + } if ( count( (array) $this->disputes_due_within_7d ) === 1 ) { $dispute = $this->disputes_due_within_7d[0]; $amount = WC_Payments_Utils::interpret_stripe_amount( $dispute['amount'], $dispute['currency'] ); @@ -275,6 +284,9 @@ public function is_complete() { * @return bool */ public function can_view() { + if ( null === $this->disputes_needing_response ) { + $this->fetch_relevant_disputes(); + } return count( (array) $this->disputes_due_within_7d ) > 0; } @@ -322,15 +334,24 @@ private function get_disputes_needing_response_within_days( $num_days ) { * @return array|null Array of disputes awaiting a response. Null on failure. */ private function get_disputes_needing_response() { - return $this->database_cache->get_or_add( + if ( null !== $this->disputes_needing_response ) { + return $this->disputes_needing_response; + } + + $this->disputes_needing_response = $this->database_cache->get_or_add( Database_Cache::ACTIVE_DISPUTES_KEY, function () { - $response = $this->api_client->get_disputes( - [ - 'pagesize' => 50, - 'search' => [ 'warning_needs_response', 'needs_response' ], - ] - ); + try { + $response = $this->api_client->get_disputes( + [ + 'pagesize' => 50, + 'search' => [ 'warning_needs_response', 'needs_response' ], + ] + ); + } catch ( \Exception $e ) { + // Ensure an array is always returned, even if the API call fails. + return []; + } $active_disputes = $response['data'] ?? []; @@ -347,8 +368,9 @@ function ( $a, $b ) { return $active_disputes; }, - // We'll consider all array values to be valid as the cache is only invalidated when it is deleted or it expires. 'is_array' ); + + return $this->disputes_needing_response; } } diff --git a/includes/class-wc-payments-tasks.php b/includes/class-wc-payments-tasks.php index ee3feacff48..b0b01e22896 100644 --- a/includes/class-wc-payments-tasks.php +++ b/includes/class-wc-payments-tasks.php @@ -21,7 +21,11 @@ class WC_Payments_Tasks { * WC_Payments_Admin_Tasks constructor. */ public static function init() { - include_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php'; + // As WooCommerce Onboarding tasks need to hook into 'init' and requires an API call. + // We only add this task for users who can manage_woocommerce / view the task. + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return; + } add_action( 'init', [ __CLASS__, 'add_task_disputes_need_response' ] ); } @@ -31,9 +35,11 @@ public static function init() { */ public static function add_task_disputes_need_response() { $account_service = WC_Payments::get_account_service(); - if ( ! $account_service || ! $account_service->is_stripe_account_valid() ) { + // The task is not required if the account is not connected, under review, or rejected. + if ( ! $account_service || ! $account_service->is_stripe_account_valid() || $account_service->is_account_under_review() || $account_service->is_account_rejected() ) { return; } + include_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php'; // 'extended' = 'Things to do next' task list on WooCommerce > Home. TaskLists::add_task( 'extended', new WC_Payments_Task_Disputes() ); diff --git a/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php b/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php index 43771de278d..729be9743f0 100644 --- a/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php +++ b/tests/unit/admin/tasks/test-class-wc-payments-task-disputes.php @@ -8,6 +8,7 @@ use WCPay\Constants\Country_Code; use WooCommerce\Payments\Tasks\WC_Payments_Task_Disputes; +require_once WCPAY_ABSPATH . 'includes/admin/tasks/class-wc-payments-task-disputes.php'; /** * WC_Payments_Task_Disputes unit tests. */ From 99db1fb899286f547481e0ef9969827b896eb606 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 18 Dec 2024 10:05:16 +0100 Subject: [PATCH 4/4] fix: tokenized cart w/ multiple variations (#9979) --- .../fix-tokenized-cart-multiple-variations | 5 +++++ client/tokenized-express-checkout/index.js | 17 ++++++++++++++++- .../transformers/wc-to-stripe.js | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 changelog/fix-tokenized-cart-multiple-variations diff --git a/changelog/fix-tokenized-cart-multiple-variations b/changelog/fix-tokenized-cart-multiple-variations new file mode 100644 index 00000000000..5d155cd5513 --- /dev/null +++ b/changelog/fix-tokenized-cart-multiple-variations @@ -0,0 +1,5 @@ +Significance: patch +Type: fix +Comment: fix: tokenized cart & multiple variations. + + diff --git a/client/tokenized-express-checkout/index.js b/client/tokenized-express-checkout/index.js index eca8a2f8a36..3b2eb1a8b46 100644 --- a/client/tokenized-express-checkout/index.js +++ b/client/tokenized-express-checkout/index.js @@ -326,6 +326,21 @@ jQuery( ( $ ) => { 'wcpay.express-checkout.update-button-data', 'automattic/wcpay/express-checkout', async () => { + // if the product cannot be added to cart (because of missing variation selection, etc), + // don't try to add it to the cart to get new data - the call will likely fail. + if ( + getExpressCheckoutData( 'button_context' ) === 'product' + ) { + const addToCartButton = $( + '.single_add_to_cart_button' + ); + + // First check if product can be added to cart. + if ( addToCartButton.is( '.disabled' ) ) { + return; + } + } + try { expressCheckoutButtonUi.blockButton(); @@ -375,7 +390,7 @@ jQuery( ( $ ) => { expressCheckoutButtonUi.unblockButton(); } catch ( e ) { - expressCheckoutButtonUi.hide(); + expressCheckoutButtonUi.hideContainer(); } } ); diff --git a/client/tokenized-express-checkout/transformers/wc-to-stripe.js b/client/tokenized-express-checkout/transformers/wc-to-stripe.js index 867a389006b..7c4c9c14822 100644 --- a/client/tokenized-express-checkout/transformers/wc-to-stripe.js +++ b/client/tokenized-express-checkout/transformers/wc-to-stripe.js @@ -96,7 +96,7 @@ export const transformCartDataForDisplayItems = ( cartData ) => { * @return {{id: string, label: string, amount: integer, deliveryEstimate: string}} `shippingRates` for Stripe. */ export const transformCartDataForShippingRates = ( cartData ) => - cartData.shipping_rates?.[ 0 ].shipping_rates + cartData.shipping_rates?.[ 0 ]?.shipping_rates .sort( ( rateA, rateB ) => { if ( rateA.selected === rateB.selected ) { return 0; // Keep relative order if both have the same value for 'selected'