Commit 295dc0b2bf6 for woocommerce

commit 295dc0b2bf6ed9d3d893e84acff7af299ab75b99
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date:   Mon Jun 8 18:04:39 2026 +0100

    Surface settings UI fallback through wc_doing_it_wrong (#65499)

    * dev: surface settings ui fallback notices

    * Fix settings UI fallback notice version

    * Revert "Fix settings UI fallback notice version"

    This reverts commit 73fa8de992822137ded2a1bee0d6483fc8a3e560.

    * Polish settings UI fallback handling

    * Log settings UI script handle errors

    * Log settings UI fallback debug details

diff --git a/plugins/woocommerce/changelog/fix-settings-ui-fallback-doing-it-wrong b/plugins/woocommerce/changelog/fix-settings-ui-fallback-doing-it-wrong
new file mode 100644
index 00000000000..6fa780347ac
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-settings-ui-fallback-doing-it-wrong
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Surface Settings UI fallback through wc_doing_it_wrong.
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
index 82b308f8690..cc847268411 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-page.php
@@ -148,6 +148,29 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 			return "$classes woocommerce-settings-ui-page";
 		}

+		/**
+		 * Log a developer-facing notice when settings UI rendering falls back to the legacy renderer.
+		 *
+		 * @since 10.9.0
+		 *
+		 * @param SettingsUIPageInterface $settings_ui_page Settings UI page adapter.
+		 * @param string                  $section_id Section id.
+		 * @param string                  $reason Fallback reason.
+		 */
+		private function log_settings_ui_fallback( SettingsUIPageInterface $settings_ui_page, string $section_id, string $reason ): void {
+			wc_doing_it_wrong(
+				'WC_Settings_Page::output',
+				sprintf(
+					/* translators: 1: settings page id, 2: settings section id, 3: fallback reason. */
+					__( 'Settings UI rendering for page "%1$s" section "%2$s" fell back to the legacy settings renderer. Reason: %3$s', 'woocommerce' ),
+					$settings_ui_page->get_page_id(),
+					'' === $section_id ? 'default' : $section_id,
+					$reason
+				),
+				'10.9.0'
+			);
+		}
+
 		/**
 		 * Creates the React mount point for settings slot.
 		 */
@@ -313,41 +336,68 @@ if ( ! class_exists( 'WC_Settings_Page', false ) ) :
 			$page_id          = $settings_ui_page instanceof SettingsUIPageInterface ? $settings_ui_page->get_page_id() : '';
 			$schema_failed    = ! empty( $GLOBALS['wc_settings_ui_schema_failed'][ $page_id ][ $section_key ] );

-			if ( Features::is_enabled( 'settings-ui' ) && $settings_ui_page instanceof SettingsUIPageInterface && ! $schema_failed ) {
-				$render_settings_ui = true;
-
-				try {
-					$script_handles = $settings_ui_page->get_script_handles( $current_section );
-				} catch ( \Throwable $e ) {
-					$script_handles     = array();
-					$render_settings_ui = false;
+			if ( Features::is_enabled( 'settings-ui' ) && $settings_ui_page instanceof SettingsUIPageInterface ) {
+				if ( $schema_failed ) {
+					$this->log_settings_ui_fallback(
+						$settings_ui_page,
+						$current_section,
+						__( 'Settings UI schema generation failed.', 'woocommerce' )
+					);
+				} else {
+					$render_settings_ui = true;
+
+					try {
+						$script_handles = $settings_ui_page->get_script_handles( $current_section );
+					} catch ( \Throwable $e ) {
+						$script_handles     = array();
+						$render_settings_ui = false;
+						$reason             = __( 'Settings UI script handles could not be resolved.', 'woocommerce' );
+
+						wc_get_logger()->debug(
+							sprintf(
+								'Settings UI script handles could not be resolved for page "%1$s" section "%2$s": %3$s: %4$s',
+								$settings_ui_page->get_page_id(),
+								'' === $current_section ? 'default' : $current_section,
+								get_class( $e ),
+								$e->getMessage()
+							),
+							array( 'source' => 'settings-ui' )
+						);
+
+						if ( $e instanceof \Exception ) {
+							$reason = sprintf(
+								/* translators: %s: exception message. */
+								__( 'Settings UI script handles could not be resolved: %s', 'woocommerce' ),
+								$e->getMessage()
+							);
+							wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__ );
+						}

-					if ( $e instanceof \Exception ) {
-						wc_caught_exception( $e, __CLASS__ . '::' . __FUNCTION__ );
+						$this->log_settings_ui_fallback( $settings_ui_page, $current_section, $reason );
 					}
-				}

-				if ( $render_settings_ui ) {
-					/**
-					 * Extension-provided handles may violate the interface contract.
-					 *
-					 * @var mixed[] $script_handles
-					 */
-					foreach ( $script_handles as $script_handle ) {
-						if ( is_string( $script_handle ) && '' !== $script_handle ) {
-							wp_enqueue_script( $script_handle );
+					if ( $render_settings_ui ) {
+						/**
+						 * Extension-provided handles may violate the interface contract.
+						 *
+						 * @var mixed[] $script_handles
+						 */
+						foreach ( $script_handles as $script_handle ) {
+							if ( is_string( $script_handle ) && '' !== $script_handle ) {
+								wp_enqueue_script( $script_handle );
+							}
 						}
-					}

-					$GLOBALS['hide_save_button'] = true;
+						$GLOBALS['hide_save_button'] = true;

-					printf(
-						'<div id="%1$s" data-wc-settings-ui="1" data-wc-settings-page="%2$s" data-wc-settings-section="%3$s"></div>',
-						esc_attr( 'wc_settings_ui_' . sanitize_html_class( $this->id ) . '_' . sanitize_html_class( '' === $current_section ? 'default' : $current_section ) ),
-						esc_attr( $settings_ui_page->get_page_id() ),
-						esc_attr( $current_section )
-					);
-					return;
+						printf(
+							'<div id="%1$s" data-wc-settings-ui="1" data-wc-settings-page="%2$s" data-wc-settings-section="%3$s"></div>',
+							esc_attr( 'wc_settings_ui_' . sanitize_html_class( $this->id ) . '_' . sanitize_html_class( '' === $current_section ? 'default' : $current_section ) ),
+							esc_attr( $settings_ui_page->get_page_id() ),
+							esc_attr( $current_section )
+						);
+						return;
+					}
 				}
 			}

diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php
index a3141a1c03a..fef3cab7598 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/SettingsUIFeatureFlagTest.php
@@ -133,6 +133,93 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 		$this->assertTrue( $GLOBALS['hide_save_button'] );
 	}

+	/**
+	 * It emits developer feedback when settings UI rendering falls back to legacy output.
+	 */
+	public function test_settings_ui_fallback_emits_doing_it_wrong_notice(): void {
+		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
+		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		$this->setExpectedIncorrectUsage( 'WC_Settings_Page::output' );
+
+		$notices = array();
+		$action  = function ( $function_name, $message, $version ) use ( &$notices ) {
+			$notices[] = array(
+				'function_name' => $function_name,
+				'message'       => $message,
+				'version'       => $version,
+			);
+		};
+		add_action( 'doing_it_wrong_run', $action, 10, 3 );
+
+		global $current_section;
+		$current_section = 'advanced';
+		$page            = $this->get_settings_ui_test_page_with_failing_script_handles();
+
+		try {
+			ob_start();
+			$page->output();
+			$output = ob_get_clean();
+		} finally {
+			remove_action( 'doing_it_wrong_run', $action, 10 );
+			remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		}
+
+		$settings_page_notices = $this->get_settings_page_output_notices( $notices );
+
+		$this->assertStringContainsString( 'name="woocommerce_settings_ui_flag_test"', $output );
+		$this->assertStringNotContainsString( 'data-wc-settings-ui="1"', $output );
+		$this->assertNotEmpty( $settings_page_notices );
+		$this->assertSame( '10.9.0', $settings_page_notices[0]['version'] );
+		$this->assertStringContainsString( 'settings_ui_flag_test', $settings_page_notices[0]['message'] );
+		$this->assertStringContainsString( 'advanced', $settings_page_notices[0]['message'] );
+		$this->assertStringContainsString( 'Unable to load extension script handles.', $settings_page_notices[0]['message'] );
+	}
+
+	/**
+	 * It emits developer feedback when settings UI schema generation has failed.
+	 */
+	public function test_settings_ui_schema_failure_fallback_emits_doing_it_wrong_notice(): void {
+		add_filter( 'woocommerce_admin_features', array( $this, 'enable_settings_ui_feature' ) );
+		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		$this->setExpectedIncorrectUsage( 'WC_Settings_Page::output' );
+
+		$notices = array();
+		$action  = function ( $function_name, $message, $version ) use ( &$notices ) {
+			$notices[] = array(
+				'function_name' => $function_name,
+				'message'       => $message,
+				'version'       => $version,
+			);
+		};
+		add_action( 'doing_it_wrong_run', $action, 10, 3 );
+
+		global $current_section;
+		$current_section = 'advanced';
+		$page            = $this->get_settings_ui_test_page_with_failing_script_handles();
+
+		try {
+			$GLOBALS['wc_settings_ui_schema_failed']['settings_ui_flag_test']['advanced'] = true;
+
+			ob_start();
+			$page->output();
+			$output = ob_get_clean();
+		} finally {
+			unset( $GLOBALS['wc_settings_ui_schema_failed']['settings_ui_flag_test']['advanced'] );
+			remove_action( 'doing_it_wrong_run', $action, 10 );
+			remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		}
+
+		$settings_page_notices = $this->get_settings_page_output_notices( $notices );
+
+		$this->assertStringContainsString( 'name="woocommerce_settings_ui_flag_test"', $output );
+		$this->assertStringNotContainsString( 'data-wc-settings-ui="1"', $output );
+		$this->assertNotEmpty( $settings_page_notices );
+		$this->assertSame( '10.9.0', $settings_page_notices[0]['version'] );
+		$this->assertStringContainsString( 'settings_ui_flag_test', $settings_page_notices[0]['message'] );
+		$this->assertStringContainsString( 'advanced', $settings_page_notices[0]['message'] );
+		$this->assertStringContainsString( 'Settings UI schema generation failed.', $settings_page_notices[0]['message'] );
+	}
+
 	/**
 	 * It exposes section navigation metadata from legacy settings pages.
 	 */
@@ -270,6 +357,79 @@ class SettingsUIFeatureFlagTest extends WC_Unit_Test_Case {
 		};
 	}

+	/**
+	 * Get captured doing-it-wrong notices emitted by the settings page output method.
+	 *
+	 * @param array $notices Captured doing-it-wrong notices.
+	 * @return array
+	 */
+	private function get_settings_page_output_notices( array $notices ): array {
+		return array_values(
+			array_filter(
+				$notices,
+				static function ( array $notice ): bool {
+					return 'WC_Settings_Page::output' === $notice['function_name'];
+				}
+			)
+		);
+	}
+
+	/**
+	 * Build a settings page whose settings UI adapter cannot provide script handles.
+	 *
+	 * @return \WC_Settings_Page
+	 */
+	private function get_settings_ui_test_page_with_failing_script_handles(): \WC_Settings_Page {
+		return new class() extends \WC_Settings_Page {
+			/**
+			 * Constructor.
+			 */
+			public function __construct() {
+				$this->id    = 'settings_ui_flag_test';
+				$this->label = 'Settings UI flag test';
+			}
+
+			/**
+			 * Get the settings UI page adapter.
+			 *
+			 * @return \Automattic\WooCommerce\Admin\Settings\SettingsUIPageInterface|null
+			 */
+			public function get_settings_ui_page(): ?\Automattic\WooCommerce\Admin\Settings\SettingsUIPageInterface {
+				return new class( $this ) extends \Automattic\WooCommerce\Admin\Settings\LegacySettingsPageAdapter {
+					/**
+					 * Get script handles.
+					 *
+					 * @param string $section_id Section id.
+					 * @return array
+					 */
+					public function get_script_handles( string $section_id ): array {
+						if ( 'advanced' === $section_id ) {
+							throw new \RuntimeException( 'Unable to load extension script handles.' );
+						}
+
+						return array();
+					}
+				};
+			}
+
+			/**
+			 * Get settings for a section.
+			 *
+			 * @param string $section_id Section id.
+			 * @return array
+			 */
+			protected function get_settings_for_section_core( $section_id ) {
+				return array(
+					array(
+						'id'    => 'woocommerce_settings_ui_flag_test',
+						'type'  => 'text',
+						'title' => 'Settings UI flag test',
+					),
+				);
+			}
+		};
+	}
+
 	/**
 	 * Build a settings page with multiple sections.
 	 *