Commit 3c9338533fe for woocommerce
commit 3c9338533fe8f6a7190cf4dbb5d50b5866399128
Author: Tom Cafferkey <tjcafferkey@gmail.com>
Date: Fri Jul 17 09:35:24 2026 +0100
Add feature-gated order withdrawal endpoint (#66538)
* Create order withdrawal route and feature flag
* Add Order Withdrawal to the Page Setup settings page
* Add changelog
* Move order-withdrawal content for Shortcode class
* Filter out endpoint if FG is disabled
* Refactor order withdrawal
* Remove skip_compat checks
* Undo auto formatting
* Undo changes
* Change render_endpoint to render_view
* Fix callback typo
* Move render logic to only when user is logged out
* Check WC query
diff --git a/plugins/woocommerce/changelog/add-order-withdrawal-endpoint b/plugins/woocommerce/changelog/add-order-withdrawal-endpoint
new file mode 100644
index 00000000000..e3e711ff588
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-order-withdrawal-endpoint
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add a feature-gated public order withdrawal endpoint.
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index c808504d4d5..acaf6d55204 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -418,6 +418,7 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\Orders\PointOfSaleEmailHandler::class )->register();
$container->get( Automattic\WooCommerce\Internal\POS\POSController::class )->register();
$container->get( Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController::class )->register();
+ $container->get( Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalController::class )->register();
// Classes inheriting from RestApiControllerBase.
$container->get( Automattic\WooCommerce\Internal\ReceiptRendering\ReceiptRenderingRestController::class )->register();
diff --git a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
index c5dda9a9102..21ca1025616 100644
--- a/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
+++ b/plugins/woocommerce/includes/shortcodes/class-wc-shortcode-my-account.php
@@ -8,6 +8,8 @@
* @version 2.0.0
*/
+use Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalController;
+
defined( 'ABSPATH' ) || exit;
/**
@@ -49,7 +51,12 @@ class WC_Shortcode_My_Account {
// Show login form if not logged in.
if ( ! is_user_logged_in() ) {
- wc_get_template( 'myaccount/form-login.php' );
+ if ( wc_get_container()->get( OrderWithdrawalController::class )->is_endpoint_request() ) {
+ // Order withdrawal is an EU regulation requirement which needs to be accessible without logging in.
+ wc_get_container()->get( OrderWithdrawalController::class )->render_view();
+ } else {
+ wc_get_template( 'myaccount/form-login.php' );
+ }
return;
}
diff --git a/plugins/woocommerce/includes/wc-formatting-functions.php b/plugins/woocommerce/includes/wc-formatting-functions.php
index 7488ae85731..6e511236f33 100644
--- a/plugins/woocommerce/includes/wc-formatting-functions.php
+++ b/plugins/woocommerce/includes/wc-formatting-functions.php
@@ -1695,6 +1695,7 @@ add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_de
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
+add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_order_withdrawal_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 );
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index dbd567e205a..a12b1d51c7a 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -458,6 +458,14 @@ class FeaturesController {
'enabled_by_default' => false,
'is_experimental' => false,
),
+ 'order_withdrawal' => array(
+ 'name' => __( 'Order withdrawal', 'woocommerce' ),
+ 'description' => __( 'Enable the public order withdrawal endpoint for stakeholder testing.', 'woocommerce' ),
+ 'enabled_by_default' => false,
+ 'disable_ui' => false,
+ 'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
+ 'is_experimental' => true,
+ ),
'abandoned_cart_recovery' => array(
'name' => __( 'Abandoned cart recovery', 'woocommerce' ),
'description' => __(
diff --git a/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
new file mode 100644
index 00000000000..c447c2534e2
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/OrderWithdrawal/OrderWithdrawalController.php
@@ -0,0 +1,151 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Internal\OrderWithdrawal;
+
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
+
+/**
+ * Registers the order withdrawal endpoint and related feature-gated UI.
+ *
+ * @internal Just for internal use.
+ */
+final class OrderWithdrawalController implements RegisterHooksInterface {
+
+ private const FEATURE_ID = 'order_withdrawal';
+ private const ENDPOINT_KEY = 'order-withdrawal';
+ private const ENDPOINT_OPTION = 'woocommerce_myaccount_order_withdrawal_endpoint';
+
+ /**
+ * Register hooks.
+ */
+ public function register(): void {
+ add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'maybe_flush_rewrite_rules' ), 10, 1 );
+ 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' ) );
+ }
+
+ /**
+ * Whether order withdrawal is enabled.
+ */
+ public function is_enabled(): bool {
+ return FeaturesUtil::feature_is_enabled( self::FEATURE_ID );
+ }
+
+ /**
+ * Whether the current My Account request is for the public order withdrawal endpoint.
+ */
+ public function is_endpoint_request(): bool {
+ global $wp;
+
+ return $this->is_enabled()
+ && isset( $wp->query_vars[ self::ENDPOINT_KEY ] )
+ && self::ENDPOINT_KEY === WC()->query->get_current_endpoint();
+ }
+
+ /**
+ * Queue a rewrite rules flush when the feature is toggled.
+ *
+ * @param string $feature_id Feature being toggled.
+ */
+ public function maybe_flush_rewrite_rules( string $feature_id ): void {
+ if ( self::FEATURE_ID === $feature_id ) {
+ update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
+ }
+ }
+
+ /**
+ * Register the order withdrawal query var.
+ *
+ * @param array $query_vars Existing query vars keyed by endpoint key.
+ */
+ public function add_query_var( $query_vars ): array {
+ if ( ! is_array( $query_vars ) ) {
+ return array();
+ }
+
+ if ( $this->is_enabled() ) {
+ $query_vars[ self::ENDPOINT_KEY ] = (string) get_option( self::ENDPOINT_OPTION, self::ENDPOINT_KEY );
+ }
+
+ return $query_vars;
+ }
+
+ /**
+ * Order withdrawal endpoint page title.
+ *
+ * @param string $title Default title.
+ */
+ public function get_endpoint_title( $title ): string {
+ return __( 'Order withdrawal', 'woocommerce' );
+ }
+
+ /**
+ * Add the endpoint setting when the feature is enabled.
+ *
+ * @param array $settings Page settings.
+ */
+ public function add_endpoint_setting( $settings ): array {
+ if ( ! is_array( $settings ) ) {
+ return array();
+ }
+
+ if ( ! $this->is_enabled() ) {
+ return $settings;
+ }
+
+ $endpoint_setting = array(
+ 'title' => __( 'Order withdrawal', 'woocommerce' ),
+ 'desc' => __( 'Endpoint for the order withdrawal page.', 'woocommerce' ),
+ 'id' => self::ENDPOINT_OPTION,
+ 'type' => 'text',
+ 'default' => self::ENDPOINT_KEY,
+ 'desc_tip' => true,
+ );
+
+ $new_settings = array();
+ $added = false;
+
+ foreach ( $settings as $setting ) {
+ if ( is_array( $setting ) && self::ENDPOINT_OPTION === ( $setting['id'] ?? '' ) ) {
+ return $settings;
+ }
+
+ if (
+ ! $added &&
+ is_array( $setting ) &&
+ 'sectionend' === ( $setting['type'] ?? '' ) &&
+ 'account_endpoint_options' === ( $setting['id'] ?? '' )
+ ) {
+ $new_settings[] = $endpoint_setting;
+ $added = true;
+ }
+
+ $new_settings[] = $setting;
+ }
+
+ if ( ! $added ) {
+ $new_settings[] = $endpoint_setting;
+ }
+
+ return $new_settings;
+ }
+
+ /**
+ * Render the order withdrawal view.
+ */
+ public function render_view(): void {
+ if ( ! $this->is_enabled() ) {
+ return;
+ }
+
+ ?>
+ <h2><?php esc_html_e( 'Order withdrawal', 'woocommerce' ); ?></h2>
+ <p><?php esc_html_e( 'This is placeholder content for the order withdrawal page.', 'woocommerce' ); ?></p>
+ <?php
+ }
+}