Commit 47d19d2350a for woocommerce

commit 47d19d2350a6dbc80996928db0661cf8e24d5509
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Thu May 14 13:15:06 2026 +0300

    Gate Customer Review Request behind a FeaturesController flag (#64845)

    * Gate Customer Review Request behind a feature flag

    New 'customer_review_request' feature in FeaturesController, off by
    default. With the flag off:

    - The OrderReviews DI services (Scheduler, Endpoint, SubmissionHandler,
      ItemEligibility) aren't resolved, so none of their init() hooks fire.
    - WC_Email_Customer_Review_Request isn't registered with WC_Emails.
    - The 10.8.0 update callback that seeded the page is gone — the host
      page is now created lazily on the first init() after the feature is
      enabled (Endpoint::maybe_create_host_page at init:4, before the
      rewrite rule registration).

    Test classes in tests/php/src/Internal/OrderReviews enable the flag
    in setUp() and clean it up in tearDown(); SchedulerTest also wires
    its service and reruns WC_Emails::init() so the mailer map picks up
    the gated email class.

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Tag Endpoint::maybe_create_host_page() @since 10.8.0

    * Address CodeRabbit review feedback

    - Defer the OrderReviews container resolution to `init` priority 0
      (class-woocommerce.php). FeaturesController's `init_feature_definitions()`
      calls `__()` for the feature name; running `feature_is_enabled()` from
      the constructor would trigger those translations too early.
    - Wrap `WC_Install::create_pages()` in a try/finally inside
      `Endpoint::maybe_create_host_page()` so the temporary
      `woocommerce_create_pages` filter is removed even if the page
      creation throws.
    - Use `wc_get_container()->get( Scheduler::class )->init()` in
      SchedulerTest::setUp instead of `new Scheduler()` so the container's
      cached instance is reused across tests instead of stacking duplicate
      hook registrations.

    * Replace OrderReviews gate closure with a named WooCommerce method

    Move the inline static closure that gates the OrderReviews container
    resolution to a new WooCommerce::maybe_init_order_reviews() method
    hooked at init priority 1.

    * Move OrderReviews gate next to the other init add_action calls

    * Remove review_order from WC_Install::create_pages default set

    The page is now seeded lazily by Endpoint::maybe_create_host_page on
    first feature-enabled init. Updated maybe_create_host_page to replace
    the woocommerce_create_pages filter output with just the review_order
    entry instead of intersecting against the default set.

    * Drop static modifier from inline closure

    * Use self::PAGE_KEY and self::SHORTCODE constants in the seed array

    * Require a published page entry before skipping the lazy seed

    If `woocommerce_review_order_page_id` points at a draft / private /
    wrong-type post, recreate the page so `add_rewrite_rule` can register
    the route (it requires `'publish'`).

    * Republish a draft / private host page instead of re-running seed

    WC_Install::create_pages() short-circuits when a page already exists
    at the stored option ID, even if its status is draft / private.
    Force-update the status to publish in place, then defer the rewrite
    flush so add_rewrite_rule registers the route on the next request.

    * Tighten the two new comments in maybe_create_host_page

    * Make Review Order page-creation self-healing and label it in the Pages list

    Surfaces two follow-ups from the manual test pass:

    - `maybe_create_host_page()` is no longer option-driven. A new private
      `find_existing_host_page()` looks up the host page by slug first (matching
      what pretty-permalink routing will resolve `/review-order/` to), then
      falls back to a `LIKE %[woocommerce_review_order]%` content search. If a
      page is found, the option is re-pointed at it and a rewrite-flush is
      queued; if the page is not published, it is republished in place. This
      fixes a class of bug where leftover duplicate "Review your order" rows
      from prior activations made `is_page($option_id)` diverge from WP's
      slug routing, so `gate_request()` silently skipped enqueueing the
      Review Order JS/CSS and the form rendered without progressive
      enhancement.
    - Added a `display_post_states` filter so the Pages admin list labels the
      WC-managed Review Order page as "— Review Order Page", matching how
      `WC_Admin_Post_Types` already labels Shop / Cart / Checkout / My
      account. The filter is registered from Endpoint::init() so it stays
      off whenever the feature flag is off.

    Adds two regression tests in EndpointTest:

    - `test_maybe_create_host_page_realigns_option_with_slug_routed_duplicate`
      seeds two pages with slug `review-order` (forcing the clash via
      `$wpdb->update`), points the option at the wrong one, and asserts the
      function re-aligns the option to the slug-routed page and queues a
      rewrite flush.
    - `test_maybe_create_host_page_republishes_draft_host_page` seeds a
      draft host page and asserts the function republishes it and queues a
      rewrite flush.

    Both tests share a `reset_review_order_pages()` helper that scrubs the
    shared setUp's seeded page so each test controls its own state.

    * Tighten host-page adoption to slug+shortcode signal and respect renames

    Addresses two Major findings from the local Copilot pass on 97b52123c2:

    - The shortcode-only fallback in `find_existing_host_page()` could silently
      republish a merchant's intentionally-private staging page that happened
      to embed `[woocommerce_review_order]`.
    - The same fallback could hijack `woocommerce_review_order_page_id` to an
      arbitrary older page on every request via `ORDER BY ID ASC LIMIT 1`.

    `maybe_create_host_page()` now adopts a page only when both signals
    agree: WP's slug routing resolves `/review-order/` to it AND the page
    embeds our shortcode. If that combined match fails but the option still
    points at a valid page (a merchant who renamed our slug), the function
    respects it and only republishes a draft we already own. Only when
    neither path applies does it fall through to `WC_Install::create_pages()`.

    * Fix PHPCS: reformat indented inline comments in maybe_create_host_page

    * Address prettyboymp review: restore Tools repair, cut per-request slug lookup

    Three findings from the review on #64845:

    - `Endpoint::init()` now registers a permanent `woocommerce_create_pages`
      filter (`inject_review_order_page()`) that appends our entry whenever
      the feature is on. Any caller of `WC_Install::create_pages()` —
      including Status → Tools "Create default pages" repair — sees the
      Review Order page again. The transient add/remove dance inside
      `maybe_create_host_page()` is gone.
    - `maybe_create_host_page()` short-circuits before the slug lookup when
      the stored option already points at a published page whose content
      embeds `[woocommerce_review_order]`. `get_post()` is served from the
      posts cache by id, so the steady-state cost drops from an indexed
      `wp_posts` SELECT (via `get_page_by_path`, invalidated by every
      post insert/update/delete on the site) to a cached lookup.
    - Updated the docblock on `maybe_flush_pending_rewrite()` so it no
      longer references the removed 10.8.0 db update; now describes the
      init:4 seeding + wp_loaded flush path actually in use.

    Tests:

    - New `test_inject_review_order_page_filter_adds_entry_for_third_party_callers`
      asserting the public method appends our entry and passes non-array
      values through.
    - Renamed `test_maybe_create_host_page_realigns_option_...` to
      `..._adopts_slug_canonical_when_option_dangles` and removed the
      stale option pre-stage so the test exercises the slug-reconciliation
      path with the fast path correctly skipped.

    * Fix Show more button in filter blocks being visible when it was not necessary (#64798)

    * Fix Show more button in filter blocks being visible when it was not necessary

    * Add changelog

    * Make sure overflow selected items are SSR

    * Animate the quick edit drawer close transition and close on save (#64858)

    * Animate the quick edit drawer close and close on successful save

    * Use stable boolean dep so close animation isn't undone by re-render

    * improve logic

    ---------

    Co-authored-by: Luigi Teschio <gigitux@gmail.com>

    * Add Shipping Class filter to the experimental products app product list (#64823)

    * Add Shipping Class filter to the experimental products app product list

    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

    * Fix shipping_class field getValue to return term ID so the filter aligns with row state

    Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
    Co-authored-by: Luigi Teschio <gigitux@gmail.com>

    * Address review follow-ups: image overflow, star sizing, @since/@internal

    - `__item-image img` constrained to `width: 100%; height: auto;` so the WC
      placeholder thumbnail respects its 120-px column on block themes that don't
      ship the global `img { max-width: 100% }` reset.
    - Star icon switched from `1.5em` to absolute `24px` so themes that reset
      `font-size: 0` on `input[type=radio]+label` can't collapse it to 0x0.
    - `@since 10.8.0` added to `inject_review_order_page`, `add_post_state_label`
      and the `find_canonical_host_page` helper.
    - `@internal` added to `inject_review_order_page` and `add_post_state_label`
      to match the pattern on existing filter-callback methods; these are public
      only because WP filter callbacks must be callable from outside.

    * Add changefile(s) from automation for the following project(s): @woocommerce/experimental-products-app, woocommerce

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
    Co-authored-by: Albert Juhé Lluveras <contact@albertjuhe.com>
    Co-authored-by: verofasulo <98944206+verofasulo@users.noreply.github.com>
    Co-authored-by: Luigi Teschio <gigitux@gmail.com>
    Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git a/packages/js/experimental-products-app/changelog/64845-wooplug-6696-gate-the-customer-review-request-feature-behind-a b/packages/js/experimental-products-app/changelog/64845-wooplug-6696-gate-the-customer-review-request-feature-behind-a
new file mode 100644
index 00000000000..8eca5bfa2a6
--- /dev/null
+++ b/packages/js/experimental-products-app/changelog/64845-wooplug-6696-gate-the-customer-review-request-feature-behind-a
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Gate the Customer Review Request feature behind a FeaturesController flag (off by default). The host page is created lazily on the first request after the feature is enabled, not on plugin install.
\ No newline at end of file
diff --git a/plugins/woocommerce/changelog/64845-wooplug-6696-gate-the-customer-review-request-feature-behind-a b/plugins/woocommerce/changelog/64845-wooplug-6696-gate-the-customer-review-request-feature-behind-a
new file mode 100644
index 00000000000..8eca5bfa2a6
--- /dev/null
+++ b/plugins/woocommerce/changelog/64845-wooplug-6696-gate-the-customer-review-request-feature-behind-a
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Gate the Customer Review Request feature behind a FeaturesController flag (off by default). The host page is created lazily on the first request after the feature is enabled, not on plugin install.
\ No newline at end of file
diff --git a/plugins/woocommerce/client/legacy/css/order-review.scss b/plugins/woocommerce/client/legacy/css/order-review.scss
index 6bb8bb792c3..6f0b1f6842d 100644
--- a/plugins/woocommerce/client/legacy/css/order-review.scss
+++ b/plugins/woocommerce/client/legacy/css/order-review.scss
@@ -48,6 +48,12 @@
 	&__item-image {
 		flex: 0 0 120px;
 		max-width: 120px;
+
+		img {
+			display: block;
+			width: 100%;
+			height: auto;
+		}
 	}

 	&__item-rating {
@@ -187,10 +193,12 @@
 		margin-left: 0;
 	}

+	// Absolute px so theme resets on `input[type=radio]+label` (e.g. font-size:0)
+	// can't collapse the icon to 0x0.
 	&__icon {
 		display: block;
-		width: 1.5em;
-		height: 1.5em;
+		width: 24px;
+		height: 24px;
 		fill: currentColor;
 		opacity: 0.3;
 		transition: opacity 100ms ease-in-out;
diff --git a/plugins/woocommerce/includes/class-wc-emails.php b/plugins/woocommerce/includes/class-wc-emails.php
index 551c4c48652..6441c8c6fd6 100644
--- a/plugins/woocommerce/includes/class-wc-emails.php
+++ b/plugins/woocommerce/includes/class-wc-emails.php
@@ -292,7 +292,6 @@ class WC_Emails {
 			'WC_Email_Customer_Processing_Order'     => __DIR__ . '/emails/class-wc-email-customer-processing-order.php',
 			'WC_Email_Customer_Completed_Order'      => __DIR__ . '/emails/class-wc-email-customer-completed-order.php',
 			'WC_Email_Customer_Refunded_Order'       => __DIR__ . '/emails/class-wc-email-customer-refunded-order.php',
-			'WC_Email_Customer_Review_Request'       => __DIR__ . '/emails/class-wc-email-customer-review-request.php',
 			'WC_Email_Customer_Invoice'              => __DIR__ . '/emails/class-wc-email-customer-invoice.php',
 			'WC_Email_Customer_Note'                 => __DIR__ . '/emails/class-wc-email-customer-note.php',
 			'WC_Email_Customer_Reset_Password'       => __DIR__ . '/emails/class-wc-email-customer-reset-password.php',
@@ -308,6 +307,9 @@ class WC_Emails {
 			$emails['WC_Email_Customer_Fulfillment_Updated'] = __DIR__ . '/emails/class-wc-email-customer-fulfillment-updated.php';
 			$emails['WC_Email_Customer_Fulfillment_Deleted'] = __DIR__ . '/emails/class-wc-email-customer-fulfillment-deleted.php';
 		}
+		if ( FeaturesUtil::feature_is_enabled( 'customer_review_request' ) ) {
+			$emails['WC_Email_Customer_Review_Request'] = __DIR__ . '/emails/class-wc-email-customer-review-request.php';
+		}

 		// Prime caches to reduce future queries.
 		wp_prime_option_caches(
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 8b7fb5fa5df..a81552692bc 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -46,7 +46,7 @@ class WC_Install {
 	 * @var array
 	 */
 	private static $db_updates = array(
-		'2.0.0'    => array(
+		'2.0.0'  => array(
 			'wc_update_200_file_paths',
 			'wc_update_200_permalinks',
 			'wc_update_200_subcat_display',
@@ -55,42 +55,42 @@ class WC_Install {
 			'wc_update_200_images',
 			'wc_update_200_db_version',
 		),
-		'2.0.9'    => array(
+		'2.0.9'  => array(
 			'wc_update_209_brazillian_state',
 			'wc_update_209_db_version',
 		),
-		'2.1.0'    => array(
+		'2.1.0'  => array(
 			'wc_update_210_remove_pages',
 			'wc_update_210_file_paths',
 			'wc_update_210_db_version',
 		),
-		'2.2.0'    => array(
+		'2.2.0'  => array(
 			'wc_update_220_shipping',
 			'wc_update_220_order_status',
 			'wc_update_220_variations',
 			'wc_update_220_attributes',
 			'wc_update_220_db_version',
 		),
-		'2.3.0'    => array(
+		'2.3.0'  => array(
 			'wc_update_230_options',
 			'wc_update_230_db_version',
 		),
-		'2.4.0'    => array(
+		'2.4.0'  => array(
 			'wc_update_240_options',
 			'wc_update_240_shipping_methods',
 			'wc_update_240_api_keys',
 			'wc_update_240_refunds',
 			'wc_update_240_db_version',
 		),
-		'2.4.1'    => array(
+		'2.4.1'  => array(
 			'wc_update_241_variations',
 			'wc_update_241_db_version',
 		),
-		'2.5.0'    => array(
+		'2.5.0'  => array(
 			'wc_update_250_currency',
 			'wc_update_250_db_version',
 		),
-		'2.6.0'    => array(
+		'2.6.0'  => array(
 			'wc_update_260_options',
 			'wc_update_260_termmeta',
 			'wc_update_260_zones',
@@ -98,26 +98,26 @@ class WC_Install {
 			'wc_update_260_refunds',
 			'wc_update_260_db_version',
 		),
-		'3.0.0'    => array(
+		'3.0.0'  => array(
 			'wc_update_300_grouped_products',
 			'wc_update_300_settings',
 			'wc_update_300_product_visibility',
 			'wc_update_300_db_version',
 		),
-		'3.1.0'    => array(
+		'3.1.0'  => array(
 			'wc_update_310_downloadable_products',
 			'wc_update_310_old_comments',
 			'wc_update_310_db_version',
 		),
-		'3.1.2'    => array(
+		'3.1.2'  => array(
 			'wc_update_312_shop_manager_capabilities',
 			'wc_update_312_db_version',
 		),
-		'3.2.0'    => array(
+		'3.2.0'  => array(
 			'wc_update_320_mexican_states',
 			'wc_update_320_db_version',
 		),
-		'3.3.0'    => array(
+		'3.3.0'  => array(
 			'wc_update_330_image_options',
 			'wc_update_330_webhooks',
 			'wc_update_330_product_stock_status',
@@ -126,48 +126,48 @@ class WC_Install {
 			'wc_update_330_set_paypal_sandbox_credentials',
 			'wc_update_330_db_version',
 		),
-		'3.4.0'    => array(
+		'3.4.0'  => array(
 			'wc_update_340_states',
 			'wc_update_340_state',
 			'wc_update_340_last_active',
 			'wc_update_340_db_version',
 		),
-		'3.4.3'    => array(
+		'3.4.3'  => array(
 			'wc_update_343_cleanup_foreign_keys',
 			'wc_update_343_db_version',
 		),
-		'3.4.4'    => array(
+		'3.4.4'  => array(
 			'wc_update_344_recreate_roles',
 			'wc_update_344_db_version',
 		),
-		'3.5.0'    => array(
+		'3.5.0'  => array(
 			'wc_update_350_reviews_comment_type',
 			'wc_update_350_db_version',
 		),
-		'3.5.2'    => array(
+		'3.5.2'  => array(
 			'wc_update_352_drop_download_log_fk',
 		),
-		'3.5.4'    => array(
+		'3.5.4'  => array(
 			'wc_update_354_modify_shop_manager_caps',
 			'wc_update_354_db_version',
 		),
-		'3.6.0'    => array(
+		'3.6.0'  => array(
 			'wc_update_360_product_lookup_tables',
 			'wc_update_360_term_meta',
 			'wc_update_360_downloadable_product_permissions_index',
 			'wc_update_360_db_version',
 		),
-		'3.7.0'    => array(
+		'3.7.0'  => array(
 			'wc_update_370_tax_rate_classes',
 			'wc_update_370_mro_std_currency',
 			'wc_update_370_db_version',
 		),
-		'3.9.0'    => array(
+		'3.9.0'  => array(
 			'wc_update_390_move_maxmind_database',
 			'wc_update_390_change_geolocation_database_update_cron',
 			'wc_update_390_db_version',
 		),
-		'4.0.0'    => array(
+		'4.0.0'  => array(
 			'wc_update_product_lookup_tables',
 			'wc_update_400_increase_size_of_column',
 			'wc_update_400_reset_action_scheduler_migration_status',
@@ -176,27 +176,27 @@ class WC_Install {
 			'wc_admin_update_0251_remove_unsnooze_action',
 			'wc_update_400_db_version',
 		),
-		'4.4.0'    => array(
+		'4.4.0'  => array(
 			'wc_update_440_insert_attribute_terms_for_variable_products',
 			'wc_admin_update_110_remove_facebook_note',
 			'wc_admin_update_130_remove_dismiss_action_from_tracking_opt_in_note',
 			'wc_update_440_db_version',
 		),
-		'4.5.0'    => array(
+		'4.5.0'  => array(
 			'wc_update_450_sanitize_coupons_code',
 			'wc_update_450_db_version',
 		),
-		'5.0.0'    => array(
+		'5.0.0'  => array(
 			'wc_update_500_fix_product_review_count',
 			'wc_admin_update_160_remove_facebook_note',
 			'wc_admin_update_170_homescreen_layout',
 			'wc_update_500_db_version',
 		),
-		'5.6.0'    => array(
+		'5.6.0'  => array(
 			'wc_update_560_create_refund_returns_page',
 			'wc_update_560_db_version',
 		),
-		'6.0.0'    => array(
+		'6.0.0'  => array(
 			'wc_update_600_migrate_rate_limit_options',
 			'wc_admin_update_270_delete_report_downloads',
 			'wc_admin_update_271_update_task_list_options',
@@ -205,133 +205,130 @@ class WC_Install {
 			'wc_admin_update_290_delete_default_homepage_layout_option',
 			'wc_update_600_db_version',
 		),
-		'6.3.0'    => array(
+		'6.3.0'  => array(
 			'wc_update_630_create_product_attributes_lookup_table',
 			'wc_admin_update_300_update_is_read_from_last_read',
 			'wc_update_630_db_version',
 		),
-		'6.4.0'    => array(
+		'6.4.0'  => array(
 			'wc_update_640_add_primary_key_to_product_attributes_lookup_table',
 			'wc_admin_update_340_remove_is_primary_from_note_action',
 			'wc_update_640_db_version',
 		),
-		'6.5.0'    => array(
+		'6.5.0'  => array(
 			'wc_update_650_approved_download_directories',
 		),
-		'6.5.1'    => array(
+		'6.5.1'  => array(
 			'wc_update_651_approved_download_directories',
 		),
-		'6.7.0'    => array(
+		'6.7.0'  => array(
 			'wc_update_670_purge_comments_count_cache',
 			'wc_update_670_delete_deprecated_remote_inbox_notifications_option',
 		),
-		'7.0.0'    => array(
+		'7.0.0'  => array(
 			'wc_update_700_remove_download_log_fk',
 			'wc_update_700_remove_recommended_marketing_plugins_transient',
 		),
-		'7.2.1'    => array(
+		'7.2.1'  => array(
 			'wc_update_721_adjust_new_zealand_states',
 			'wc_update_721_adjust_ukraine_states',
 		),
-		'7.2.2'    => array(
+		'7.2.2'  => array(
 			'wc_update_722_adjust_new_zealand_states',
 			'wc_update_722_adjust_ukraine_states',
 		),
-		'7.5.0'    => array(
+		'7.5.0'  => array(
 			'wc_update_750_add_columns_to_order_stats_table',
 			'wc_update_750_disable_new_product_management_experience',
 		),
-		'7.7.0'    => array(
+		'7.7.0'  => array(
 			'wc_update_770_remove_multichannel_marketing_feature_options',
 		),
-		'7.9.0'    => array(
+		'7.9.0'  => array(
 			'wc_update_790_blockified_product_grid_block',
 		),
-		'8.1.0'    => array(
+		'8.1.0'  => array(
 			'wc_update_810_migrate_transactional_metadata_for_hpos',
 		),
-		'8.3.0'    => array(
+		'8.3.0'  => array(
 			'wc_update_830_rename_checkout_template',
 			'wc_update_830_rename_cart_template',
 		),
-		'8.6.0'    => array(
+		'8.6.0'  => array(
 			'wc_update_860_remove_recommended_marketing_plugins_transient',
 		),
-		'8.7.0'    => array(
+		'8.7.0'  => array(
 			'wc_update_870_prevent_listing_of_transient_files_directory',
 		),
-		'8.9.0'    => array(
+		'8.9.0'  => array(
 			'wc_update_890_update_connect_to_woocommerce_note',
 			'wc_update_890_update_paypal_standard_load_eligibility',
 		),
-		'8.9.1'    => array(
+		'8.9.1'  => array(
 			'wc_update_891_create_plugin_autoinstall_history_option',
 		),
-		'9.1.0'    => array(
+		'9.1.0'  => array(
 			'wc_update_910_add_launch_your_store_tour_option',
 			'wc_update_910_remove_obsolete_user_meta',
 		),
-		'9.2.0'    => array(
+		'9.2.0'  => array(
 			'wc_update_920_add_wc_hooked_blocks_version_option',
 		),
-		'9.3.0'    => array(
+		'9.3.0'  => array(
 			'wc_update_930_add_woocommerce_coming_soon_option',
 			'wc_update_930_migrate_user_meta_for_launch_your_store_tour',
 		),
-		'9.4.0'    => array(
+		'9.4.0'  => array(
 			'wc_update_940_add_phone_to_order_address_fts_index',
 			'wc_update_940_remove_help_panel_highlight_shown',
 		),
-		'9.5.0'    => array(
+		'9.5.0'  => array(
 			'wc_update_950_tracking_option_autoload',
 		),
-		'9.6.1'    => array(
+		'9.6.1'  => array(
 			'wc_update_961_migrate_default_email_base_color',
 		),
-		'9.8.0'    => array(
+		'9.8.0'  => array(
 			'wc_update_980_remove_order_attribution_install_banner_dismissed_option',
 		),
-		'9.8.5'    => array(
+		'9.8.5'  => array(
 			'wc_update_985_enable_new_payments_settings_page_feature',
 		),
-		'9.9.0'    => array(
+		'9.9.0'  => array(
 			'wc_update_990_remove_wc_count_comments_transient',
 			'wc_update_990_remove_email_notes',
 		),
-		'10.0.0'   => array(
+		'10.0.0' => array(
 			'wc_update_1000_multisite_visibility_setting',
 			'wc_update_1000_remove_patterns_toolkit_transient',
 		),
-		'10.2.0'   => array(
+		'10.2.0' => array(
 			'wc_update_1020_add_old_refunded_order_items_to_product_lookup_table',
 		),
-		'10.3.0'   => array(
+		'10.3.0' => array(
 			'wc_update_1030_add_comments_date_type_index',
 		),
-		'10.4.0'   => array(
+		'10.4.0' => array(
 			'wc_update_1040_add_idx_date_paid_status_parent',
 			'wc_update_1040_cleanup_legacy_ptk_patterns_fetching',
 		),
-		'10.5.0'   => array(
+		'10.5.0' => array(
 			'wc_update_1050_migrate_brand_permalink_setting',
 			'wc_update_1050_enable_autoload_options',
 			'wc_update_1050_add_idx_user_email',
 			'wc_update_1050_remove_deprecated_marketplace_option',
 		),
-		'10.6.0'   => array(
+		'10.6.0' => array(
 			'wc_update_1060_add_woo_idx_comment_approved_type_index',
 		),
-		'10.7.0'   => array(
+		'10.7.0' => array(
 			'wc_update_1070_disable_hpos_sync_on_read',
 		),
-		'10.8.0'   => array(
+		'10.8.0' => array(
 			'wc_update_1080_migrate_analytics_import_option',
 			'wc_update_1080_slim_orders_meta_key_index',
 			'wc_update_1080_backfill_email_template_sync_meta',
 		),
-		'10.8.0-1' => array(
-			'wc_update_1080_create_review_order_page',
-		),
 	);

 	/**
@@ -1156,11 +1153,6 @@ class WC_Install {
 					'title'   => _x( 'My account', 'Page title', 'woocommerce' ),
 					'content' => '<!-- wp:shortcode -->[' . $my_account_shortcode . ']<!-- /wp:shortcode -->',
 				),
-				'review_order'   => array(
-					'name'    => _x( 'review-order', 'Page slug', 'woocommerce' ),
-					'title'   => _x( 'Review your order', 'Page title', 'woocommerce' ),
-					'content' => '<!-- wp:shortcode -->[woocommerce_review_order]<!-- /wp:shortcode -->',
-				),
 				'refund_returns' => array(
 					'name'        => _x( 'refund_returns', 'Page slug', 'woocommerce' ),
 					'title'       => _x( 'Refund and Returns Policy', 'Page title', 'woocommerce' ),
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 626071d2a63..5999b66ea5c 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -322,6 +322,7 @@ final class WooCommerce {
 		add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 );
 		add_action( 'load-post.php', array( $this, 'includes' ) );
 		add_action( 'init', array( $this, 'init' ), 0 );
+		add_action( 'init', array( $this, 'maybe_init_order_reviews' ), 1 );
 		add_action( 'init', array( 'WC_Shortcodes', 'init' ) );
 		add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) );
 		add_action( 'init', array( $this, 'add_image_sizes' ) );
@@ -376,10 +377,6 @@ final class WooCommerce {
 		$container->get( ProductVersionStringInvalidator::class );
 		$container->get( OrdersVersionStringInvalidator::class );
 		$container->get( TaxRateVersionStringInvalidator::class );
-		$container->get( Automattic\WooCommerce\Internal\OrderReviews\Scheduler::class );
-		$container->get( Automattic\WooCommerce\Internal\OrderReviews\Endpoint::class );
-		$container->get( Automattic\WooCommerce\Internal\OrderReviews\SubmissionHandler::class );
-		$container->get( Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility::class );

 		// Feature flags.
 		if ( Constants::is_true( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' ) ) {
@@ -963,6 +960,25 @@ final class WooCommerce {
 		do_action( 'woocommerce_init' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingSinceComment
 	}

+	/**
+	 * Resolve the OrderReviews services when the `customer_review_request`
+	 * feature flag is on. Hooked to `init` priority 1 from `init_hooks()`
+	 * so it runs after the textdomain is loaded.
+	 *
+	 * @since 10.8.0
+	 * @internal
+	 */
+	public function maybe_init_order_reviews(): void {
+		if ( ! \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled( 'customer_review_request' ) ) {
+			return;
+		}
+		$container = wc_get_container();
+		$container->get( \Automattic\WooCommerce\Internal\OrderReviews\Scheduler::class );
+		$container->get( \Automattic\WooCommerce\Internal\OrderReviews\Endpoint::class );
+		$container->get( \Automattic\WooCommerce\Internal\OrderReviews\SubmissionHandler::class );
+		$container->get( \Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility::class );
+	}
+
 	/**
 	 * Load Localisation files.
 	 *
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index 5630210607f..3648be5bb37 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -3511,31 +3511,3 @@ function wc_update_1080_slim_orders_meta_key_index(): void {
 function wc_update_1080_backfill_email_template_sync_meta(): bool {
 	return WCEmailTemplateSyncBackfill::run();
 }
-
-/**
- * Seeds the Review Order page on existing installs so the rewrite rule and
- * helper URL work after upgrading to 10.8.0. Mirrors how
- * `wc_update_560_create_refund_returns_page` backfilled the refund/returns
- * page when that feature shipped.
- *
- * @since 10.8.0
- *
- * @return void
- */
-function wc_update_1080_create_review_order_page(): void {
-	$only_review_order = static function ( array $pages ): array {
-		return array_intersect_key( $pages, array_flip( array( 'review_order' ) ) );
-	};
-
-	add_filter( 'woocommerce_create_pages', $only_review_order );
-
-	WC_Install::create_pages();
-
-	remove_filter( 'woocommerce_create_pages', $only_review_order );
-
-	// `Endpoint::add_rewrite_rule` runs on init:10; this update routine fires
-	// from WC_Install::check_version on init:5, so flushing here would
-	// persist the rules table without the new /review-order/{id}/ rule.
-	// Defer the flush via an option that the endpoint clears on wp_loaded.
-	update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
-}
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 69bcb03f025..caa86505e46 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -439,6 +439,18 @@ class FeaturesController {
 				'enabled_by_default'           => false,
 				'is_experimental'              => false,
 			),
+			'customer_review_request'            => array(
+				'name'                         => __( 'Customer review request (beta)', 'woocommerce' ),
+				'description'                  => __(
+					'Send customers a transactional email after order completion inviting them to review the products they bought, and host the per-order Review Order landing page.',
+					'woocommerce'
+				),
+				// Skip compatibility checks like the other opt-in transactional-email features.
+				'skip_compatibility_checks'    => true,
+				'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
+				'enabled_by_default'           => false,
+				'is_experimental'              => false,
+			),
 			'email_improvements'                 => array(
 				'name'                         => __( 'Email improvements', 'woocommerce' ),
 				'description'                  => __(
diff --git a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
index 7191d78e22f..9cabf5ebfdc 100644
--- a/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
+++ b/plugins/woocommerce/src/Internal/OrderReviews/Endpoint.php
@@ -66,15 +66,169 @@ class Endpoint {
 	 * @internal
 	 */
 	final public function init(): void {
+		// Seed the host page before `add_rewrite_rule` runs on init:10.
+		add_action( 'init', array( $this, 'maybe_create_host_page' ), 4 );
 		add_action( 'init', array( $this, 'add_rewrite_rule' ) );
 		add_filter( 'query_vars', array( $this, 'add_query_var' ), 0 );
 		add_action( 'template_redirect', array( $this, 'gate_request' ) );
 		add_action( 'wp_loaded', array( $this, 'maybe_flush_pending_rewrite' ) );
 		add_action( 'transition_post_status', array( $this, 'skip_auto_menu_for_self' ), 9, 3 );
 		add_filter( 'get_pages', array( $this, 'exclude_self_from_page_list' ) );
+		add_filter( 'display_post_states', array( $this, 'add_post_state_label' ), 10, 2 );
+		// Inject our entry into every `WC_Install::create_pages()` invocation so
+		// Status → Tools "Create default pages" and any other repair caller see it too.
+		add_filter( 'woocommerce_create_pages', array( $this, 'inject_review_order_page' ) );
 		add_shortcode( self::SHORTCODE, array( $this, 'render_shortcode' ) );
 	}

+	/**
+	 * Create or adopt the Review Order host page on every feature-on init.
+	 *
+	 * Idempotent and self-healing: re-aligns the stored option with whichever
+	 * row WP's permalink routing would resolve `/review-order/` to, so the
+	 * page id `gate_request()` checks always matches the page that
+	 * `add_rewrite_rule()` points at. Leftover duplicates from prior
+	 * activation/disable cycles no longer cause asset enqueueing to silently
+	 * skip.
+	 *
+	 * @since 10.8.0
+	 *
+	 * @internal
+	 */
+	public function maybe_create_host_page(): void {
+		// Fast path: the stored option already points at a published page
+		// that still embeds our shortcode. `get_post()` is served from the
+		// posts cache so this short-circuit costs ~nothing per request and
+		// avoids the slug `wp_posts` lookup the reconciliation path runs.
+		$option_id   = (int) wc_get_page_id( self::PAGE_KEY );
+		$option_page = $option_id > 0 ? get_post( $option_id ) : null;
+		if ( $option_page instanceof WP_Post
+			&& 'page' === $option_page->post_type
+			&& 'publish' === $option_page->post_status
+			&& false !== strpos( (string) $option_page->post_content, '[' . self::SHORTCODE . ']' ) ) {
+			return;
+		}
+
+		// Reconcile: adopt the slug-routed page when it also embeds our
+		// shortcode. The combined signal avoids hijacking a merchant page
+		// that happens to share either the slug or the shortcode alone.
+		$canonical = $this->find_canonical_host_page();
+		if ( $canonical instanceof WP_Post ) {
+			$needs_save = false;
+
+			if ( $option_id !== (int) $canonical->ID ) {
+				update_option( 'woocommerce_review_order_page_id', (int) $canonical->ID );
+				$needs_save = true;
+			}
+			if ( 'publish' !== $canonical->post_status ) {
+				wp_update_post(
+					array(
+						'ID'          => (int) $canonical->ID,
+						'post_status' => 'publish',
+					)
+				);
+				$needs_save = true;
+			}
+			if ( $needs_save ) {
+				update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+			}
+			return;
+		}
+
+		// No slug-canonical page. If the merchant renamed the host page away
+		// from our default slug but the stored option still resolves to a
+		// non-trashed page, respect it and only republish a draft we own.
+		if ( $option_page instanceof WP_Post && 'page' === $option_page->post_type && 'trash' !== $option_page->post_status ) {
+			if ( 'publish' !== $option_page->post_status ) {
+				wp_update_post(
+					array(
+						'ID'          => (int) $option_page->ID,
+						'post_status' => 'publish',
+					)
+				);
+				update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+			}
+			return;
+		}
+
+		// No managed page anywhere. The permanent `woocommerce_create_pages`
+		// filter (registered in `init()`) makes the call inject our entry.
+		\WC_Install::create_pages();
+
+		// Defer the rewrite flush to wp_loaded; rewrite_rule fires later on init.
+		update_option( 'woocommerce_review_order_flush_rewrite_pending', 'yes' );
+	}
+
+	/**
+	 * Append the Review Order page to any caller of
+	 * `WC_Install::create_pages()` — keeps Status → Tools' "Create default
+	 * pages" repair path and any third-party callers seeded with our page
+	 * whenever the feature is on, without having to call create_pages()
+	 * with a one-off filter in `maybe_create_host_page()`.
+	 *
+	 * @since 10.8.0
+	 *
+	 * @internal Public only because WP filter callbacks need to be callable from outside.
+	 *
+	 * @param array<string,array<string,string>>|mixed $pages Existing page definitions.
+	 * @return array<string,array<string,string>>|mixed
+	 */
+	public function inject_review_order_page( $pages ) {
+		if ( ! is_array( $pages ) ) {
+			return $pages;
+		}
+		$pages[ self::PAGE_KEY ] = array(
+			'name'    => _x( 'review-order', 'Page slug', 'woocommerce' ),
+			'title'   => _x( 'Review your order', 'Page title', 'woocommerce' ),
+			'content' => '<!-- wp:shortcode -->[' . self::SHORTCODE . ']<!-- /wp:shortcode -->',
+		);
+		return $pages;
+	}
+
+	/**
+	 * Return the slug-routed page if it also embeds our shortcode, so we only
+	 * adopt rows that are unambiguously WC-owned (matching slug alone or the
+	 * shortcode alone would hijack merchant-authored pages).
+	 *
+	 * @since 10.8.0
+	 *
+	 * @return WP_Post|null
+	 */
+	private function find_canonical_host_page(): ?WP_Post {
+		$page = get_page_by_path( _x( 'review-order', 'Page slug', 'woocommerce' ), OBJECT, 'page' );
+		if ( ! $page instanceof WP_Post || 'trash' === $page->post_status ) {
+			return null;
+		}
+		if ( false === strpos( (string) $page->post_content, '[' . self::SHORTCODE . ']' ) ) {
+			return null;
+		}
+		return $page;
+	}
+
+	/**
+	 * Label the Review Order page in the admin Pages list ("— Review Order
+	 * Page"), mirroring how `WC_Admin_Post_Types` labels Shop / Cart /
+	 * Checkout / My account so editors can spot it at a glance.
+	 *
+	 * @since 10.8.0
+	 *
+	 * @internal Public only because WP filter callbacks need to be callable from outside.
+	 *
+	 * @param array<string,string>|mixed $post_states Existing post-state labels keyed by id.
+	 * @param \WP_Post|mixed             $post        Current post being listed.
+	 * @return array<string,string>|mixed
+	 */
+	public function add_post_state_label( $post_states, $post ) {
+		if ( ! is_array( $post_states ) || ! $post instanceof \WP_Post ) {
+			return $post_states;
+		}
+		$page_id = (int) wc_get_page_id( self::PAGE_KEY );
+		if ( $page_id > 0 && $page_id === (int) $post->ID ) {
+			$post_states['wc_page_for_review_order'] = __( 'Review Order Page', 'woocommerce' );
+		}
+		return $post_states;
+	}
+
 	/**
 	 * Hide the Review Order page from `get_pages()` results.
 	 *
@@ -230,12 +384,13 @@ class Endpoint {
 	}

 	/**
-	 * Flush rewrite rules once after the 10.8.0 upgrade installs the
-	 * Review Order page.
+	 * Flush rewrite rules once after the Review Order page is seeded or
+	 * republished.
 	 *
-	 * The 10.8.0 db update runs on `init` priority 5 and only seeds the
-	 * page; `add_rewrite_rule()` doesn't fire until `init` priority 10, so
-	 * the flush has to happen later. `wp_loaded` runs after every `init`
+	 * `maybe_create_host_page()` runs on `init` priority 4 and queues the
+	 * flush by setting `woocommerce_review_order_flush_rewrite_pending`;
+	 * `add_rewrite_rule()` doesn't fire until `init` priority 10, so the
+	 * flush has to happen later. `wp_loaded` runs after every `init`
 	 * callback, which is the earliest safe moment.
 	 */
 	public function maybe_flush_pending_rewrite(): void {
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
index bef8c22dd48..36c89b59d08 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/EndpointTest.php
@@ -29,6 +29,8 @@ class EndpointTest extends WC_Unit_Test_Case {
 	 */
 	public function setUp(): void {
 		parent::setUp();
+		// Feature flag gates the OrderReviews stack.
+		update_option( 'woocommerce_feature_customer_review_request_enabled', 'yes' );
 		$this->endpoint = new Endpoint();

 		// `Endpoint::get_url()` derives the URL from the WC-managed Review
@@ -62,6 +64,7 @@ class EndpointTest extends WC_Unit_Test_Case {
 		}
 		wp_reset_postdata();
 		wp_set_current_user( 0 );
+		delete_option( 'woocommerce_feature_customer_review_request_enabled' );
 		parent::tearDown();
 	}

@@ -634,4 +637,115 @@ class EndpointTest extends WC_Unit_Test_Case {
 		$fresh = wc_get_order( $order->get_id() );
 		$this->assertSame( $preset, (string) $fresh->get_meta( SubmissionHandler::COMPLETED_META_KEY ) );
 	}
+
+	/**
+	 * @testdox maybe_create_host_page() re-aligns the option with the slug-routed page when duplicates exist.
+	 *
+	 * Prior activation/disable cycles can leave multiple pages with slug
+	 * `review-order`. WP's permalink routing resolves `/review-order/` to the
+	 * lowest-id match, so the option must agree with that or `gate_request()`
+	 * silently skips its work (assets never enqueue).
+	 */
+	public function test_maybe_create_host_page_adopts_slug_canonical_when_option_dangles(): void {
+		global $wpdb;
+
+		// Wipe whatever the shared setUp seeded so this test controls state.
+		$this->reset_review_order_pages();
+
+		$first_id  = (int) wp_insert_post(
+			array(
+				'post_type'    => 'page',
+				'post_status'  => 'publish',
+				'post_title'   => 'Review your order',
+				'post_name'    => 'review-order',
+				'post_content' => '<!-- wp:shortcode -->[woocommerce_review_order]<!-- /wp:shortcode -->',
+			)
+		);
+		$second_id = (int) wp_insert_post(
+			array(
+				'post_type'    => 'page',
+				'post_status'  => 'publish',
+				'post_title'   => 'Review your order',
+				'post_name'    => 'review-order-alt',
+				'post_content' => '<!-- wp:shortcode -->[woocommerce_review_order]<!-- /wp:shortcode -->',
+			)
+		);
+		// Force the slug clash that WP's uniqueness check would normally avoid.
+		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
+		$wpdb->update( $wpdb->posts, array( 'post_name' => 'review-order' ), array( 'ID' => $first_id ) );
+		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange
+		$wpdb->update( $wpdb->posts, array( 'post_name' => 'review-order' ), array( 'ID' => $second_id ) );
+		clean_post_cache( $first_id );
+		clean_post_cache( $second_id );
+
+		// Option absent so the fast-path short-circuit fails and reconciliation runs.
+		delete_option( 'woocommerce_review_order_page_id' );
+		delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
+
+		$this->endpoint->maybe_create_host_page();
+
+		$this->assertSame( $first_id, (int) wc_get_page_id( Endpoint::PAGE_KEY ), 'option should adopt the slug-routed (lowest-id) page' );
+		$this->assertSame( 'yes', get_option( 'woocommerce_review_order_flush_rewrite_pending' ), 'rewrite flush should be queued when the option moves' );
+	}
+
+	/**
+	 * @testdox maybe_create_host_page() republishes a draft host page and queues a rewrite flush.
+	 */
+	public function test_maybe_create_host_page_republishes_draft_host_page(): void {
+		$this->reset_review_order_pages();
+
+		$page_id = (int) wp_insert_post(
+			array(
+				'post_type'    => 'page',
+				'post_status'  => 'draft',
+				'post_title'   => 'Review your order',
+				'post_name'    => 'review-order',
+				'post_content' => '<!-- wp:shortcode -->[woocommerce_review_order]<!-- /wp:shortcode -->',
+			)
+		);
+		update_option( 'woocommerce_review_order_page_id', $page_id );
+		delete_option( 'woocommerce_review_order_flush_rewrite_pending' );
+
+		$this->endpoint->maybe_create_host_page();
+
+		$fresh = get_post( $page_id );
+		$this->assertSame( 'publish', $fresh->post_status, 'draft host page should be republished' );
+		$this->assertSame( 'yes', get_option( 'woocommerce_review_order_flush_rewrite_pending' ) );
+	}
+
+	/**
+	 * @testdox The `woocommerce_create_pages` filter injects the Review Order entry so any caller of `WC_Install::create_pages()` (e.g. Status → Tools repair) seeds the page.
+	 */
+	public function test_inject_review_order_page_filter_adds_entry_for_third_party_callers(): void {
+		$pages = $this->endpoint->inject_review_order_page( array() );
+
+		$this->assertArrayHasKey( Endpoint::PAGE_KEY, $pages );
+		$this->assertSame( 'review-order', $pages[ Endpoint::PAGE_KEY ]['name'] );
+		$this->assertStringContainsString( '[woocommerce_review_order]', $pages[ Endpoint::PAGE_KEY ]['content'] );
+
+		// Defensive: a non-array value passes through untouched (matches the
+		// guard inside the method so other filters in the chain stay intact).
+		$this->assertNull( $this->endpoint->inject_review_order_page( null ) );
+	}
+
+	/**
+	 * Remove every page that could match the Review Order lookup, plus the
+	 * stored option, so a test can stage a clean slate before exercising
+	 * `maybe_create_host_page()`.
+	 */
+	private function reset_review_order_pages(): void {
+		$candidates = get_posts(
+			array(
+				'name'             => 'review-order',
+				'post_type'        => 'page',
+				'post_status'      => 'any',
+				'numberposts'      => -1,
+				'suppress_filters' => false,
+			)
+		);
+		foreach ( $candidates as $page ) {
+			wp_delete_post( (int) $page->ID, true );
+		}
+		delete_option( 'woocommerce_review_order_page_id' );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php
index ce8dd2e5ecc..7b4cf23c4f8 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/ItemEligibilityTest.php
@@ -17,11 +17,20 @@ use WC_Unit_Test_Case;
  */
 class ItemEligibilityTest extends WC_Unit_Test_Case {

+	/**
+	 * Feature flag gates the OrderReviews stack.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		update_option( 'woocommerce_feature_customer_review_request_enabled', 'yes' );
+	}
+
 	/**
 	 * Reset between tests.
 	 */
 	public function tearDown(): void {
 		ItemEligibility::reset_cache();
+		delete_option( 'woocommerce_feature_customer_review_request_enabled' );
 		parent::tearDown();
 	}

diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php
index 0e225a6b965..dbd9f09448b 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SchedulerTest.php
@@ -22,8 +22,13 @@ class SchedulerTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		// Make sure the email class is available for WC()->mailer().
-		WC()->mailer();
+		// Feature flag gates the OrderReviews stack. Enable it, then resolve
+		// the Scheduler from the container (singleton across the test run)
+		// and call init() to wire hooks. Re-init WC_Emails so the
+		// review-request email class lands in the mailer map.
+		update_option( 'woocommerce_feature_customer_review_request_enabled', 'yes' );
+		wc_get_container()->get( Scheduler::class )->init();
+		WC()->mailer()->init();

 		$this->set_review_email_enabled( true );
 	}
@@ -35,6 +40,7 @@ class SchedulerTest extends WC_Unit_Test_Case {
 		$this->set_review_email_enabled( false );
 		remove_all_filters( 'woocommerce_should_send_review_request' );
 		remove_all_filters( 'woocommerce_review_request_delay_seconds' );
+		delete_option( 'woocommerce_feature_customer_review_request_enabled' );

 		parent::tearDown();
 	}
diff --git a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
index 669e2091f2b..56019616ace 100644
--- a/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/OrderReviews/SubmissionHandlerTest.php
@@ -17,6 +17,14 @@ use WPAjaxDieContinueException;
  */
 class SubmissionHandlerTest extends WC_Unit_Test_Case {

+	/**
+	 * Feature flag gates the OrderReviews stack.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+		update_option( 'woocommerce_feature_customer_review_request_enabled', 'yes' );
+	}
+
 	/**
 	 * Reset state between tests.
 	 */
@@ -29,6 +37,7 @@ class SubmissionHandlerTest extends WC_Unit_Test_Case {
 		remove_all_filters( 'wp_die_ajax_handler' );
 		remove_all_filters( 'wp_send_json_handler' );
 		remove_all_filters( 'wp_doing_ajax' );
+		delete_option( 'woocommerce_feature_customer_review_request_enabled' );
 		parent::tearDown();
 	}