Commit cf5f05c987b for woocommerce
commit cf5f05c987bc0987da147316617d917c1fb4c55b
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Mon Sep 28 15:34:33 2026 +0300
Charge shipping in the classic checkout when a hidden address field is required (#69139)
diff --git a/plugins/woocommerce/changelog/fix-show-shipping-hidden-address-fields b/plugins/woocommerce/changelog/fix-show-shipping-hidden-address-fields
new file mode 100644
index 00000000000..f1c9c971405
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-show-shipping-hidden-address-fields
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Charge shipping in the classic checkout when an extension hides a required state or postcode field.
diff --git a/plugins/woocommerce/includes/class-wc-cart.php b/plugins/woocommerce/includes/class-wc-cart.php
index 4e8ff09e292..2403b92af51 100644
--- a/plugins/woocommerce/includes/class-wc-cart.php
+++ b/plugins/woocommerce/includes/class-wc-cart.php
@@ -1889,7 +1889,7 @@ class WC_Cart extends WC_Legacy_Cart {
* @param bool $show_state Whether to use the state field. Default true.
*/
$state_enabled = apply_filters( 'woocommerce_shipping_calculator_enable_state', true );
- $state_required = isset( $country_fields['shipping_state'] ) && $country_fields['shipping_state']['required'];
+ $state_required = isset( $country_fields['shipping_state'] ) && $country_fields['shipping_state']['required'] && true !== ( $country_fields['shipping_state']['hidden'] ?? false );
// Takes care of late unsetting of checkout fields via hooks (woocommerce_checkout_fields, woocommerce_shipping_fields).
$checkout_state_field_exists = isset( $checkout_fields['shipping']['shipping_state'] );
if ( $state_enabled && $state_required && ! $this->get_customer()->get_shipping_state() && $checkout_state_field_exists ) {
@@ -1904,7 +1904,7 @@ class WC_Cart extends WC_Legacy_Cart {
* @param bool $show_postcode Whether to use the postcode field. Default true.
*/
$postcode_enabled = apply_filters( 'woocommerce_shipping_calculator_enable_postcode', true );
- $postcode_required = isset( $country_fields['shipping_postcode'] ) && $country_fields['shipping_postcode']['required'];
+ $postcode_required = isset( $country_fields['shipping_postcode'] ) && $country_fields['shipping_postcode']['required'] && true !== ( $country_fields['shipping_postcode']['hidden'] ?? false );
// Takes care of late unsetting of checkout fields via hooks (woocommerce_checkout_fields, woocommerce_shipping_fields).
$checkout_postcode_field_exists = isset( $checkout_fields['shipping']['shipping_postcode'] );
if ( $postcode_enabled && $postcode_required && '' === $this->get_customer()->get_shipping_postcode() && $checkout_postcode_field_exists ) {
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index d1b9a29a63e..ba02c2b8ea7 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -350,9 +350,7 @@ class WC_Install {
'11.1.0-1' => array(
'wc_update_11101_remove_deprecated_variation_gallery_option',
),
- '11.2.0-1' => array(
- // Run again to cancel all pattern fetch jobs now that scheduling is disabled.
- 'wc_update_1040_cleanup_legacy_ptk_patterns_fetching',
+ '11.2.0' => array(
'wc_update_1120_remove_abandoned_cart_recovery',
'wc_update_1120_migrate_stock_notifications_alpha_constant',
'wc_update_1120_delete_surface_cart_checkout_note',
@@ -362,6 +360,10 @@ class WC_Install {
'wc_update_11202_reset_refund_returning_customer_markers',
'wc_update_11203_normalize_stock_notification_emails',
),
+ '11.2.0-1' => array(
+ // Run again to cancel all pattern fetch jobs now that scheduling is disabled.
+ 'wc_update_1040_cleanup_legacy_ptk_patterns_fetching',
+ ),
'11.3.0' => array(
'wc_update_1130_set_legacy_variation_price_hash_option',
'wc_update_1130_delete_unpublished_variation_lookup_rows',
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
index d8f8c35fc24..82d2e772dc1 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-cart-test.php
@@ -21,6 +21,13 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
*/
protected $add_to_cart_quantity_filter_args = array();
+ /**
+ * Customer shipping address to restore after a test that changes it, keyed by prop name.
+ *
+ * @var array<string, string>|null
+ */
+ private $original_shipping_address = null;
+
/**
* Called before every test.
*/
@@ -39,6 +46,11 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
WC()->customer->set_is_vat_exempt( false );
WC()->session->set( 'wc_notices', null );
+ if ( null !== $this->original_shipping_address ) {
+ WC()->customer->set_props( $this->original_shipping_address );
+ $this->original_shipping_address = null;
+ }
+
// The parent teardown only clears chosen_shipping_methods, through
// WC_Shipping::reset_shipping(). Planted shipping_for_package_* rates survive and
// make later shipping calculations fail on a missing package hash, so clear them
@@ -968,6 +980,82 @@ class WC_Cart_Test extends \WC_Unit_Test_Case {
WC()->cart->cart_context = 'shortcode'; // Reset to default.
}
+ /**
+ * @testdox show_shipping() in the classic cart does not wait for an address field that the country locale hides.
+ *
+ * @testWith [ "postcode" ]
+ * [ "state" ]
+ *
+ * @param string $field Address field the locale hides while it stays required.
+ */
+ public function test_show_shipping_skips_an_address_field_the_locale_hides( string $field ): void {
+ $this->hide_us_address_field_in_locale( $field, true );
+ $this->add_product_for_a_us_address_missing( $field );
+
+ WC()->cart->calculate_totals();
+
+ $this->assertTrue( WC()->cart->show_shipping(), "A hidden {$field} should not hold back shipping." );
+ $this->assertTrue( WC()->cart->has_calculated_shipping(), "A hidden {$field} should let shipping be calculated." );
+ $this->assertGreaterThan( 0.0, (float) WC()->cart->get_shipping_total(), "A hidden {$field} should still charge the flat rate." );
+ }
+
+ /**
+ * @testdox show_shipping() in the classic cart still requires an address field whose hidden flag is not exactly true.
+ *
+ * @testWith [ "postcode", "yes" ]
+ * [ "postcode", 1 ]
+ * [ "state", "yes" ]
+ * [ "state", 1 ]
+ *
+ * @param string $field Address field the locale flags as hidden.
+ * @param mixed $hidden Hidden flag the locale sets on the field.
+ */
+ public function test_show_shipping_requires_a_field_whose_hidden_flag_is_not_exactly_true( string $field, $hidden ): void {
+ $this->hide_us_address_field_in_locale( $field, $hidden );
+ $this->add_product_for_a_us_address_missing( $field );
+
+ $this->assertFalse( WC()->cart->show_shipping(), "The classic checkout only treats hidden => true as hidden, so the {$field} stays required." );
+ }
+
+ /**
+ * Hide a US address field in the country locale while leaving it required.
+ *
+ * @param string $field Address field key, without the type prefix.
+ * @param mixed $hidden Value for the hidden flag.
+ */
+ private function hide_us_address_field_in_locale( string $field, $hidden ): void {
+ add_filter(
+ 'woocommerce_get_country_locale',
+ function ( $locale ) use ( $field, $hidden ) {
+ $locale['US'][ $field ]['hidden'] = $hidden;
+ return $locale;
+ }
+ );
+ WC()->countries->locale = array();
+ }
+
+ /**
+ * Add a product to the cart and give the customer a US shipping address without the given field, with shipping costs hidden until an address is entered.
+ *
+ * @param string $missing_field Address field to leave empty: 'postcode' or 'state'.
+ */
+ private function add_product_for_a_us_address_missing( string $missing_field ): void {
+ update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
+ $product = WC_Helper_Product::create_simple_product();
+ WC()->cart->add_to_cart( $product->get_id(), 1 );
+ $customer = WC()->cart->get_customer();
+ $this->original_shipping_address = array(
+ 'shipping_country' => $customer->get_shipping_country(),
+ 'shipping_state' => $customer->get_shipping_state(),
+ 'shipping_city' => $customer->get_shipping_city(),
+ 'shipping_postcode' => $customer->get_shipping_postcode(),
+ );
+ $customer->set_shipping_country( 'US' );
+ $customer->set_shipping_city( 'New York' );
+ $customer->set_shipping_state( 'state' === $missing_field ? '' : 'NY' );
+ $customer->set_shipping_postcode( 'postcode' === $missing_field ? '' : '10001' );
+ }
+
/**
* Test show_shipping for countries with various state/postcode requirement.
*/