From 49c02b879f02680409273508e0003ba6fc6e5013 Mon Sep 17 00:00:00 2001 From: Wesley Rosa Date: Mon, 20 Jan 2025 10:16:54 -0300 Subject: [PATCH 1/4] Fix insufficient funds error message on the legacy shortcode checkout (#3731) * Fix insufficient funds error message on the legacy shortcode checkout * Changelog and readme entries * Adding conditional operator when checking for decline code --- assets/js/stripe.js | 7 ++++++- changelog.txt | 1 + includes/class-wc-stripe-helper.php | 1 + readme.txt | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/js/stripe.js b/assets/js/stripe.js index a3ac7d010e..b724ab41f8 100644 --- a/assets/js/stripe.js +++ b/assets/js/stripe.js @@ -885,10 +885,15 @@ jQuery( function($ ) { message = wc_stripe_params.invalid_request_error; } - if ( wc_stripe_params.hasOwnProperty(result.error.code) ) { + if ( wc_stripe_params.hasOwnProperty( result.error.code ) ) { message = wc_stripe_params[ result.error.code ]; } + // Correctly sets the insufficient funds message. + if ( 'card_declined' === result.error.code && 'insufficient_funds' === result.error?.decline_code ) { + message = wc_stripe_params.insufficient_funds; + } + wc_stripe_form.reset(); $( '.woocommerce-NoticeGroup-checkout' ).remove(); console.log( result.error.message ); // Leave for troubleshooting. diff --git a/changelog.txt b/changelog.txt index f441a67147..ae3ab104f6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ *** Changelog *** = 9.2.0 - xxxx-xx-xx = +* Fix - Fixes incorrect error message for card failures due insufficient funds on the shortcode checkout page (legacy). * Fix - Fixes deprecation warnings related to nullable method parameters when using PHP 8.4, and increases the minimum PHP version Code Sniffer considers to 7.4. * Fix - Adds support for the Reunion country when checking out using the new checkout experience. * Add - Support zero-amount refunds. diff --git a/includes/class-wc-stripe-helper.php b/includes/class-wc-stripe-helper.php index 6c6c72cdb7..a7b9dda147 100644 --- a/includes/class-wc-stripe-helper.php +++ b/includes/class-wc-stripe-helper.php @@ -265,6 +265,7 @@ public static function get_localized_messages() { 'tax_id_invalid' => __( 'Invalid Tax Id, please try again with a valid tax id', 'woocommerce-gateway-stripe' ), 'invalid_wallet_type' => __( 'Invalid wallet payment type, please try again or use an alternative method.', 'woocommerce-gateway-stripe' ), 'payment_intent_authentication_failure' => __( 'We are unable to authenticate your payment method. Please choose a different payment method and try again.', 'woocommerce-gateway-stripe' ), + 'insufficient_funds' => __( 'Your card has insufficient funds.', 'woocommerce-gateway-stripe' ), ] ); } diff --git a/readme.txt b/readme.txt index 95101f8f99..f4cea7a86d 100644 --- a/readme.txt +++ b/readme.txt @@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o == Changelog == = 9.2.0 - xxxx-xx-xx = +* Fix - Fixes incorrect error message for card failures due insufficient funds on the shortcode checkout page (legacy). * Fix - Fixes deprecation warnings related to nullable method parameters when using PHP 8.4, and increases the minimum PHP version Code Sniffer considers to 7.4. * Fix - Adds support for the Reunion country when checking out using the new checkout experience. * Add - Support zero-amount refunds. From 2c8de79d96e985f5a2a83eb844c1c89b696ad453 Mon Sep 17 00:00:00 2001 From: Wesley Rosa Date: Mon, 20 Jan 2025 10:27:57 -0300 Subject: [PATCH 2/4] Using Blocks API to process ECE orders (#3691) * Initial draft for Blocks API integration * Introducing feature flag * Introducing feature flag * Revert unnecessary changes * Remove redundant shipping and billing properties * Fix nonce * Fix request format * Add missing fields * Fix payment method param * Fix payment intent confirmation * Creating new methods to reduce cyclomatic complexity * Renaming some methods * Changelog and readme entries * Fix passing of repeated params * Reducing code duplication * Adding specific unit tests * Adding specific unit tests * Adding specific unit test * Removing unnecessary attribute * Update client/express-checkout/event-handler.js Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com> * Fix lint issue * Fix tests * Fix tests --------- Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com> --- changelog.txt | 1 + client/api/index.js | 44 ++ client/blocks/express-checkout/hooks.js | 13 +- client/entrypoints/express-checkout/index.js | 13 +- .../__tests__/event-handler.test.js | 404 ++++++++++++++++++ client/express-checkout/event-handler.js | 89 ++++ .../utils/__tests__/normalize.test.js | 303 ++++++++++++- client/express-checkout/utils/normalize.js | 107 +++++ ...ass-wc-stripe-express-checkout-element.php | 2 + ...lass-wc-stripe-express-checkout-helper.php | 9 + readme.txt | 1 + ...test-wc-stripe-express-checkout-helper.php | 21 +- 12 files changed, 999 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index ae3ab104f6..1e740243b3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ *** Changelog *** = 9.2.0 - xxxx-xx-xx = +* Tweak - Process ECE orders using the Blocks API. * Fix - Fixes incorrect error message for card failures due insufficient funds on the shortcode checkout page (legacy). * Fix - Fixes deprecation warnings related to nullable method parameters when using PHP 8.4, and increases the minimum PHP version Code Sniffer considers to 7.4. * Fix - Adds support for the Reunion country when checking out using the new checkout experience. diff --git a/client/api/index.js b/client/api/index.js index fa85ef36c8..f0bb94701d 100644 --- a/client/api/index.js +++ b/client/api/index.js @@ -1,5 +1,6 @@ /* global Stripe */ import { __ } from '@wordpress/i18n'; +import apiFetch from '@wordpress/api-fetch'; import { getExpressCheckoutData, getExpressCheckoutAjaxURL, @@ -539,6 +540,49 @@ export default class WCStripeAPI { } ); } + /** + * Creates order based on Express Checkout ECE payment method. + * + * @param {Object} paymentData Order data. + * @return {Promise} Promise for the request to the server. + */ + expressCheckoutECECreateOrderForBlocksAPI( paymentData ) { + return this.postToBlocksAPI( '/wc/store/v1/checkout', { + ...getRequiredFieldDataFromCheckoutForm( paymentData ), + } ); + } + + /** + * Pays for an order based on the Express Checkout payment method. + * + * @param {number} order The order ID. + * @param {Object} paymentData Order data. + * @return {Promise} Promise for the request to the server. + */ + expressCheckoutECEPayForOrderForBlocksAPI( order, paymentData ) { + return this.postToBlocksAPI( `/wc/store/v1/checkout/${ order }`, { + ...paymentData, + } ); + } + + /** + * Posts data to the Blocks API. + * + * @param {string} path The path to post to. + * @param {Object} data The data to post. + * @return {Promise} The promise for the request to the server. + */ + postToBlocksAPI( path, data ) { + return apiFetch( { + method: 'POST', + path, + headers: { + Nonce: getExpressCheckoutData( 'nonce' )?.wc_store_api, + }, + data, + } ); + } + /** * Add product to cart from product page. * diff --git a/client/blocks/express-checkout/hooks.js b/client/blocks/express-checkout/hooks.js index 30e78550f8..7addae27d0 100644 --- a/client/blocks/express-checkout/hooks.js +++ b/client/blocks/express-checkout/hooks.js @@ -7,6 +7,7 @@ import { onClickHandler, onCompletePaymentHandler, onConfirmHandler, + onConfirmHandlerForBlocksAPI, } from 'wcstripe/express-checkout/event-handler'; import { displayExpressCheckoutNotice, @@ -116,7 +117,17 @@ export const useExpressCheckout = ( { ); const onConfirm = async ( event ) => { - await onConfirmHandler( + if ( getExpressCheckoutData( 'use_blocks_api' ) ) { + return await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + } + return await onConfirmHandler( api, stripe, elements, diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js index f2ab6557bd..d78fa489b6 100644 --- a/client/entrypoints/express-checkout/index.js +++ b/client/entrypoints/express-checkout/index.js @@ -19,6 +19,7 @@ import { onClickHandler, onCompletePaymentHandler, onConfirmHandler, + onConfirmHandlerForBlocksAPI, onReadyHandler, shippingAddressChangeHandler, shippingRateChangeHandler, @@ -233,7 +234,17 @@ jQuery( function ( $ ) { eceButton.on( 'confirm', async ( event ) => { const order = options.order ? options.order : 0; - + if ( getExpressCheckoutData( 'use_blocks_api' ) ) { + return await onConfirmHandlerForBlocksAPI( + api, + api.getStripe(), + elements, + wcStripeECE.completePayment, + wcStripeECE.abortPayment, + event, + order + ); + } return await onConfirmHandler( api, api.getStripe(), diff --git a/client/express-checkout/__tests__/event-handler.test.js b/client/express-checkout/__tests__/event-handler.test.js index 9aa754fb1a..180af81533 100644 --- a/client/express-checkout/__tests__/event-handler.test.js +++ b/client/express-checkout/__tests__/event-handler.test.js @@ -6,9 +6,12 @@ import { normalizeShippingAddress, normalizeOrderData, normalizePayForOrderData, + normalizeOrderDataForBlocksAPI, + normalizePayForOrderDataForBlocksAPI, } from '../utils'; import { onConfirmHandler, + onConfirmHandlerForBlocksAPI, shippingAddressChangeHandler, shippingRateChangeHandler, } from 'wcstripe/express-checkout/event-handler'; @@ -581,4 +584,405 @@ describe( 'Express checkout event handlers', () => { expect( completePayment ).not.toHaveBeenCalled(); } ); } ); + + describe( 'onConfirmHandlerForBlocksAPI', () => { + let api; + let stripe; + let elements; + let completePayment; + let abortPayment; + let event; + let order; + + beforeEach( () => { + api = { + expressCheckoutECECreateOrderForBlocksAPI: jest.fn(), + expressCheckoutECEPayForOrderForBlocksAPI: jest.fn(), + confirmIntent: jest.fn(), + }; + stripe = { + createPaymentMethod: jest.fn(), + }; + elements = { + submit: jest.fn(), + }; + completePayment = jest.fn(); + abortPayment = jest.fn(); + event = { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + organization: 'Some Company', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + phone: '(123) 456-7890', + }, + shippingAddress: { + name: 'John Doe', + organization: 'Some Company', + address: { + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + }, + shippingRate: { id: 'rate_1' }, + expressPaymentType: 'express', + }; + order = 123; + } ); + + afterEach( () => { + jest.clearAllMocks(); + } ); + + test( 'should abort payment if elements.submit fails', async () => { + elements.submit.mockResolvedValue( { + error: { message: 'Submit error' }, + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( elements.submit ).toHaveBeenCalled(); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Submit error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if stripe.createPaymentMethod fails', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + error: { message: 'Payment method error' }, + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( elements.submit ).toHaveBeenCalled(); + expect( stripe.createPaymentMethod ).toHaveBeenCalledWith( { + elements, + } ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Payment method error' + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if expressCheckoutECECreateOrder fails', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'error', + payment_details: [ + { + key: 'errorMessage', + value: 'Order creation error', + }, + ], + }, + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + const expectedOrderData = normalizeOrderDataForBlocksAPI( + event, + 'pm_123' + ); + expect( + api.expressCheckoutECECreateOrderForBlocksAPI + ).toHaveBeenCalledWith( expectedOrderData ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Order creation error', + true + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment if confirmationRequest is true', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'success', + redirect_url: 'https://example.com/redirect', + }, + } ); + api.confirmIntent.mockReturnValue( true ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment if confirmationRequest returns a redirect URL', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'success', + redirect_url: 'https://example.com/redirect', + }, + } ); + api.confirmIntent.mockReturnValue( { + request: Promise.resolve( + 'https://example.com/confirmation_redirect' + ), + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/confirmation_redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if confirmIntent throws an error', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'success', + redirect_url: 'https://example.com/redirect', + }, + } ); + api.confirmIntent.mockReturnValue( { + request: Promise.reject( + new Error( 'Intent confirmation error' ) + ), + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Intent confirmation error', + true + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment if expressCheckoutECEPayForOrder fails', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'error', + payment_details: [ + { + key: 'errorMessage', + value: 'Order creation error', + }, + ], + }, + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + const expectedOrderData = normalizePayForOrderDataForBlocksAPI( + event, + 'pm_123' + ); + expect( + api.expressCheckoutECEPayForOrderForBlocksAPI + ).toHaveBeenCalledWith( 123, expectedOrderData ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Order creation error', + true + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment (pay for order) if confirmationRequest is true', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'success', + redirect_url: 'https://example.com/redirect', + }, + } ); + api.confirmIntent.mockReturnValue( true ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should complete payment (pay for order) if confirmationRequest returns a redirect URL', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'success', + redirect_url: 'https://example.com/redirect', + }, + } ); + api.confirmIntent.mockReturnValue( { + request: Promise.resolve( + 'https://example.com/confirmation_redirect' + ), + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( completePayment ).toHaveBeenCalledWith( + 'https://example.com/confirmation_redirect' + ); + expect( abortPayment ).not.toHaveBeenCalled(); + } ); + + test( 'should abort payment (pay for order) if confirmIntent throws an error', async () => { + elements.submit.mockResolvedValue( {} ); + stripe.createPaymentMethod.mockResolvedValue( { + paymentMethod: { id: 'pm_123' }, + } ); + api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + payment_result: { + payment_status: 'success', + redirect_url: 'https://example.com/redirect', + }, + } ); + api.confirmIntent.mockReturnValue( { + request: Promise.reject( + new Error( 'Intent confirmation error' ) + ), + } ); + + await onConfirmHandlerForBlocksAPI( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order + ); + + expect( api.confirmIntent ).toHaveBeenCalledWith( + 'https://example.com/redirect' + ); + expect( abortPayment ).toHaveBeenCalledWith( + event, + 'Intent confirmation error', + true + ); + expect( completePayment ).not.toHaveBeenCalled(); + } ); + } ); } ); diff --git a/client/express-checkout/event-handler.js b/client/express-checkout/event-handler.js index eb99b0c693..21a854156f 100644 --- a/client/express-checkout/event-handler.js +++ b/client/express-checkout/event-handler.js @@ -6,6 +6,8 @@ import { normalizeShippingAddress, normalizeLineItems, getExpressCheckoutData, + normalizeOrderDataForBlocksAPI, + normalizePayForOrderDataForBlocksAPI, } from './utils'; import { trackExpressCheckoutButtonClick, @@ -122,6 +124,93 @@ export const onConfirmHandler = async ( } }; +export const onConfirmHandlerForBlocksAPI = async ( + api, + stripe, + elements, + completePayment, + abortPayment, + event, + order = 0 // Order ID for the pay for order flow. +) => { + const submitResponse = await elements.submit(); + if ( submitResponse?.error ) { + return abortPayment( event, submitResponse?.error?.message ); + } + + const { paymentMethod, error } = await stripe.createPaymentMethod( { + elements, + } ); + + if ( error ) { + return abortPayment( event, error.message ); + } + + try { + // Kick off checkout processing step. + let orderResponse; + if ( ! order ) { + orderResponse = await api.expressCheckoutECECreateOrderForBlocksAPI( + normalizeOrderDataForBlocksAPI( event, paymentMethod.id ) + ); + } else { + orderResponse = await api.expressCheckoutECEPayForOrderForBlocksAPI( + order, + normalizePayForOrderDataForBlocksAPI( event, paymentMethod.id ) + ); + } + + if ( orderResponse.payment_result.payment_status !== 'success' ) { + return abortPayment( + event, + getErrorMessageFromNotice( + orderResponse.payment_result?.payment_details.find( + ( detail ) => detail.key === 'errorMessage' + )?.value + ), + true + ); + } + + const confirmationRequest = api.confirmIntent( + orderResponse.payment_result.redirect_url + ); + + // `true` means there is no intent to confirm. + if ( confirmationRequest === true ) { + completePayment( orderResponse.payment_result.redirect_url ); + } else { + const { request } = confirmationRequest; + const redirectUrl = await request; + + completePayment( redirectUrl ); + } + } catch ( e ) { + let errorMessage; + if ( e.message ) { + errorMessage = e.message; + } else { + const paymentDetailsErrorMessage = e.payment_result?.payment_details.find( + ( detail ) => detail.key === 'errorMessage' + )?.value; + if ( paymentDetailsErrorMessage ) { + errorMessage = paymentDetailsErrorMessage; + } + } + if ( ! errorMessage ) { + errorMessage = __( + 'There was a problem processing the order.', + 'woocommerce-gateway-stripe' + ); + } + return abortPayment( + event, + getErrorMessageFromNotice( errorMessage ), + true + ); + } +}; + export const onReadyHandler = function ( { availablePaymentMethods } ) { if ( availablePaymentMethods ) { const enabledMethods = Object.entries( availablePaymentMethods ) diff --git a/client/express-checkout/utils/__tests__/normalize.test.js b/client/express-checkout/utils/__tests__/normalize.test.js index d39d15a9ea..c780cbf1f7 100644 --- a/client/express-checkout/utils/__tests__/normalize.test.js +++ b/client/express-checkout/utils/__tests__/normalize.test.js @@ -4,7 +4,9 @@ import { normalizeLineItems, normalizeOrderData, + normalizeOrderDataForBlocksAPI, normalizePayForOrderData, + normalizePayForOrderDataForBlocksAPI, normalizeShippingAddress, } from '../normalize'; @@ -263,6 +265,8 @@ describe( 'Express checkout normalization', () => { billing_address_2: '', billing_city: '', billing_state: '', + express_checkout_type: undefined, + express_payment_type: undefined, billing_postcode: '', shipping_first_name: '', shipping_last_name: '', @@ -281,7 +285,6 @@ describe( 'Express checkout normalization', () => { terms: 1, 'wc-stripe-is-deferred-intent': true, 'wc-stripe-payment-method': paymentMethodId, - express_payment_type: undefined, }; expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( @@ -346,6 +349,304 @@ describe( 'Express checkout normalization', () => { } ); } ); + describe( 'normalizeOrderDataForBlocksAPI', () => { + test( 'should normalize order data with complete event and paymentMethodId', () => { + const event = { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + organization: 'Some Company', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + phone: '(123) 456-7890', + }, + shippingAddress: { + name: 'John Doe', + organization: 'Some Company', + address: { + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + }, + shippingRate: { id: 'rate_1' }, + expressPaymentType: 'express', + }; + + const paymentMethodId = 'pm_123456'; + + const expectedNormalizedData = { + billing_address: { + address_1: '123 Main St', + address_2: 'Apt 4B', + city: 'New York', + company: 'Some Company', + country: 'US', + email: 'john.doe@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + postcode: '10001', + state: 'NY', + }, + customer_note: undefined, + payment_data: [ + { + key: 'payment_method', + value: 'stripe', + }, + { + key: 'wc-stripe-payment-method', + value: 'pm_123456', + }, + { + key: 'express_payment_type', + value: 'express', + }, + { + key: 'wc-stripe-is-deferred-intent', + value: true, + }, + ], + payment_method: 'stripe', + shipping_address: { + address_1: '123 Main St', + address_2: 'Apt 4B', + city: 'New York', + company: 'Some Company', + country: 'US', + first_name: 'John', + last_name: 'Doe', + method: [ 'rate_1' ], + phone: '1234567890', + postcode: '10001', + state: 'NY', + }, + }; + + expect( + normalizeOrderDataForBlocksAPI( event, paymentMethodId ) + ).toEqual( expectedNormalizedData ); + } ); + + test( 'should normalize order data with missing optional event fields', () => { + const event = {}; + const paymentMethodId = 'pm_123456'; + + const expectedNormalizedData = { + billing_address: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + email: '', + first_name: '', + last_name: '-', + phone: '', + postcode: '', + state: '', + }, + customer_note: undefined, + payment_data: [ + { + key: 'payment_method', + value: 'stripe', + }, + { + key: 'wc-stripe-payment-method', + value: 'pm_123456', + }, + { + key: 'express_payment_type', + value: undefined, + }, + { + key: 'wc-stripe-is-deferred-intent', + value: true, + }, + ], + payment_method: 'stripe', + shipping_address: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + method: [ null ], + phone: '', + postcode: '', + state: '', + }, + }; + + expect( + normalizeOrderDataForBlocksAPI( event, paymentMethodId ) + ).toEqual( expectedNormalizedData ); + } ); + + test( 'should normalize order data with minimum required fields', () => { + const event = { + billingDetails: { + name: 'John', + }, + }; + const paymentMethodId = 'pm_123456'; + + const expectedNormalizedData = { + billing_address: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + email: '', + first_name: 'John', + last_name: '', + phone: '', + postcode: '', + state: '', + }, + customer_note: undefined, + payment_data: [ + { + key: 'payment_method', + value: 'stripe', + }, + { + key: 'wc-stripe-payment-method', + value: 'pm_123456', + }, + { + key: 'express_payment_type', + value: undefined, + }, + { + key: 'wc-stripe-is-deferred-intent', + value: true, + }, + ], + payment_method: 'stripe', + shipping_address: { + address_1: '', + address_2: '', + city: '', + company: '', + country: '', + first_name: '', + last_name: '', + method: [ null ], + phone: '', + postcode: '', + state: '', + }, + }; + + expect( + normalizeOrderDataForBlocksAPI( event, paymentMethodId ) + ).toEqual( expectedNormalizedData ); + } ); + } ); + + describe( 'normalizePayForOrderDataForBlocksAPI', () => { + test( 'should normalize pay for order data with complete event and paymentMethodId', () => { + const event = { + billingDetails: { + name: 'John Doe', + email: 'john.doe@example.com', + address: { + organization: 'Some Company', + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + phone: '(123) 456-7890', + }, + shippingAddress: { + name: 'John Doe', + organization: 'Some Company', + address: { + country: 'US', + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + postal_code: '10001', + }, + }, + shippingRate: { id: 'rate_1' }, + expressPaymentType: 'express', + }; + + expect( + normalizePayForOrderDataForBlocksAPI( event, 'pm_123456' ) + ).toEqual( { + payment_data: [ + { + key: 'payment_method', + value: 'stripe', + }, + { + key: 'wc-stripe-payment-method', + value: 'pm_123456', + }, + { + key: 'express_payment_type', + value: 'express', + }, + { + key: 'wc-stripe-is-deferred-intent', + value: true, + }, + ], + payment_method: 'stripe', + } ); + } ); + + test( 'should normalize pay for order data with empty event and empty payment method', () => { + const event = {}; + const paymentMethodId = ''; + + expect( + normalizePayForOrderDataForBlocksAPI( event, paymentMethodId ) + ).toEqual( { + payment_data: [ + { + key: 'payment_method', + value: 'stripe', + }, + { + key: 'wc-stripe-payment-method', + value: '', + }, + { + key: 'express_payment_type', + value: undefined, + }, + { + key: 'wc-stripe-is-deferred-intent', + value: true, + }, + ], + payment_method: 'stripe', + } ); + } ); + } ); + describe( 'normalizeShippingAddress', () => { test( 'should normalize shipping address with all fields present', () => { const shippingAddress = { diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index 0101e664ab..852f61b015 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -78,6 +78,64 @@ export const normalizeOrderData = ( event, paymentMethodId ) => { }; }; +/** + * Normalize order data from Stripe's object to the expected format for WC (when using the Blocks API). + * + * @param {Object} event Stripe's event object. + * @param {string} paymentMethodId Stripe's payment method id. + * + * @return {Object} Order object in the format WooCommerce expects. + */ +export const normalizeOrderDataForBlocksAPI = ( event, paymentMethodId ) => { + const name = event?.billingDetails?.name; + const email = event?.billingDetails?.email ?? ''; + const billing = event?.billingDetails?.address ?? {}; + const shipping = event?.shippingAddress ?? {}; + + const phone = + event?.billingDetails?.phone?.replace( /[() -]/g, '' ) ?? + event?.payerPhone?.replace( /[() -]/g, '' ) ?? + ''; + + return { + billing_address: { + first_name: name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '', + last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '-', + company: billing?.organization ?? '', + email: email ?? event?.payerEmail ?? '', + phone, + country: billing?.country ?? '', + address_1: billing?.line1 ?? '', + address_2: billing?.line2 ?? '', + city: billing?.city ?? '', + state: billing?.state ?? '', + postcode: billing?.postal_code ?? '', + }, + shipping_address: { + first_name: + shipping?.name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '', + last_name: + shipping?.name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '', + company: shipping?.organization ?? '', + phone, + country: shipping?.address?.country ?? '', + address_1: shipping?.address?.line1 ?? '', + address_2: shipping?.address?.line2 ?? '', + city: shipping?.address?.city ?? '', + state: shipping?.address?.state ?? '', + postcode: shipping?.address?.postal_code ?? '', + method: [ event?.shippingRate?.id ?? null ], + }, + customer_note: event.order_comments, + payment_method: 'stripe', + payment_data: buildBlocksAPIPaymentData( + event?.expressPaymentType, + paymentMethodId + ), + ...extractOrderAttributionData(), + }; +}; + /** * Normalize Pay for Order data from Stripe's object to the expected format for WC. * @@ -95,6 +153,27 @@ export const normalizePayForOrderData = ( event, paymentMethodId ) => { }; }; +/** + * Normalize Pay for Order data from Stripe's object to the expected format for WC (when using Blocks API). + * + * @param {Object} event Stripe's event object. + * @param {string} paymentMethodId Stripe's payment method id. + * + * @return {Object} Order object in the format WooCommerce expects. + */ +export const normalizePayForOrderDataForBlocksAPI = ( + event, + paymentMethodId +) => { + return { + payment_method: 'stripe', + payment_data: buildBlocksAPIPaymentData( + event?.expressPaymentType, + paymentMethodId + ), + }; +}; + /** * Normalize shipping address information from Stripe's address object to * the cart shipping address object shape. @@ -122,3 +201,31 @@ export const normalizeShippingAddress = ( shippingAddress ) => { postcode: shippingAddress?.postal_code ?? '', }; }; + +/** + * Builds the payment data for the Blocks API. + * + * @param {string} expressPaymentType The express payment type. + * @param {string} paymentMethodId The payment method ID. + * @return {Array} The payment data. + */ +const buildBlocksAPIPaymentData = ( expressPaymentType, paymentMethodId ) => { + return [ + { + key: 'payment_method', + value: 'stripe', + }, + { + key: 'wc-stripe-payment-method', + value: paymentMethodId, + }, + { + key: 'express_payment_type', + value: expressPaymentType, + }, + { + key: 'wc-stripe-is-deferred-intent', + value: true, + }, + ]; +}; 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 35ef041662..13500b4f88 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -197,6 +197,7 @@ public function javascript_params() { 'log_errors' => wp_create_nonce( 'wc-stripe-log-errors' ), 'clear_cart' => wp_create_nonce( 'wc-stripe-clear-cart' ), 'pay_for_order' => wp_create_nonce( 'wc-stripe-pay-for-order' ), + 'wc_store_api' => wp_create_nonce( 'wc_store_api' ), ], 'i18n' => [ 'no_prepaid_card' => __( 'Sorry, we\'re not accepting prepaid cards at this time.', 'woocommerce-gateway-stripe' ), @@ -213,6 +214,7 @@ public function javascript_params() { 'product' => $this->express_checkout_helper->get_product_data(), 'is_cart_page' => is_cart(), 'taxes_based_on_billing' => wc_tax_enabled() && get_option( 'woocommerce_tax_based_on' ) === 'billing', + 'use_blocks_api' => $this->express_checkout_helper->use_blocks_api(), ]; } 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 fe8aa12724..3b2d57e1f5 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-helper.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-helper.php @@ -1354,6 +1354,15 @@ public function is_express_checkout_enabled() { return isset( $this->stripe_settings['payment_request'] ) && 'yes' === $this->stripe_settings['payment_request']; } + /** + * Returns whether Stripe express checkout element should use the Blocks API. + * + * @return boolean + */ + public function use_blocks_api() { + return isset( $this->stripe_settings['express_checkout_use_blocks_api'] ) && 'yes' === $this->stripe_settings['express_checkout_use_blocks_api']; + } + /** * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated * during the express checkout get_shipping_options flow. diff --git a/readme.txt b/readme.txt index f4cea7a86d..8e6f600858 100644 --- a/readme.txt +++ b/readme.txt @@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o == Changelog == = 9.2.0 - xxxx-xx-xx = +* Tweak - Process ECE orders using the Blocks API. * Fix - Fixes incorrect error message for card failures due insufficient funds on the shortcode checkout page (legacy). * Fix - Fixes deprecation warnings related to nullable method parameters when using PHP 8.4, and increases the minimum PHP version Code Sniffer considers to 7.4. * Fix - Adds support for the Reunion country when checking out using the new checkout experience. diff --git a/tests/phpunit/test-wc-stripe-express-checkout-helper.php b/tests/phpunit/test-wc-stripe-express-checkout-helper.php index b618aaa6fc..47f9a86902 100644 --- a/tests/phpunit/test-wc-stripe-express-checkout-helper.php +++ b/tests/phpunit/test-wc-stripe-express-checkout-helper.php @@ -16,11 +16,12 @@ class WC_Stripe_Express_Checkout_Helper_Test extends WP_UnitTestCase { public function set_up() { parent::set_up(); - $stripe_settings = WC_Stripe_Helper::get_stripe_settings(); - $stripe_settings['enabled'] = 'yes'; - $stripe_settings['testmode'] = 'yes'; - $stripe_settings['test_publishable_key'] = 'pk_test_key'; - $stripe_settings['test_secret_key'] = 'sk_test_key'; + $stripe_settings = WC_Stripe_Helper::get_stripe_settings(); + $stripe_settings['enabled'] = 'yes'; + $stripe_settings['testmode'] = 'yes'; + $stripe_settings['test_publishable_key'] = 'pk_test_key'; + $stripe_settings['test_secret_key'] = 'sk_test_key'; + $stripe_settings['express_checkout_use_blocks_api'] = 'yes'; WC_Stripe_Helper::update_main_stripe_settings( $stripe_settings ); } @@ -311,4 +312,14 @@ public function provide_test_get_normalized_postal_code() { ], ]; } + + /** + * Test for `use_blocks_api`. + * + * @return void + */ + public function test_use_blocks_api() { + $wc_stripe_ece_helper = new WC_Stripe_Express_Checkout_Helper(); + $this->assertTrue( $wc_stripe_ece_helper->use_blocks_api() ); + } } From a92867d7bbc36290728fc25c3a8da52b52320e3a Mon Sep 17 00:00:00 2001 From: Wesley Rosa Date: Mon, 20 Jan 2025 10:55:05 -0300 Subject: [PATCH 3/4] Fix order attribution data with ECE and Blocks API (#3722) * Initial draft for Blocks API integration * Introducing feature flag * Introducing feature flag * Revert unnecessary changes * Remove redundant shipping and billing properties * Fix nonce * Fix request format * Add missing fields * Fix payment method param * Fix payment intent confirmation * Creating new methods to reduce cyclomatic complexity * Renaming some methods * Changelog and readme entries * Fix passing of repeated params * Reducing code duplication * Adding specific unit tests * Adding specific unit tests * Adding specific unit test * Removing unnecessary attribute * Update client/express-checkout/event-handler.js Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com> * Fix lint issue * Fix tests * Fix tests * Fix order attribution data with ECE and Blocks API * Moving method call * Fix tests * Adding specific unit tests * Changelog and readme entries * Fix tests --------- Co-authored-by: Mayisha <33387139+Mayisha@users.noreply.github.com> --- changelog.txt | 1 + client/blocks/express-checkout/hooks.js | 1 + client/entrypoints/express-checkout/index.js | 1 + .../__tests__/wc-order-attribution.test.js | 26 +++++++++++++++++++ .../compatibility/wc-order-attribution.js | 26 +++++++++++++++++++ .../utils/__tests__/normalize.test.js | 3 +++ client/express-checkout/utils/normalize.js | 6 ++++- readme.txt | 1 + 8 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 client/express-checkout/compatibility/__tests__/wc-order-attribution.test.js create mode 100644 client/express-checkout/compatibility/wc-order-attribution.js diff --git a/changelog.txt b/changelog.txt index 1e740243b3..1bea822925 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,7 @@ *** Changelog *** = 9.2.0 - xxxx-xx-xx = +* Fix - Fixes order attribution data for the Express Checkout Element when using the Blocks API to process. * Tweak - Process ECE orders using the Blocks API. * Fix - Fixes incorrect error message for card failures due insufficient funds on the shortcode checkout page (legacy). * Fix - Fixes deprecation warnings related to nullable method parameters when using PHP 8.4, and increases the minimum PHP version Code Sniffer considers to 7.4. diff --git a/client/blocks/express-checkout/hooks.js b/client/blocks/express-checkout/hooks.js index 7addae27d0..8a1271618c 100644 --- a/client/blocks/express-checkout/hooks.js +++ b/client/blocks/express-checkout/hooks.js @@ -16,6 +16,7 @@ import { getExpressCheckoutData, normalizeLineItems, } from 'wcstripe/express-checkout/utils'; +import 'wcstripe/express-checkout/compatibility/wc-order-attribution'; export const useExpressCheckout = ( { api, diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js index d78fa489b6..bdeb036a14 100644 --- a/client/entrypoints/express-checkout/index.js +++ b/client/entrypoints/express-checkout/index.js @@ -26,6 +26,7 @@ import { } from 'wcstripe/express-checkout/event-handler'; import { getStripeServerData } from 'wcstripe/stripe-utils'; import { getAddToCartVariationParams } from 'wcstripe/utils'; +import 'wcstripe/express-checkout/compatibility/wc-order-attribution'; import './styles.scss'; jQuery( function ( $ ) { diff --git a/client/express-checkout/compatibility/__tests__/wc-order-attribution.test.js b/client/express-checkout/compatibility/__tests__/wc-order-attribution.test.js new file mode 100644 index 0000000000..b52d0609a1 --- /dev/null +++ b/client/express-checkout/compatibility/__tests__/wc-order-attribution.test.js @@ -0,0 +1,26 @@ +import { applyFilters } from '@wordpress/hooks'; + +describe( 'ECE order attribution compatibility', () => { + it( 'filters out order attribution data', () => { + const orderAttributionData = applyFilters( + 'wcstripe.express-checkout.cart-place-order-extension-data', + { + session_count: '1', + session_pages: '79', + session_start_time: '2025-01-14 14:50:29', + source_type: 'utm', + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + utm_source: 'Facebook', + } + ); + + expect( orderAttributionData ).toStrictEqual( { + session_count: '1', + session_pages: '79', + session_start_time: '2025-01-14 14:50:29', + source_type: 'utm', + user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + utm_source: 'Facebook', + } ); + } ); +} ); diff --git a/client/express-checkout/compatibility/wc-order-attribution.js b/client/express-checkout/compatibility/wc-order-attribution.js new file mode 100644 index 0000000000..3aa795a6a6 --- /dev/null +++ b/client/express-checkout/compatibility/wc-order-attribution.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { extractOrderAttributionData } from 'wcstripe/blocks/utils'; + +addFilter( + 'wcstripe.express-checkout.cart-place-order-extension-data', + 'automattic/wcstripe/express-checkout', + ( extensionData ) => { + const orderAttributionData = {}; + for ( const [ name, value ] of Object.entries( + extractOrderAttributionData() + ) ) { + if ( name && value ) { + orderAttributionData[ + name.replace( 'wc_order_attribution_', '' ) + ] = value; + } + } + return { + ...extensionData, + 'woocommerce/order-attribution': orderAttributionData, + }; + } +); diff --git a/client/express-checkout/utils/__tests__/normalize.test.js b/client/express-checkout/utils/__tests__/normalize.test.js index c780cbf1f7..a5865e8cd3 100644 --- a/client/express-checkout/utils/__tests__/normalize.test.js +++ b/client/express-checkout/utils/__tests__/normalize.test.js @@ -399,6 +399,7 @@ describe( 'Express checkout normalization', () => { state: 'NY', }, customer_note: undefined, + extensions: {}, payment_data: [ { key: 'payment_method', @@ -457,6 +458,7 @@ describe( 'Express checkout normalization', () => { state: '', }, customer_note: undefined, + extensions: {}, payment_data: [ { key: 'payment_method', @@ -519,6 +521,7 @@ describe( 'Express checkout normalization', () => { state: '', }, customer_note: undefined, + extensions: {}, payment_data: [ { key: 'payment_method', diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index 852f61b015..a5d754de9c 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -1,3 +1,4 @@ +import { applyFilters } from '@wordpress/hooks'; import { extractOrderAttributionData } from 'wcstripe/blocks/utils'; /** @@ -132,7 +133,10 @@ export const normalizeOrderDataForBlocksAPI = ( event, paymentMethodId ) => { event?.expressPaymentType, paymentMethodId ), - ...extractOrderAttributionData(), + extensions: applyFilters( + 'wcstripe.express-checkout.cart-place-order-extension-data', + {} + ), }; }; diff --git a/readme.txt b/readme.txt index 8e6f600858..fd47cbb38e 100644 --- a/readme.txt +++ b/readme.txt @@ -111,6 +111,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o == Changelog == = 9.2.0 - xxxx-xx-xx = +* Fix - Fixes order attribution data for the Express Checkout Element when using the Blocks API to process. * Tweak - Process ECE orders using the Blocks API. * Fix - Fixes incorrect error message for card failures due insufficient funds on the shortcode checkout page (legacy). * Fix - Fixes deprecation warnings related to nullable method parameters when using PHP 8.4, and increases the minimum PHP version Code Sniffer considers to 7.4. From 69c9a9bad28e754b29353fe3fa8e6306e8d97e12 Mon Sep 17 00:00:00 2001 From: Malith Senaweera <6216000+malithsen@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:29:54 -0600 Subject: [PATCH 4/4] Fix error when discarding changes to the display order (#3698) * Fix error when discarding changes to the display order When changes to the payment method order list are discarded via the cancel button, it leads to an empty settings screen. This happens because it tries to find the Link payment method in the mapping list but fails and throw an error. We fix it by returning early if the Icon and label fields for a payment method are not found on the mapping. * Replicate the fix for draggable list * Restore PM order when cancelling the changes --- changelog.txt | 1 + .../general-settings-section/index.js | 25 ++++++++++++++++--- .../payment-methods-list.js | 14 +++++++++-- .../section-heading.js | 8 +----- readme.txt | 1 + 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1bea822925..3d4c2d57b1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,6 +51,7 @@ * Update - Migrate payment request settings data to express checkout settings data. * Update - Make the new Stripe Express Checkout Element enabled by default in all accounts. * Fix - Duplicate emails when enabling the gateway. +* Fix - Prevent empty settings screen when cancelling changes to the payment methods display order. = 9.0.0 - 2024-12-12 = * Fix - Fix 404 that happens when using ECE and 3D Secure auth is triggered. diff --git a/client/settings/general-settings-section/index.js b/client/settings/general-settings-section/index.js index 4b8fc4a867..5d9a4fcc2b 100644 --- a/client/settings/general-settings-section/index.js +++ b/client/settings/general-settings-section/index.js @@ -10,6 +10,7 @@ import SectionFooter from './section-footer'; import PaymentMethodsList from './payment-methods-list'; import UpeToggleContext from 'wcstripe/settings/upe-toggle/context'; import { useAccount } from 'wcstripe/data/account'; +import { useGetOrderedPaymentMethodIds } from 'wcstripe/data'; import './styles.scss'; const AccountRefreshingOverlay = styled.div` @@ -39,13 +40,30 @@ const GeneralSettingsSection = ( { ); const { isUpeEnabled, setIsUpeEnabled } = useContext( UpeToggleContext ); const { isRefreshing } = useAccount(); + const { + orderedPaymentMethodIds, + setOrderedPaymentMethodIds, + } = useGetOrderedPaymentMethodIds(); - const onChangeDisplayOrder = ( isChanging, data = null ) => { - setIsChangingDisplayOrder( isChanging ); + const [ initialOrder, setInitialOrder ] = useState( [] ); - if ( data ) { + const onChangeDisplayOrder = ( isChanging, data = null ) => { + if ( isChanging ) { + // Store the initial order before entering reorder mode + setInitialOrder( [ ...orderedPaymentMethodIds ] ); + } else if ( ! data ) { + // This is a cancel action - restore the initial order + if ( initialOrder.length > 0 ) { + setOrderedPaymentMethodIds( initialOrder ); + } + setInitialOrder( [] ); + } else { + // This is a save action onSaveChanges( 'ordered_payment_method_ids', data ); + setInitialOrder( [] ); } + + setIsChangingDisplayOrder( isChanging ); }; return ( @@ -78,6 +96,7 @@ const GeneralSettingsSection = ( { onChangeDisplayOrder( false ) } /> { isUpeEnabled && } diff --git a/client/settings/general-settings-section/payment-methods-list.js b/client/settings/general-settings-section/payment-methods-list.js index 2097691f7e..32b216b461 100644 --- a/client/settings/general-settings-section/payment-methods-list.js +++ b/client/settings/general-settings-section/payment-methods-list.js @@ -273,7 +273,12 @@ const GeneralSettingsSection = ( { Icon, label, allows_manual_capture: isAllowingManualCapture, - } = PaymentMethodsMap[ method ]; + } = PaymentMethodsMap[ method ] || {}; + + // Skip if there are no mapped fields for the payment method. + if ( ! Icon || ! label ) { + return null; + } return ( { const { isUpeEnabled } = useContext( UpeToggleContext ); - const upePaymentMethods = useGetAvailablePaymentMethodIds(); const { orderedPaymentMethodIds, - setOrderedPaymentMethodIds, isSaving, saveOrderedPaymentMethodIds, } = useGetOrderedPaymentMethodIds(); @@ -64,7 +59,6 @@ const SectionHeading = ( { isChangingDisplayOrder, onChangeDisplayOrder } ) => { const onChangeDisplayOrderCancel = () => { onChangeDisplayOrder( false ); - setOrderedPaymentMethodIds( upePaymentMethods ); }; const onChangeDisplayOrderSave = async () => { diff --git a/readme.txt b/readme.txt index fd47cbb38e..bf2fa5def6 100644 --- a/readme.txt +++ b/readme.txt @@ -118,5 +118,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Fix - Adds support for the Reunion country when checking out using the new checkout experience. * Add - Support zero-amount refunds. * Fix - A potential fix to prevent duplicate charges. +* Fix - Prevent empty settings screen when cancelling changes to the payment methods display order. [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).