Commit 881a5cd5a17 for woocommerce
commit 881a5cd5a175b8f80225fb9f0f4d8cf9ef65a006
Author: Seghir Nadir <nadir.seghir@gmail.com>
Date: Thu Oct 1 11:04:32 2026 +0100
Fix additional checkout field display across checkout types (#68780)
* Fix additional checkout field display across checkout types
Refs #68156.
* Add changelog for additional checkout field display
* Keep checkout defaults out of saved order fields
Refs #59792.
* Update changelog for saved order field defaults
* Make additional field defaults optional in the shared reader
Refs #59792 and #68156.
* Clarify changelog for saved additional checkout fields
* Make bulk field defaults optional while preserving checkout prefills
Refs #59792 and #68156.
* Revert bulk field defaults and Store API response changes
Reverts commit 506cee677db5e61089bd2858b5a1b7d0989527f2. Keep the fix limited to saved order field display. Refs #59792 and #68156.
diff --git a/plugins/woocommerce/changelog/fix-additional-checkout-field-order-values b/plugins/woocommerce/changelog/fix-additional-checkout-field-order-values
new file mode 100644
index 00000000000..c8ffa726ffc
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-additional-checkout-field-order-values
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Only show additional checkout fields with saved values, from any checkout flow.
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldTypes/CheckboxFieldType.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldTypes/CheckboxFieldType.php
index a543fbd4929..16ea566b8cd 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldTypes/CheckboxFieldType.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldTypes/CheckboxFieldType.php
@@ -50,13 +50,13 @@ class CheckboxFieldType extends AbstractFieldType {
}
/**
- * Converts the stored '1'/'0' meta value back to a boolean.
+ * Converts the stored '1'/'0' meta value back to a boolean, returning null for missing or empty values.
*
* @param mixed $value The stored value.
- * @return bool
+ * @return bool|null
*/
public function from_storage( $value ) {
- return '1' === $value;
+ return null === $value || '' === $value ? null : '1' === $value;
}
/**
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
index 7a9420d58ff..6f34341204f 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFields.php
@@ -923,7 +923,7 @@ class CheckoutFields {
}
/**
- * Get additional fields for an order.
+ * Get saved additional fields for an order without applying checkout defaults.
*
* @param WC_Order $order Order object.
* @param string $location The location to get fields for (address|contact|order).
@@ -933,21 +933,13 @@ class CheckoutFields {
*/
public function get_order_additional_fields_with_values( WC_Order $order, string $location, string $group = 'other', string $context = 'edit' ) {
- // Because the Additional Checkout Fields API only applies to orders created with Store API, we should not
- // return any values unless it was created using Store API. This is mainly to prevent "empty" checkbox values
- // from being shown on the order confirmation page for orders placed using the shortcode. It's rare that this
- // will happen but not impossible.
- if ( 'store-api' !== $order->get_created_via() ) {
- return [];
- }
-
$location = $this->prepare_location_name( $location );
$group = $this->prepare_group_name( $group );
$fields = $this->get_fields_for_location( $location );
$fields_with_values = [];
foreach ( $fields as $field_key => $field ) {
- $value = $this->get_field_from_object( $field_key, $order, $group );
+ $value = $this->get_field_from_object( $field_key, $order, $group, false );
if ( '' === $value || null === $value ) {
continue;
diff --git a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsStorage.php b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsStorage.php
index 9067cd94d94..ef650767071 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsStorage.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Services/CheckoutFieldsStorage.php
@@ -84,18 +84,21 @@ trait CheckoutFieldsStorage {
/**
* Returns a field value for a given object.
*
+ * @since 11.3.0 Added the $apply_defaults parameter.
+ *
* @param string $key The field key.
* @param WC_Customer|WC_Order $wc_object The customer or order to get the field value for.
* @param string $group The group to get the field value for (shipping|billing|other).
+ * @param bool $apply_defaults Whether to apply the default-value filter when no value is set.
*
* @return mixed The field value.
*/
- public function get_field_from_object( string $key, WC_Data $wc_object, string $group = 'other' ) {
+ public function get_field_from_object( string $key, WC_Data $wc_object, string $group = 'other', bool $apply_defaults = true ) {
$group = $this->prepare_group_name( $group );
$meta_key = self::get_group_key( $group ) . $key;
$value = $wc_object->get_meta( $meta_key, true );
- if ( ! $value && '0' !== $value ) {
+ if ( $apply_defaults && ! $value && '0' !== $value ) {
/**
* Allow providing a default value for additional fields if no value is already set.
*
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php
index 916ecf9a621..50a02117573 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsFrontendTest.php
@@ -380,7 +380,6 @@ class CheckoutFieldsFrontendTest extends \WC_Unit_Test_Case {
);
$order = \WC_Helper_Order::create_order();
- $order->set_created_via( 'store-api' );
$this->controller->persist_field_for_order( 'mynamespace/billing_confirmation_field', 'secret value', $order, 'billing', false );
$order->save();
@@ -404,7 +403,6 @@ class CheckoutFieldsFrontendTest extends \WC_Unit_Test_Case {
);
$order = \WC_Helper_Order::create_order();
- $order->set_created_via( 'store-api' );
$this->controller->persist_field_for_order( 'mynamespace/billing_confirmation_field', 'visible value', $order, 'billing', false );
$order->save();
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsTest.php b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsTest.php
index a09d2810109..3d3c962bc8b 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsTest.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Domain/Services/CheckoutFieldsTest.php
@@ -204,6 +204,115 @@ class CheckoutFieldsTest extends WP_UnitTestCase {
$this->assertArrayNotHasKey( 'namespace/vat-number', $fields );
}
+ /**
+ * @testdox Missing checkboxes stay hidden while saved answers are shown.
+ * @testWith [null, "", null]
+ * [false, false, "No"]
+ * [true, true, "Yes"]
+ *
+ * @param bool|null $saved_value Checkbox answer, or null for a missing value.
+ * @param bool|string $expected_value Expected raw value.
+ * @param string|null $expected_display Expected display value, or null for a hidden field.
+ */
+ public function test_order_checkbox_values( ?bool $saved_value, $expected_value, ?string $expected_display ): void {
+ $order = new \WC_Order();
+ $key = 'plugin-namespace/leave-on-porch';
+
+ if ( null !== $saved_value ) {
+ $this->controller->persist_field_for_order( $key, $saved_value, $order, 'other', false );
+ }
+
+ $this->assertSame( $expected_value, $this->controller->get_field_from_object( $key, $order ), 'Missing values must remain distinct from saved unchecked answers.' );
+
+ $fields = $this->controller->get_order_additional_fields_with_values( $order, 'order', 'other', 'view' );
+ if ( null === $expected_display ) {
+ $this->assertArrayNotHasKey( $key, $fields, 'A missing checkbox must not appear on the order.' );
+ } else {
+ $this->assertArrayHasKey( $key, $fields, 'A saved checkbox must appear on the order.' );
+ $this->assertSame( $expected_display, $fields[ $key ]['value'] );
+ }
+ }
+
+ /**
+ * @testdox Order fields use saved values and never apply checkout defaults.
+ * @testWith ["text", null, null, null]
+ * ["text", "", null, null]
+ * ["text", "Saved answer", "Saved answer", "Saved answer"]
+ * ["text", "0", "0", "0"]
+ * ["checkbox", null, null, null]
+ * ["checkbox", "", null, null]
+ * ["checkbox", "0", false, "No"]
+ * ["checkbox", "1", true, "Yes"]
+ *
+ * @param string $type Field type.
+ * @param string|null $saved_value Saved answer, or null for a missing value.
+ * @param bool|string|null $expected_edit Expected edit value, or null for an omitted field.
+ * @param string|null $expected_view Expected display value, or null for an omitted field.
+ */
+ public function test_order_fields_do_not_use_defaults( string $type, ?string $saved_value, $expected_edit, ?string $expected_view ): void {
+ $key = 'plugin-namespace/default-address-field';
+ woocommerce_register_additional_checkout_field(
+ array(
+ 'id' => $key,
+ 'label' => 'Default address field',
+ 'location' => 'address',
+ 'type' => $type,
+ )
+ );
+
+ $default_calls = 0;
+ add_filter(
+ "woocommerce_get_default_value_for_{$key}",
+ static function () use ( &$default_calls ) {
+ ++$default_calls;
+ return '1';
+ }
+ );
+
+ $order = new \WC_Order();
+ if ( null !== $saved_value ) {
+ $this->controller->persist_field_for_order( $key, $saved_value, $order, 'billing', false );
+ }
+
+ $this->assertSame( $expected_edit ?? '', $this->controller->get_field_from_object( $key, $order, 'billing', false ), 'The reader must preserve saved values when defaults are disabled.' );
+
+ $expected_values = array(
+ 'edit' => $expected_edit,
+ 'view' => $expected_view,
+ );
+ foreach ( $expected_values as $context => $expected ) {
+ $fields = $this->controller->get_order_additional_fields_with_values( $order, 'address', 'billing', $context );
+ if ( null === $expected ) {
+ $this->assertArrayNotHasKey( $key, $fields, 'Missing and empty answers must not be replaced by defaults.' );
+ } else {
+ $this->assertArrayHasKey( $key, $fields, 'Saved answers must remain available.' );
+ $this->assertSame( $expected, $fields[ $key ]['value'] );
+ }
+ }
+
+ $this->assertSame( 0, $default_calls, 'Reading order fields must not call the default-value filter.' );
+ }
+
+ /**
+ * @testdox Checkbox defaults still apply when no value is saved.
+ * @testWith ["0", false]
+ * ["1", true]
+ *
+ * @param string $default_value Filtered default value.
+ * @param bool $expected_value Expected checkbox value.
+ */
+ public function test_missing_checkbox_uses_filtered_default( string $default_value, bool $expected_value ): void {
+ $key = 'plugin-namespace/leave-on-porch';
+ add_filter(
+ "woocommerce_get_default_value_for_{$key}",
+ static function () use ( $default_value ) {
+ return $default_value;
+ }
+ );
+
+ $this->assertSame( $expected_value, $this->controller->get_field_from_object( $key, new \WC_Order() ), 'The default-value filter must still supply checkbox values.' );
+ }
+
/**
* @testdox Date fields can be registered, with their constraints stored as registered.
*/