diff --git a/changelog.txt b/changelog.txt index 744bc7265a..7292a1d2b1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,6 +2,8 @@ = 9.2.0 - xxxx-xx-xx = * Fix - Fixes a fatal error when editing the shortcode checkout page with an empty cart on PHP 8.4. +* Fix - Fixes processing of orders through the Pay for Order page when using ECE with Blocks (Store) API. +* Add - Enables the use of Blocks API for Express Checkout Element orders by default. * Add - Adds a new filter to allow changing the user attributed to an order when paying for it through the Order Pay page. * Fix - Fixes an error with the fingerprint property setting when using the legacy checkout. * Fix - Fixes order attribution data for the Express Checkout Element when using the Blocks API to process. @@ -12,6 +14,7 @@ * Add - Support zero-amount refunds. * Fix - A potential fix to prevent duplicate charges. * Fix - Improve product page caching when Express Payment buttons are not enabled. +* Fix - Allow editing uncaptured orders but show a warning about the possible failure scenario. * Fix - Error when changing subscription payment method to a 3D Secure card while using a custom checkout endpoint. = 9.1.1 - 2025-01-10 = diff --git a/client/api/index.js b/client/api/index.js index bbbc7a447c..883dcca698 100644 --- a/client/api/index.js +++ b/client/api/index.js @@ -504,34 +504,6 @@ export default class WCStripeAPI { * @return {Promise} Promise for the request to the server. */ expressCheckoutECECreateOrder( paymentData ) { - return this.request( getExpressCheckoutAjaxURL( 'create_order' ), { - _wpnonce: getExpressCheckoutData( 'nonce' )?.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. - */ - expressCheckoutECEPayForOrder( order, paymentData ) { - return this.request( getExpressCheckoutAjaxURL( 'pay_for_order' ), { - _wpnonce: getExpressCheckoutData( 'nonce' )?.pay_for_order, - order, - ...paymentData, - } ); - } - - /** - * 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 ), } ); @@ -544,7 +516,7 @@ export default class WCStripeAPI { * @param {Object} paymentData Order data. * @return {Promise} Promise for the request to the server. */ - expressCheckoutECEPayForOrderForBlocksAPI( order, paymentData ) { + expressCheckoutECEPayForOrder( order, paymentData ) { return this.postToBlocksAPI( `/wc/store/v1/checkout/${ order }`, { ...paymentData, } ); diff --git a/client/blocks/express-checkout/hooks.js b/client/blocks/express-checkout/hooks.js index 8a1271618c..3a8b950485 100644 --- a/client/blocks/express-checkout/hooks.js +++ b/client/blocks/express-checkout/hooks.js @@ -7,7 +7,6 @@ import { onClickHandler, onCompletePaymentHandler, onConfirmHandler, - onConfirmHandlerForBlocksAPI, } from 'wcstripe/express-checkout/event-handler'; import { displayExpressCheckoutNotice, @@ -118,16 +117,6 @@ export const useExpressCheckout = ( { ); const onConfirm = async ( event ) => { - if ( getExpressCheckoutData( 'use_blocks_api' ) ) { - return await onConfirmHandlerForBlocksAPI( - api, - stripe, - elements, - completePayment, - abortPayment, - event - ); - } return await onConfirmHandler( api, stripe, diff --git a/client/entrypoints/express-checkout/index.js b/client/entrypoints/express-checkout/index.js index bdeb036a14..dbaa227256 100644 --- a/client/entrypoints/express-checkout/index.js +++ b/client/entrypoints/express-checkout/index.js @@ -19,7 +19,6 @@ import { onClickHandler, onCompletePaymentHandler, onConfirmHandler, - onConfirmHandlerForBlocksAPI, onReadyHandler, shippingAddressChangeHandler, shippingRateChangeHandler, @@ -235,17 +234,6 @@ 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 180af81533..4c630f1571 100644 --- a/client/express-checkout/__tests__/event-handler.test.js +++ b/client/express-checkout/__tests__/event-handler.test.js @@ -5,13 +5,9 @@ import { normalizeLineItems, normalizeShippingAddress, normalizeOrderData, - normalizePayForOrderData, - normalizeOrderDataForBlocksAPI, - normalizePayForOrderDataForBlocksAPI, } from '../utils'; import { onConfirmHandler, - onConfirmHandlerForBlocksAPI, shippingAddressChangeHandler, shippingRateChangeHandler, } from 'wcstripe/express-checkout/event-handler'; @@ -324,381 +320,6 @@ describe( 'Express checkout event handlers', () => { paymentMethod: { id: 'pm_123' }, } ); api.expressCheckoutECECreateOrder.mockResolvedValue( { - result: 'error', - messages: 'Order creation error', - } ); - - await onConfirmHandler( - api, - stripe, - elements, - completePayment, - abortPayment, - event - ); - - const expectedOrderData = normalizeOrderData( event, 'pm_123' ); - expect( api.expressCheckoutECECreateOrder ).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.expressCheckoutECECreateOrder.mockResolvedValue( { - result: 'success', - redirect: 'https://example.com/redirect', - } ); - api.confirmIntent.mockReturnValue( true ); - - await onConfirmHandler( - 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.expressCheckoutECECreateOrder.mockResolvedValue( { - result: 'success', - redirect: 'https://example.com/redirect', - } ); - api.confirmIntent.mockReturnValue( { - request: Promise.resolve( - 'https://example.com/confirmation_redirect' - ), - isOrderPage: false, - } ); - - await onConfirmHandler( - 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.expressCheckoutECECreateOrder.mockResolvedValue( { - result: 'success', - redirect: 'https://example.com/redirect', - } ); - api.confirmIntent.mockReturnValue( { - request: Promise.reject( - new Error( 'Intent confirmation error' ) - ), - isOrderPage: false, - } ); - - await onConfirmHandler( - api, - stripe, - elements, - completePayment, - abortPayment, - event - ); - - expect( api.confirmIntent ).toHaveBeenCalledWith( - 'https://example.com/redirect' - ); - expect( abortPayment ).toHaveBeenCalledWith( - event, - 'Intent confirmation error' - ); - expect( completePayment ).not.toHaveBeenCalled(); - } ); - - test( 'should abort payment if expressCheckoutECEPayForOrder fails', async () => { - elements.submit.mockResolvedValue( {} ); - stripe.createPaymentMethod.mockResolvedValue( { - paymentMethod: { id: 'pm_123' }, - } ); - api.expressCheckoutECEPayForOrder.mockResolvedValue( { - result: 'error', - messages: 'Order creation error', - } ); - - await onConfirmHandler( - api, - stripe, - elements, - completePayment, - abortPayment, - event, - order - ); - - const expectedOrderData = normalizePayForOrderData( - event, - 'pm_123' - ); - expect( api.expressCheckoutECEPayForOrder ).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.expressCheckoutECEPayForOrder.mockResolvedValue( { - result: 'success', - redirect: 'https://example.com/redirect', - } ); - api.confirmIntent.mockReturnValue( true ); - - await onConfirmHandler( - 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.expressCheckoutECEPayForOrder.mockResolvedValue( { - result: 'success', - redirect: 'https://example.com/redirect', - } ); - api.confirmIntent.mockReturnValue( { - request: Promise.resolve( - 'https://example.com/confirmation_redirect' - ), - isOrderPage: false, - } ); - - await onConfirmHandler( - 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.expressCheckoutECEPayForOrder.mockResolvedValue( { - result: 'success', - redirect: 'https://example.com/redirect', - } ); - api.confirmIntent.mockReturnValue( { - request: Promise.reject( - new Error( 'Intent confirmation error' ) - ), - isOrderPage: false, - } ); - - await onConfirmHandler( - api, - stripe, - elements, - completePayment, - abortPayment, - event, - order - ); - - expect( api.confirmIntent ).toHaveBeenCalledWith( - 'https://example.com/redirect' - ); - expect( abortPayment ).toHaveBeenCalledWith( - event, - 'Intent confirmation error' - ); - 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: [ @@ -710,7 +331,7 @@ describe( 'Express checkout event handlers', () => { }, } ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -719,13 +340,10 @@ describe( 'Express checkout event handlers', () => { event ); - const expectedOrderData = normalizeOrderDataForBlocksAPI( - event, - 'pm_123' + const expectedOrderData = normalizeOrderData( event, 'pm_123' ); + expect( api.expressCheckoutECECreateOrder ).toHaveBeenCalledWith( + expectedOrderData ); - expect( - api.expressCheckoutECECreateOrderForBlocksAPI - ).toHaveBeenCalledWith( expectedOrderData ); expect( abortPayment ).toHaveBeenCalledWith( event, 'Order creation error', @@ -739,7 +357,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECECreateOrder.mockResolvedValue( { payment_result: { payment_status: 'success', redirect_url: 'https://example.com/redirect', @@ -747,7 +365,7 @@ describe( 'Express checkout event handlers', () => { } ); api.confirmIntent.mockReturnValue( true ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -770,7 +388,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECECreateOrder.mockResolvedValue( { payment_result: { payment_status: 'success', redirect_url: 'https://example.com/redirect', @@ -782,7 +400,7 @@ describe( 'Express checkout event handlers', () => { ), } ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -805,7 +423,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECECreateOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECECreateOrder.mockResolvedValue( { payment_result: { payment_status: 'success', redirect_url: 'https://example.com/redirect', @@ -817,7 +435,7 @@ describe( 'Express checkout event handlers', () => { ), } ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -842,7 +460,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECEPayForOrder.mockResolvedValue( { payment_result: { payment_status: 'error', payment_details: [ @@ -854,7 +472,7 @@ describe( 'Express checkout event handlers', () => { }, } ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -864,13 +482,11 @@ describe( 'Express checkout event handlers', () => { order ); - const expectedOrderData = normalizePayForOrderDataForBlocksAPI( - event, - 'pm_123' + const expectedOrderData = normalizeOrderData( event, 'pm_123' ); + expect( api.expressCheckoutECEPayForOrder ).toHaveBeenCalledWith( + 123, + expectedOrderData ); - expect( - api.expressCheckoutECEPayForOrderForBlocksAPI - ).toHaveBeenCalledWith( 123, expectedOrderData ); expect( abortPayment ).toHaveBeenCalledWith( event, 'Order creation error', @@ -884,7 +500,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECEPayForOrder.mockResolvedValue( { payment_result: { payment_status: 'success', redirect_url: 'https://example.com/redirect', @@ -892,7 +508,7 @@ describe( 'Express checkout event handlers', () => { } ); api.confirmIntent.mockReturnValue( true ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -916,7 +532,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECEPayForOrder.mockResolvedValue( { payment_result: { payment_status: 'success', redirect_url: 'https://example.com/redirect', @@ -928,7 +544,7 @@ describe( 'Express checkout event handlers', () => { ), } ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, @@ -952,7 +568,7 @@ describe( 'Express checkout event handlers', () => { stripe.createPaymentMethod.mockResolvedValue( { paymentMethod: { id: 'pm_123' }, } ); - api.expressCheckoutECEPayForOrderForBlocksAPI.mockResolvedValue( { + api.expressCheckoutECEPayForOrder.mockResolvedValue( { payment_result: { payment_status: 'success', redirect_url: 'https://example.com/redirect', @@ -964,7 +580,7 @@ describe( 'Express checkout event handlers', () => { ), } ); - await onConfirmHandlerForBlocksAPI( + await onConfirmHandler( api, stripe, elements, diff --git a/client/express-checkout/event-handler.js b/client/express-checkout/event-handler.js index 21a854156f..a5745cf475 100644 --- a/client/express-checkout/event-handler.js +++ b/client/express-checkout/event-handler.js @@ -2,12 +2,9 @@ import { __ } from '@wordpress/i18n'; import { getErrorMessageFromNotice, normalizeOrderData, - normalizePayForOrderData, normalizeShippingAddress, normalizeLineItems, getExpressCheckoutData, - normalizeOrderDataForBlocksAPI, - normalizePayForOrderDataForBlocksAPI, } from './utils'; import { trackExpressCheckoutButtonClick, @@ -87,80 +84,11 @@ export const onConfirmHandler = async ( } else { orderResponse = await api.expressCheckoutECEPayForOrder( order, - normalizePayForOrderData( event, paymentMethod.id ) - ); - } - - if ( orderResponse.result !== 'success' ) { - return abortPayment( - event, - getErrorMessageFromNotice( orderResponse.messages ), - true - ); - } - - const confirmationRequest = api.confirmIntent( orderResponse.redirect ); - - // `true` means there is no intent to confirm. - if ( confirmationRequest === true ) { - completePayment( orderResponse.redirect ); - } else { - const { request } = confirmationRequest; - const redirectUrl = await request; - - completePayment( redirectUrl ); - } - } catch ( e ) { - let errorMessage; - if ( e.message ) { - errorMessage = e.message; - } else { - errorMessage = __( - 'There was a problem processing the order.', - 'woocommerce-gateway-stripe' - ); - } - return abortPayment( event, errorMessage ); - } -}; - -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 ) + normalizeOrderData( event, paymentMethod.id ) ); } - if ( orderResponse.payment_result.payment_status !== 'success' ) { + if ( orderResponse.payment_result?.payment_status !== 'success' ) { return abortPayment( event, getErrorMessageFromNotice( diff --git a/client/express-checkout/utils/__tests__/normalize.test.js b/client/express-checkout/utils/__tests__/normalize.test.js index a5865e8cd3..8489a9ad9c 100644 --- a/client/express-checkout/utils/__tests__/normalize.test.js +++ b/client/express-checkout/utils/__tests__/normalize.test.js @@ -4,9 +4,6 @@ import { normalizeLineItems, normalizeOrderData, - normalizeOrderDataForBlocksAPI, - normalizePayForOrderData, - normalizePayForOrderDataForBlocksAPI, normalizeShippingAddress, } from '../normalize'; @@ -133,223 +130,6 @@ describe( 'Express checkout normalization', () => { } ); describe( 'normalizeOrderData', () => { - 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_first_name: 'John', - billing_last_name: 'Doe', - billing_company: 'Some Company', - billing_email: 'john.doe@example.com', - billing_phone: '1234567890', - billing_country: 'US', - billing_address_1: '123 Main St', - billing_address_2: 'Apt 4B', - billing_city: 'New York', - billing_state: 'NY', - billing_postcode: '10001', - shipping_first_name: 'John', - shipping_last_name: 'Doe', - shipping_company: 'Some Company', - shipping_phone: '1234567890', - shipping_country: 'US', - shipping_address_1: '123 Main St', - shipping_address_2: 'Apt 4B', - shipping_city: 'New York', - shipping_state: 'NY', - shipping_postcode: '10001', - shipping_method: [ 'rate_1' ], - order_comments: '', - payment_method: 'stripe', - ship_to_different_address: 1, - terms: 1, - 'wc-stripe-is-deferred-intent': true, - 'wc-stripe-payment-method': paymentMethodId, - express_checkout_type: 'express', - express_payment_type: 'express', - }; - - expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( - expectedNormalizedData - ); - } ); - - test( 'should normalize order data with missing optional event fields', () => { - const event = {}; - const paymentMethodId = 'pm_123456'; - - const expectedNormalizedData = { - billing_first_name: '', - billing_last_name: '-', - billing_company: '', - billing_email: '', - billing_phone: '', - billing_country: '', - billing_address_1: '', - billing_address_2: '', - billing_city: '', - billing_state: '', - billing_postcode: '', - shipping_first_name: '', - shipping_last_name: '', - shipping_company: '', - shipping_phone: '', - shipping_country: '', - shipping_address_1: '', - shipping_address_2: '', - shipping_city: '', - shipping_state: '', - shipping_postcode: '', - shipping_method: [ null ], - order_comments: '', - payment_method: 'stripe', - ship_to_different_address: 1, - terms: 1, - 'wc-stripe-is-deferred-intent': true, - 'wc-stripe-payment-method': paymentMethodId, - express_payment_type: undefined, - }; - - expect( normalizeOrderData( 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_first_name: 'John', - billing_last_name: '', - billing_company: '', - billing_email: '', - billing_phone: '', - billing_country: '', - billing_address_1: '', - billing_address_2: '', - billing_city: '', - billing_state: '', - express_checkout_type: undefined, - express_payment_type: undefined, - billing_postcode: '', - shipping_first_name: '', - shipping_last_name: '', - shipping_company: '', - shipping_phone: '', - shipping_country: '', - shipping_address_1: '', - shipping_address_2: '', - shipping_city: '', - shipping_state: '', - shipping_postcode: '', - shipping_method: [ null ], - order_comments: '', - payment_method: 'stripe', - ship_to_different_address: 1, - terms: 1, - 'wc-stripe-is-deferred-intent': true, - 'wc-stripe-payment-method': paymentMethodId, - }; - - expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( - expectedNormalizedData - ); - } ); - } ); - - describe( 'normalizePayForOrderData', () => { - 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( normalizePayForOrderData( event, 'pm_123456' ) ).toEqual( { - payment_method: 'stripe', - 'wc-stripe-is-deferred-intent': true, - 'wc-stripe-payment-method': 'pm_123456', - express_payment_type: 'express', - } ); - } ); - - test( 'should normalize pay for order data with empty event and empty payment method', () => { - const event = {}; - const paymentMethodId = ''; - - expect( - normalizePayForOrderData( event, paymentMethodId ) - ).toEqual( { - payment_method: 'stripe', - 'wc-stripe-is-deferred-intent': true, - 'wc-stripe-payment-method': '', - express_payment_type: undefined, - } ); - } ); - } ); - - describe( 'normalizeOrderDataForBlocksAPI', () => { test( 'should normalize order data with complete event and paymentMethodId', () => { const event = { billingDetails: { @@ -434,9 +214,9 @@ describe( 'Express checkout normalization', () => { }, }; - expect( - normalizeOrderDataForBlocksAPI( event, paymentMethodId ) - ).toEqual( expectedNormalizedData ); + expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( + expectedNormalizedData + ); } ); test( 'should normalize order data with missing optional event fields', () => { @@ -493,9 +273,9 @@ describe( 'Express checkout normalization', () => { }, }; - expect( - normalizeOrderDataForBlocksAPI( event, paymentMethodId ) - ).toEqual( expectedNormalizedData ); + expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( + expectedNormalizedData + ); } ); test( 'should normalize order data with minimum required fields', () => { @@ -556,97 +336,9 @@ describe( 'Express checkout normalization', () => { }, }; - 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', - } ); + expect( normalizeOrderData( event, paymentMethodId ) ).toEqual( + expectedNormalizedData + ); } ); } ); diff --git a/client/express-checkout/utils/normalize.js b/client/express-checkout/utils/normalize.js index a5d754de9c..84b3f312ef 100644 --- a/client/express-checkout/utils/normalize.js +++ b/client/express-checkout/utils/normalize.js @@ -1,5 +1,4 @@ import { applyFilters } from '@wordpress/hooks'; -import { extractOrderAttributionData } from 'wcstripe/blocks/utils'; /** * Normalizes incoming cart total items for use as a displayItems with the Stripe api. @@ -22,63 +21,6 @@ export const normalizeLineItems = ( displayItems ) => { } ); }; -/** - * Normalize order data from Stripe's object to the expected format for WC. - * - * @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 normalizeOrderData = ( 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_first_name: - name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '', - billing_last_name: name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '-', - billing_company: billing?.organization ?? '', - billing_email: email ?? event?.payerEmail ?? '', - billing_phone: phone, - billing_country: billing?.country ?? '', - billing_address_1: billing?.line1 ?? '', - billing_address_2: billing?.line2 ?? '', - billing_city: billing?.city ?? '', - billing_state: billing?.state ?? '', - billing_postcode: billing?.postal_code ?? '', - shipping_first_name: - shipping?.name?.split( ' ' )?.slice( 0, 1 )?.join( ' ' ) ?? '', - shipping_last_name: - shipping?.name?.split( ' ' )?.slice( 1 )?.join( ' ' ) ?? '', - shipping_company: shipping?.organization ?? '', - shipping_phone: phone, - shipping_country: shipping?.address?.country ?? '', - shipping_address_1: shipping?.address?.line1 ?? '', - shipping_address_2: shipping?.address?.line2 ?? '', - shipping_city: shipping?.address?.city ?? '', - shipping_state: shipping?.address?.state ?? '', - shipping_postcode: shipping?.address?.postal_code ?? '', - shipping_method: [ event?.shippingRate?.id ?? null ], - order_comments: '', - payment_method: 'stripe', - ship_to_different_address: 1, - terms: 1, - 'wc-stripe-payment-method': paymentMethodId, - express_checkout_type: event?.expressPaymentType, - express_payment_type: event?.expressPaymentType, - 'wc-stripe-is-deferred-intent': true, - ...extractOrderAttributionData(), - }; -}; - /** * Normalize order data from Stripe's object to the expected format for WC (when using the Blocks API). * @@ -87,7 +29,7 @@ export const normalizeOrderData = ( event, paymentMethodId ) => { * * @return {Object} Order object in the format WooCommerce expects. */ -export const normalizeOrderDataForBlocksAPI = ( event, paymentMethodId ) => { +export const normalizeOrderData = ( event, paymentMethodId ) => { const name = event?.billingDetails?.name; const email = event?.billingDetails?.email ?? ''; const billing = event?.billingDetails?.address ?? {}; @@ -140,44 +82,6 @@ export const normalizeOrderDataForBlocksAPI = ( event, paymentMethodId ) => { }; }; -/** - * Normalize Pay for Order data from Stripe's object to the expected format for WC. - * - * @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 normalizePayForOrderData = ( event, paymentMethodId ) => { - return { - payment_method: 'stripe', - 'wc-stripe-is-deferred-intent': true, // Set the deferred intent flag, so the deferred intent flow is used. - 'wc-stripe-payment-method': paymentMethodId, - express_payment_type: event?.expressPaymentType, - }; -}; - -/** - * 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. diff --git a/client/settings/account-details/index.js b/client/settings/account-details/index.js index d77cc684e3..510834e3d4 100644 --- a/client/settings/account-details/index.js +++ b/client/settings/account-details/index.js @@ -37,8 +37,7 @@ const Label = styled.p` `; const AccountDetailsError = styled.p` - @import '../../styles/abstracts/colors'; - color: $alert-red; + color: #d63638; `; const useIsCardPaymentsEnabled = () => { diff --git a/includes/class-wc-stripe-order-handler.php b/includes/class-wc-stripe-order-handler.php index 5f2cc6bbd2..5d1e05eddc 100644 --- a/includes/class-wc-stripe-order-handler.php +++ b/includes/class-wc-stripe-order-handler.php @@ -29,7 +29,7 @@ public function __construct() { add_action( 'woocommerce_cancel_unpaid_order', [ $this, 'prevent_cancelling_orders_awaiting_action' ], 10, 2 ); - add_filter( 'wc_order_is_editable', [ $this, 'disable_edit_for_uncaptured_orders' ], 10, 2 ); + add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'show_warning_for_uncaptured_orders' ] ); } /** @@ -43,77 +43,27 @@ public static function get_instance() { } /** - * Disables the ability to edit for orders with uncaptured payment. + * Shows a warning message about editing uncaptured orders. * - * @param $editable boolean The current editability of the order. - * @param $order WC_Order The order object. - * @return boolean false if the order has uncaptured payment, true otherwise. + * @param $order_id */ - public function disable_edit_for_uncaptured_orders( $editable, $order ) { + public function show_warning_for_uncaptured_orders( $order_id ) { + $order = wc_get_order( $order_id ); // Bail if payment method is not manual capture supporting stripe method. if ( ! WC_Stripe_Helper::payment_method_allows_manual_capture( $order->get_payment_method() ) ) { - return $editable; + return; } try { $intent = $this->get_intent_from_order( $order ); - - if ( $intent && 'requires_capture' === $intent->status ) { - $editable = false; - - // add hooks to change text about the reason when order items cannot be edited - add_action( 'woocommerce_admin_order_totals_after_total', [ $this, 'maybe_attach_gettext_callback' ] ); - add_action( 'woocommerce_order_item_add_action_buttons', [ $this, 'maybe_unattach_gettext_callback' ] ); + if ( $intent && WC_Stripe_Intent_Status::REQUIRES_CAPTURE === $intent->status ) { + $capture_notice = __( 'Attempting to capture more than the authorized amount will fail with an error.', 'woocommerce-gateway-stripe' ); + $capture_tooltip = __( 'You may edit the order to have a total less than or equal to the original authorized amount.', 'woocommerce-gateway-stripe' ); + echo esc_html( $capture_notice ) . wc_help_tip( $capture_tooltip ); } } catch ( Exception $e ) { WC_Stripe_Logger::log( 'Error getting intent from order: ' . $e->getMessage() ); } - - return $editable; - } - - /** - * Only attach the gettext callback when on admin edit order screen. - */ - public function maybe_attach_gettext_callback() { - - if ( is_admin() && function_exists( 'get_current_screen' ) ) { - $screen = get_current_screen(); - - if ( is_object( $screen ) && in_array( $screen->id, [ 'woocommerce_page_wc-orders', 'edit-shop_order' ], true ) ) { - // Hook to gettext callback to change the tooltip text - add_filter( 'gettext', [ $this, 'change_order_item_editable_text_tooltip' ], 10, 3 ); - } - } - } - - /** - * Unattach the gettext callback. - */ - public function maybe_unattach_gettext_callback() { - - if ( is_admin() && function_exists( 'get_current_screen' ) ) { - $screen = get_current_screen(); - - if ( is_object( $screen ) && in_array( $screen->id, [ 'woocommerce_page_wc-orders', 'edit-shop_order' ], true ) ) { - // Unhook gettext callback to prevent extra call impact - remove_filter( 'gettext', [ $this, 'change_order_item_editable_text_tooltip' ], 10 ); - } - } - } - - /** - * When order items are not editable due to the charge being authorized to capture the current amount, - * change the tooltip to explain the reason. - */ - public function change_order_item_editable_text_tooltip( $translated_text, $text, $domain ) { - switch ( $text ) { - case 'To edit this order change the status back to "Pending payment"': - $translated_text = __( 'This order is no longer editable because the charge has been authorized for this amount.', 'woocommerce-gateway-stripe' ); - break; - } - - return $translated_text; } /** diff --git a/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php b/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php index 1a6c4e0f17..20e1890984 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-ajax-handler.php @@ -299,8 +299,11 @@ public function ajax_get_selected_product_data() { /** * Create order. Security is handled by WC. + * + * @deprecated 9.2.0 Payment is processed using the Blocks API by default. */ public function ajax_create_order() { + _deprecated_function( __METHOD__, '9.2.0' ); try { if ( WC()->cart->is_empty() ) { wp_send_json_error( __( 'Empty cart', 'woocommerce-gateway-stripe' ) ); @@ -347,8 +350,11 @@ public function ajax_log_errors() { /** * Processes the Pay for Order AJAX request from the Express Checkout. + * + * @deprecated 9.2.0 Payment is processed using the Blocks API by default. */ public function ajax_pay_for_order() { + _deprecated_function( __METHOD__, '9.2.0' ); check_ajax_referer( 'wc-stripe-pay-for-order' ); if ( 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 4d34c75840..b7dd0491b5 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-element.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-element.php @@ -219,7 +219,6 @@ 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 dcc4e50411..9d77a44f60 100644 --- a/includes/payment-methods/class-wc-stripe-express-checkout-helper.php +++ b/includes/payment-methods/class-wc-stripe-express-checkout-helper.php @@ -1358,8 +1358,11 @@ public function is_express_checkout_enabled() { * Returns whether Stripe express checkout element should use the Blocks API. * * @return boolean + * + * @deprecated 9.2.0 Feature flag enable by default. */ public function use_blocks_api() { + _deprecated_function( __METHOD__, '9.2.0' ); return isset( $this->stripe_settings['express_checkout_use_blocks_api'] ) && 'yes' === $this->stripe_settings['express_checkout_use_blocks_api']; } diff --git a/readme.txt b/readme.txt index b95d7fb996..d7f635c6c7 100644 --- a/readme.txt +++ b/readme.txt @@ -112,6 +112,8 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o = 9.2.0 - xxxx-xx-xx = * Fix - Fixes a fatal error when editing the shortcode checkout page with an empty cart on PHP 8.4. +* Fix - Fixes processing of orders through the Pay for Order page when using ECE with Blocks (Store) API. +* Add - Enables the use of Blocks API for Express Checkout Element orders by default. * Add - Adds a new filter to allow changing the user attributed to an order when paying for it through the Order Pay page. * Fix - Fixes an error with the fingerprint property setting when using the legacy checkout. * Fix - Fixes order attribution data for the Express Checkout Element when using the Blocks API to process. @@ -123,6 +125,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Fix - A potential fix to prevent duplicate charges. * Fix - Prevent empty settings screen when cancelling changes to the payment methods display order. * Fix - Improve product page caching when Express Payment buttons are not enabled. +* Fix - Allow editing uncaptured orders but show a warning about the possible failure scenario. * Fix - Error when changing subscription payment method to a 3D Secure card while using a custom checkout endpoint. [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). diff --git a/tests/phpunit/test-class-wc-stripe-order-handler.php b/tests/phpunit/test-class-wc-stripe-order-handler.php index a156251afe..53eea2e93a 100644 --- a/tests/phpunit/test-class-wc-stripe-order-handler.php +++ b/tests/phpunit/test-class-wc-stripe-order-handler.php @@ -50,40 +50,4 @@ public function test_prevent_cancelling_orders_awaiting_action() { $this->assertTrue( $this->order_handler->prevent_cancelling_orders_awaiting_action( true, $order ) ); } - - /** - * Test for disable_edit_for_uncaptured_orders(). - */ - public function test_disable_edit_for_uncaptured_orders() { - $order = WC_Helper_Order::create_order(); - $order->set_payment_method( 'bacs' ); - $order->save(); - - // Test when payment method is not stripe. - $this->assertTrue( $this->order_handler->disable_edit_for_uncaptured_orders( true, $order ) ); - $this->assertFalse( $this->order_handler->disable_edit_for_uncaptured_orders( false, $order ) ); - - $order->set_payment_method( 'stripe' ); - $order->save(); - - $this->order_handler - ->expects( $this->any() ) - ->method( 'get_intent_from_order' ) - ->willReturnOnConsecutiveCalls( - (object) [ - 'intent_id' => 'pi_mock1', - 'status' => 'succeeded', - ], - (object) [ - 'intent_id' => 'pi_mock2', - 'status' => 'requires_capture', - ] - ); - - // Test when intent is succeeded. - $this->assertTrue( $this->order_handler->disable_edit_for_uncaptured_orders( true, $order ) ); - - // Test when intent is requires_capture. - $this->assertFalse( $this->order_handler->disable_edit_for_uncaptured_orders( true, $order ) ); - } } diff --git a/tests/phpunit/test-wc-stripe-express-checkout-helper.php b/tests/phpunit/test-wc-stripe-express-checkout-helper.php index 47f9a86902..44f23ef9ae 100644 --- a/tests/phpunit/test-wc-stripe-express-checkout-helper.php +++ b/tests/phpunit/test-wc-stripe-express-checkout-helper.php @@ -21,7 +21,6 @@ public function set_up() { $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 ); } @@ -312,14 +311,4 @@ 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() ); - } }