Commit ed6d5ba2497 for woocommerce

commit ed6d5ba2497c3927445fdbabdb6e2a48708d431e
Author: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com>
Date:   Wed Jun 24 15:52:27 2026 +0300

    Fix custom place order button never submitting on shortcode checkout (#65933)

    Fix custom place order button never submitting order with shipping block present

    On the classic (shortcode) checkout, a gateway using a custom place order
    button never submitted the order when the cart needed shipping. The collapsed
    "Ship to a different address?" block renders hidden, empty required shipping
    fields, and the button's client-side validate() counted all .woocommerce-invalid
    fields regardless of visibility, so those hidden rows falsely blocked submission
    with no error notice.

    Gate submission (and the scroll-to-error target) on .woocommerce-invalid:visible,
    matching the :visible filter already used by the sibling required-field check.

    Fixes WOOPMNT-6250.

diff --git a/plugins/woocommerce/changelog/woopmnt-6250-custom-place-order-button b/plugins/woocommerce/changelog/woopmnt-6250-custom-place-order-button
new file mode 100644
index 00000000000..1ce7806c490
--- /dev/null
+++ b/plugins/woocommerce/changelog/woopmnt-6250-custom-place-order-button
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix custom place order button (shortcode checkout) never submitting the order when a shipping address block is present. Client-side validation now only counts visible invalid fields, so the collapsed "Ship to a different address?" shipping fields no longer block submission.
diff --git a/plugins/woocommerce/client/legacy/js/frontend/checkout.js b/plugins/woocommerce/client/legacy/js/frontend/checkout.js
index 4fdbe0b393e..8b54c351fca 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/checkout.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/checkout.js
@@ -43,8 +43,14 @@ jQuery( function ( $ ) {
 					// Trigger field-level validation (which adds `.woocommerce-invalid` to invalid fields)
 					$form.find( '.input-text, select, input:checkbox' ).trigger( 'validate' );

-					// Check for validation errors (from validate_field handler)
-					if ( $form.find( '.woocommerce-invalid' ).length > 0 ) {
+					// Check for validation errors (from validate_field handler).
+					// Only consider visible fields: `validate_field` flags any empty
+					// required field regardless of visibility, so hidden fields (e.g.
+					// the collapsed "Ship to a different address?" shipping fields)
+					// would otherwise block submission even when the visible form is
+					// valid. The `.woocommerce-invalid` class lives on the `.form-row`,
+					// which is hidden when its section is collapsed.
+					if ( $form.find( '.woocommerce-invalid:visible' ).length > 0 ) {
 						hasError = true;
 					}

@@ -79,7 +85,7 @@ jQuery( function ( $ ) {

 					// Scroll to the first invalid field in DOM order
 					if ( hasError ) {
-						var $firstInvalidField = $form.find( '.woocommerce-invalid' ).first();
+						var $firstInvalidField = $form.find( '.woocommerce-invalid:visible' ).first();
 						if ( $firstInvalidField.length ) {
 							$( 'html, body' ).animate(
 								{
diff --git a/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js b/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js
index a3e0a1c942f..f39a76a85de 100644
--- a/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js
+++ b/plugins/woocommerce/client/legacy/js/frontend/test/checkout-place-order-api.js
@@ -7,9 +7,17 @@ describe( 'createCheckoutPlaceOrderApi', () => {
 	let $termsCheckbox;
 	let $termsRow;
 	let capturedApi;
+	// Set the number of invalid `.form-row` elements that are hidden (e.g. the
+	// collapsed "Ship to a different address?" shipping fields). These must never
+	// block submission, so `validate()` should only count visible invalid fields.
+	let setHiddenInvalidCount;

 	beforeEach( () => {
 		capturedApi = null;
+		let hiddenInvalidCount = 0;
+		setHiddenInvalidCount = ( count ) => {
+			hiddenInvalidCount = count;
+		};

 		// used to track whether terms checkbox is checked
 		let termsChecked = false;
@@ -61,7 +69,9 @@ describe( 'createCheckoutPlaceOrderApi', () => {
 				if ( selector === '.input-text, select, input:checkbox' ) {
 					return { trigger: jest.fn() };
 				}
-				if ( selector === '.woocommerce-invalid' ) {
+				if ( selector === '.woocommerce-invalid:visible' ) {
+					// Visible invalid fields only (e.g. the terms row). Hidden
+					// invalid fields are deliberately excluded.
 					return {
 						length: formInvalidElements.size,
 						first: jest.fn( () => ( {
@@ -70,6 +80,19 @@ describe( 'createCheckoutPlaceOrderApi', () => {
 						} ) ),
 					};
 				}
+				if ( selector === '.woocommerce-invalid' ) {
+					// Unfiltered query (includes hidden fields). The implementation
+					// must NOT use this to gate submission; counting hidden invalid
+					// fields here is the regression these tests guard against.
+					const total = formInvalidElements.size + hiddenInvalidCount;
+					return {
+						length: total,
+						first: jest.fn( () => ( {
+							length: total > 0 ? 1 : 0,
+							offset: jest.fn( () => ( { top: 100 } ) ),
+						} ) ),
+					};
+				}
 				if ( selector === '.validate-required:visible' ) {
 					return { each: jest.fn() };
 				}
@@ -270,4 +293,32 @@ describe( 'createCheckoutPlaceOrderApi', () => {
 			expect( secondResult.hasError ).toBe( false );
 		} );
 	} );
+
+	describe( 'Hidden field validation', () => {
+		test( 'should ignore invalid fields that are hidden (e.g. collapsed shipping address)', async () => {
+			// A shippable cart renders the "Ship to a different address?" block,
+			// whose required shipping fields are present but hidden when the option
+			// is unchecked. Field-level validation flags them as invalid regardless
+			// of visibility, so validate() must only count *visible* invalid fields
+			// or the order is never submitted even when the visible form is valid.
+			$termsCheckbox.setChecked( true );
+			setHiddenInvalidCount( 5 );
+
+			const result = await capturedApi.validate();
+
+			expect( result.hasError ).toBe( false );
+		} );
+
+		test( 'should only count visible invalid fields', async () => {
+			$termsCheckbox.setChecked( true );
+			setHiddenInvalidCount( 5 );
+
+			await capturedApi.validate();
+
+			expect( $form.find ).toHaveBeenCalledWith(
+				'.woocommerce-invalid:visible'
+			);
+			expect( $form.find ).not.toHaveBeenCalledWith( '.woocommerce-invalid' );
+		} );
+	} );
 } );