Commit 036a162ab30 for woocommerce

commit 036a162ab30009ec5edfeeb3d78089bdc45fd9d8
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date:   Tue Sep 8 08:16:42 2026 +0100

    Move order withdrawal hooks to init (#68419)

    * Fix order withdrawal feature hook registration

    * Add changelog for order withdrawal hook fix

    * Fix order withdrawal cleanup hooks when disabled

    * Update plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php

    Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>

    * Preserve order withdrawal setting keys

    * Maintain is_enabled check in render_view

    ---------

    Co-authored-by: Oleksandr Aratovskyi <79862886+oaratovskyi@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/fix-order-withdrawal-feature-hooks b/plugins/woocommerce/changelog/fix-order-withdrawal-feature-hooks
new file mode 100644
index 00000000000..7870f894ec2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-order-withdrawal-feature-hooks
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Register order withdrawal endpoint hooks only when the feature is enabled.
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
index 26e5e738400..74144d58f2b 100644
--- a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
@@ -63,14 +63,28 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 	 */
 	public function register(): void {
 		add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'maybe_flush_rewrite_rules' ), 10, 1 );
+		add_action( 'init', array( $this, 'register_feature_hooks' ), 0, 0 );
+	}
+
+	/**
+	 * Register hooks for the current feature state.
+	 *
+	 * @since 11.1.0
+	 */
+	public function register_feature_hooks(): void {
+		add_action( 'woocommerce_before_delete_order', array( $this->form_processor, 'delete_order_withdrawal_inbox_note_for_order' ), 10, 1 );
+		add_action( 'before_delete_post', array( $this->form_processor, 'delete_order_withdrawal_inbox_note_for_order' ), 10, 1 );
+		add_action( 'woocommerce_privacy_remove_order_personal_data', array( $this->form_processor, 'delete_order_withdrawal_inbox_note_for_order' ), 10, 1 );
+
+		if ( ! $this->is_enabled() ) {
+			$this->feature_highlight_notification->register();
+			return;
+		}
+
 		add_filter( 'woocommerce_get_query_vars', array( $this, 'add_query_var' ), 10, 1 );
 		add_filter( 'woocommerce_endpoint_' . self::ENDPOINT_KEY . '_title', array( $this, 'get_endpoint_title' ), 10, 1 );
 		add_filter( 'woocommerce_settings_pages', array( $this, 'add_endpoint_setting' ), 10, 1 );
 		add_action( 'woocommerce_account_' . self::ENDPOINT_KEY . '_endpoint', array( $this, 'render_view' ) );
-		add_action( 'woocommerce_before_delete_order', array( $this->form_processor, 'delete_order_withdrawal_inbox_note_for_order' ), 10, 1 );
-		add_action( 'before_delete_post', array( $this->form_processor, 'delete_order_withdrawal_inbox_note_for_order' ), 10, 1 );
-		add_action( 'woocommerce_privacy_remove_order_personal_data', array( $this->form_processor, 'delete_order_withdrawal_inbox_note_for_order' ), 10, 1 );
-		add_action( 'init', array( $this, 'maybe_register_feature_highlight_notification' ), 10, 0 );
 	}

 	/**
@@ -108,17 +122,6 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 		}
 	}

-	/**
-	 * Register the order withdrawal feature highlight notification if the feature is not enabled.
-	 *
-	 * @since 11.1.0
-	 */
-	public function maybe_register_feature_highlight_notification(): void {
-		if ( ! $this->is_enabled() ) {
-			$this->feature_highlight_notification->register();
-		}
-	}
-
 	/**
 	 * Register the order withdrawal query var.
 	 *
@@ -132,8 +135,10 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 			return array();
 		}

-		if ( $this->is_enabled() ) {
-			$query_vars[ self::ENDPOINT_KEY ] = (string) get_option( self::ENDPOINT_OPTION, self::ENDPOINT_SLUG );
+		$endpoint = (string) get_option( self::ENDPOINT_OPTION, self::ENDPOINT_SLUG );
+
+		if ( ! empty( $endpoint ) ) {
+			$query_vars[ self::ENDPOINT_KEY ] = $endpoint;
 		}

 		return $query_vars;
@@ -164,10 +169,6 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 			return array();
 		}

-		if ( ! $this->is_enabled() ) {
-			return $settings;
-		}
-
 		$endpoint_setting = array(
 			'title'    => __( 'Order withdrawal', 'woocommerce' ),
 			'desc'     => __( 'Endpoint for the order withdrawal page.', 'woocommerce' ),
@@ -180,7 +181,7 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 		$new_settings = array();
 		$added        = false;

-		foreach ( $settings as $setting ) {
+		foreach ( $settings as $key => $setting ) {
 			if ( is_array( $setting ) && self::ENDPOINT_OPTION === ( $setting['id'] ?? '' ) ) {
 				return $settings;
 			}
@@ -191,15 +192,15 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 				'sectionend' === ( $setting['type'] ?? '' ) &&
 				'account_endpoint_options' === ( $setting['id'] ?? '' )
 			) {
-				$new_settings[] = $endpoint_setting;
-				$added          = true;
+				$new_settings[ self::ENDPOINT_OPTION ] = $endpoint_setting;
+				$added                                 = true;
 			}

-			$new_settings[] = $setting;
+			$new_settings[ $key ] = $setting;
 		}

 		if ( ! $added ) {
-			$new_settings[] = $endpoint_setting;
+			$new_settings[ self::ENDPOINT_OPTION ] = $endpoint_setting;
 		}

 		return $new_settings;
@@ -214,7 +215,6 @@ final class OrderWithdrawalController implements RegisterHooksInterface {
 		if ( ! $this->is_enabled() ) {
 			return;
 		}
-
 		wc_get_template( 'myaccount/form-order-withdrawal.php', $this->get_template_args() );
 	}

diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
index abc7f02af99..d73ec1f2822 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderWithdrawal/OrderWithdrawalTest.php
@@ -671,16 +671,24 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 	 * @testdox Should register cleanup hooks for HPOS and legacy order deletion.
 	 */
 	public function test_controller_registers_order_deletion_cleanup_hooks(): void {
+		$this->enable_feature();
+
 		$controller = new OrderWithdrawalController();
-		$controller->init( $this->sut, new OrderWithdrawalFormView(), new OrderWithdrawalFeatureHighlightNotification() );
+		$controller->init(
+			$this->sut,
+			new OrderWithdrawalFormView(),
+			new OrderWithdrawalFeatureHighlightNotification()
+		);

 		try {
 			$controller->register();
+			$controller->register_feature_hooks();

 			$this->assertNotFalse( has_action( 'woocommerce_before_delete_order', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ) );
 			$this->assertNotFalse( has_action( 'before_delete_post', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ) );
 		} finally {
 			remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $controller, 'maybe_flush_rewrite_rules' ), 10 );
+			remove_action( 'init', array( $controller, 'register_feature_hooks' ), 0 );
 			remove_filter( 'woocommerce_get_query_vars', array( $controller, 'add_query_var' ), 10 );
 			remove_filter( 'woocommerce_endpoint_order-withdrawal_title', array( $controller, 'get_endpoint_title' ), 10 );
 			remove_filter( 'woocommerce_settings_pages', array( $controller, 'add_endpoint_setting' ), 10 );
@@ -700,19 +708,23 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		$controller   = new OrderWithdrawalController();
 		$notification = new OrderWithdrawalFeatureHighlightNotification();

-		$controller->init( $this->sut, new OrderWithdrawalFormView(), $notification );
+		$controller->init(
+			$this->sut,
+			new OrderWithdrawalFormView(),
+			$notification
+		);

 		try {
 			$controller->register();

-			$this->assertNotFalse( has_action( 'init', array( $controller, 'maybe_register_feature_highlight_notification' ) ), 'The controller should defer feature highlight notification registration until init.' );
+			$this->assertNotFalse( has_action( 'init', array( $controller, 'register_feature_hooks' ) ), 'The controller should defer feature-specific hook registration until init.' );

-			$controller->maybe_register_feature_highlight_notification();
+			$controller->register_feature_hooks();

 			$this->assertFalse( has_action( 'update_option_woocommerce_coming_soon', array( $notification, 'maybe_add_note_when_store_goes_live' ) ), 'The feature highlight notification should not listen for coming-soon changes when the feature is enabled.' );
 			$this->assertFalse( has_action( 'wc_admin_daily', array( $notification, 'possibly_add_note' ) ), 'The feature highlight notification should not run daily when the feature is enabled.' );
 		} finally {
-			remove_action( 'init', array( $controller, 'maybe_register_feature_highlight_notification' ), 10 );
+			remove_action( 'init', array( $controller, 'register_feature_hooks' ), 0 );
 			remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $controller, 'maybe_flush_rewrite_rules' ), 10 );
 			remove_filter( 'woocommerce_get_query_vars', array( $controller, 'add_query_var' ), 10 );
 			remove_filter( 'woocommerce_endpoint_order-withdrawal_title', array( $controller, 'get_endpoint_title' ), 10 );
@@ -724,6 +736,45 @@ class OrderWithdrawalTest extends WC_Unit_Test_Case {
 		}
 	}

+	/**
+	 * @testdox Should keep cleanup hooks but skip endpoint hooks after order withdrawal is disabled.
+	 */
+	public function test_controller_keeps_cleanup_hooks_after_feature_is_disabled(): void {
+		$this->enable_feature();
+		$this->disable_feature();
+
+		$controller   = new OrderWithdrawalController();
+		$notification = new OrderWithdrawalFeatureHighlightNotification();
+
+		$controller->init(
+			$this->sut,
+			new OrderWithdrawalFormView(),
+			$notification
+		);
+
+		try {
+			$controller->register();
+			$controller->register_feature_hooks();
+
+			$this->assertFalse( has_filter( 'woocommerce_get_query_vars', array( $controller, 'add_query_var' ) ), 'The endpoint query var should not be registered while the feature is disabled.' );
+			$this->assertFalse( has_filter( 'woocommerce_endpoint_order-withdrawal_title', array( $controller, 'get_endpoint_title' ) ), 'The endpoint title should not be registered while the feature is disabled.' );
+			$this->assertFalse( has_filter( 'woocommerce_settings_pages', array( $controller, 'add_endpoint_setting' ) ), 'The endpoint setting should not be registered while the feature is disabled.' );
+			$this->assertFalse( has_action( 'woocommerce_account_order-withdrawal_endpoint', array( $controller, 'render_view' ) ), 'The endpoint renderer should not be registered while the feature is disabled.' );
+			$this->assertNotFalse( has_action( 'woocommerce_before_delete_order', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ), 'HPOS order cleanup should stay registered while the feature is disabled.' );
+			$this->assertNotFalse( has_action( 'before_delete_post', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ), 'Legacy order cleanup should stay registered while the feature is disabled.' );
+			$this->assertNotFalse( has_action( 'woocommerce_privacy_remove_order_personal_data', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ) ), 'Privacy erasure cleanup should stay registered while the feature is disabled.' );
+			$this->assertNotFalse( has_action( 'wc_admin_daily', array( $notification, 'possibly_add_note' ) ), 'The feature highlight notification should run while the feature is disabled.' );
+		} finally {
+			remove_action( 'init', array( $controller, 'register_feature_hooks' ), 0 );
+			remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $controller, 'maybe_flush_rewrite_rules' ), 10 );
+			remove_action( 'update_option_woocommerce_coming_soon', array( $notification, 'maybe_add_note_when_store_goes_live' ), 10 );
+			remove_action( 'wc_admin_daily', array( $notification, 'possibly_add_note' ), 10 );
+			remove_action( 'woocommerce_before_delete_order', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ), 10 );
+			remove_action( 'before_delete_post', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ), 10 );
+			remove_action( 'woocommerce_privacy_remove_order_personal_data', array( $this->sut, 'delete_order_withdrawal_inbox_note_for_order' ), 10 );
+		}
+	}
+
 	/**
 	 * @testdox Should keep the user on review with an error notice when notification emails fail.
 	 */