Commit 0de1de81b52 for woocommerce

commit 0de1de81b5265d3faea380991d7ae22c0e4d2c0e
Author: Samuel Urbanowicz <samiuelson@gmail.com>
Date:   Wed Jun 17 09:36:17 2026 +0200

    Remove Point of Sale feature flag from WooCommerce (#65573)

    * Remove Point of Sale feature flag

    The point_of_sale feature flag has been enabled by default for some
    time. Remove the toggle from WooCommerce → Settings → Advanced → Features
    and run all POS-related code paths unconditionally: settings page,
    product POS visibility checkbox/sync, transactional emails, and the
    block email editor's core template list.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Address review findings

    - Restore point_of_sale as a deprecation shim with deprecated_since 11.0.0
      and deprecated_value: true. Third-party code calling
      FeaturesUtil::feature_is_enabled('point_of_sale') continues to receive
      the previous behaviour (true) with a deprecation notice, matching the
      marketplace flag precedent (#62264).
    - Gate POSProductVisibilitySync::set_product_pos_visibility() on the same
      product-type/downloadable check used by the meta-box view, so saving an
      unsupported product (downloadable, grouped, external) no longer
      silently applies the pos-hidden term.
    - Drop the now-stale phpstan-baseline entry for the removed
      WC_Admin_Settings::reset_settings_pages_on_feature_change() method.
    - Extend WCTransactionalEmailsTest::testGetTransactionalEmailsReturnsDefaultEmails
      to assert the POS templates are in the unconditional list.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Drop deprecation shim for point_of_sale flag

    The flag was marked is_experimental: true and never documented as a
    public extension point — experimental features don't carry a backwards-
    compatibility contract, so a deprecation shim implies one that never
    existed. Remove the definition entirely.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

    * Restore point_of_sale feature as deprecated and always enabled

    * Extract POS product support check into shared helper

    * Remove duplicate settings tab registration from POS settings page

    * Update changelog entry for POS feature flag deprecation

    ---------

    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/woomob-2683-remove-pos-feature-flag b/plugins/woocommerce/changelog/woomob-2683-remove-pos-feature-flag
new file mode 100644
index 00000000000..b981b2aaf5b
--- /dev/null
+++ b/plugins/woocommerce/changelog/woomob-2683-remove-pos-feature-flag
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Deprecate the Point of Sale feature flag and remove its toggle from WooCommerce settings; POS-related functionality (settings page, product visibility, and transactional emails) is now always enabled. The flag setting remains readable via the REST API for mobile app compatibility.
diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
index eb10d479b86..0d7a646fcf6 100644
--- a/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
+++ b/plugins/woocommerce/includes/admin/class-wc-admin-settings.php
@@ -7,8 +7,6 @@
  */

 use Automattic\Jetpack\Constants;
-use Automattic\WooCommerce\Internal\Features\FeaturesController;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;

 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
@@ -62,9 +60,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
 				if ( \Automattic\WooCommerce\Admin\Features\Features::is_enabled( 'launch-your-store' ) ) {
 					$settings[] = include_once __DIR__ . '/settings/class-wc-settings-site-visibility.php';
 				}
-				if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) {
-					$settings[] = include_once __DIR__ . '/settings/class-wc-settings-point-of-sale.php';
-				}
+				$settings[] = include_once __DIR__ . '/settings/class-wc-settings-point-of-sale.php';
 				$settings[] = include_once __DIR__ . '/settings/class-wc-settings-advanced.php';

 				self::$settings = apply_filters( 'woocommerce_get_settings_pages', $settings );
@@ -79,29 +75,11 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
 						}
 					}
 				);
-
-				// Reset settings when features that affect settings are toggled.
-				add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( __CLASS__, 'reset_settings_pages_on_feature_change' ), 10, 2 );
 			}

 			return self::$settings;
 		}

-		/**
-		 * Reset settings when features that affect settings are toggled.
-		 *
-		 * @param string $feature_id The feature ID.
-		 * @param bool   $is_enabled Whether the feature is enabled.
-		 *
-		 * @internal For exclusive usage within this class, backwards compatibility not guaranteed.
-		 */
-		public static function reset_settings_pages_on_feature_change( $feature_id, $is_enabled ) {
-			if ( 'point_of_sale' === $feature_id && $is_enabled ) {
-				self::$settings = array();
-				self::get_settings_pages();
-			}
-		}
-
 		/**
 		 * Save the settings.
 		 */
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php
index 2de3a02f672..c71e98f1c8a 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/class-wc-meta-box-product-data.php
@@ -12,7 +12,6 @@ use Automattic\WooCommerce\Enums\ProductStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
 use Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog\POSProductVisibilitySync;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;

 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
@@ -428,9 +427,12 @@ class WC_Meta_Box_Product_Data {
 		// Remove _product_template_id for products that were created with the new product editor.
 		$product->delete_meta_data( '_product_template_id' );

-		if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) {
+		// Only sync POS visibility for product types where the "Available for POS" checkbox is rendered,
+		// so unsupported types (downloadable, grouped, external) don't silently acquire the pos-hidden term.
+		$pos_visibility_sync = wc_get_container()->get( POSProductVisibilitySync::class );
+		if ( $product instanceof WC_Product && $pos_visibility_sync->is_product_supported( $product ) ) {
 			$visible_in_pos = isset( $_POST['_visible_in_pos'] ) && 'yes' === wc_clean( wp_unslash( $_POST['_visible_in_pos'] ) );
-			wc_get_container()->get( POSProductVisibilitySync::class )->set_product_pos_visibility( $post_id, $visible_in_pos );
+			$pos_visibility_sync->set_product_pos_visibility( $post_id, $visible_in_pos );
 		}

 		/**
diff --git a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php
index ded7b062bf0..7f6553bf036 100644
--- a/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php
+++ b/plugins/woocommerce/includes/admin/meta-boxes/views/html-product-data-advanced.php
@@ -6,7 +6,7 @@
  * @var WC_Product $product_object
  */

-use Automattic\WooCommerce\Utilities\FeaturesUtil;
+use Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog\POSProductVisibilitySync;

 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
@@ -61,34 +61,32 @@ if ( ! defined( 'ABSPATH' ) ) {
 			?>
 		</div>
 	<?php endif; ?>
-	<?php if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) : ?>
-		<?php $is_pos_supported = $product_object->is_type( array( 'simple', 'variable' ) ) && ! $product_object->is_downloadable(); ?>
-		<div class="options_group" id="pos_visibility_supported" <?php echo $is_pos_supported ? '' : 'style="display: none;"'; ?>>
-			<?php
-			$visible_in_pos = ! has_term( 'pos-hidden', 'pos_product_visibility', $product_object->get_id() );
-			woocommerce_wp_checkbox(
-				array(
-					'id'          => '_visible_in_pos',
-					'value'       => $visible_in_pos ? 'yes' : 'no',
-					'label'       => __( 'Available for POS', 'woocommerce' ),
-					'desc_tip'    => true,
-					'description' => __( 'Controls whether this product appears in the Point of Sale system.', 'woocommerce' ),
-				)
-			);
-			?>
-		</div>
-		<div class="options_group" id="pos_visibility_unsupported" <?php echo $is_pos_supported ? 'style="display: none;"' : ''; ?>>
-			<?php
-			woocommerce_wp_note(
-				array(
-					'id'      => '_pos_visibility_note',
-					'label'   => __( 'Point of Sale', 'woocommerce' ),
-					'message' => __( 'This product type is not currently supported.', 'woocommerce' ),
-				)
-			);
-			?>
-		</div>
-	<?php endif; ?>
+	<?php $is_pos_supported = wc_get_container()->get( POSProductVisibilitySync::class )->is_product_supported( $product_object ); ?>
+	<div class="options_group" id="pos_visibility_supported" <?php echo $is_pos_supported ? '' : 'style="display: none;"'; ?>>
+		<?php
+		$visible_in_pos = ! has_term( 'pos-hidden', 'pos_product_visibility', $product_object->get_id() );
+		woocommerce_wp_checkbox(
+			array(
+				'id'          => '_visible_in_pos',
+				'value'       => $visible_in_pos ? 'yes' : 'no',
+				'label'       => __( 'Available for POS', 'woocommerce' ),
+				'desc_tip'    => true,
+				'description' => __( 'Controls whether this product appears in the Point of Sale system.', 'woocommerce' ),
+			)
+		);
+		?>
+	</div>
+	<div class="options_group" id="pos_visibility_unsupported" <?php echo $is_pos_supported ? 'style="display: none;"' : ''; ?>>
+		<?php
+		woocommerce_wp_note(
+			array(
+				'id'      => '_pos_visibility_note',
+				'label'   => __( 'Point of Sale', 'woocommerce' ),
+				'message' => __( 'This product type is not currently supported.', 'woocommerce' ),
+			)
+		);
+		?>
+	</div>

 	<?php do_action( 'woocommerce_product_options_advanced' ); ?>
 </div>
diff --git a/plugins/woocommerce/includes/admin/settings/class-wc-settings-point-of-sale.php b/plugins/woocommerce/includes/admin/settings/class-wc-settings-point-of-sale.php
index da0c8a3e564..bfbd55f5ec5 100644
--- a/plugins/woocommerce/includes/admin/settings/class-wc-settings-point-of-sale.php
+++ b/plugins/woocommerce/includes/admin/settings/class-wc-settings-point-of-sale.php
@@ -7,9 +7,7 @@

 declare(strict_types=1);

-use Automattic\WooCommerce\Admin\Features\Features;
 use Automattic\WooCommerce\Internal\Settings\PointOfSaleDefaultSettings;
-use Automattic\WooCommerce\Utilities\FeaturesUtil;

 defined( 'ABSPATH' ) || exit;

@@ -30,8 +28,6 @@ class WC_Settings_Point_Of_Sale extends WC_Settings_Page {
 		$this->label = __( 'Point of Sale', 'woocommerce' );

 		parent::__construct();
-
-		add_filter( 'woocommerce_settings_tabs_array', array( $this, 'add_settings_page' ), 20 );
 	}

 	/**
@@ -41,22 +37,6 @@ class WC_Settings_Point_Of_Sale extends WC_Settings_Page {
 	 */
 	public $icon = 'store';

-	/**
-	 * Add Point of Sale page to settings if the feature is enabled.
-	 *
-	 * @param array $pages Existing pages.
-	 * @return array|mixed
-	 *
-	 * @internal For exclusive usage within this class, backwards compatibility not guaranteed.
-	 */
-	public function add_settings_page( $pages ) {
-		if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) {
-			return parent::add_settings_page( $pages );
-		} else {
-			return $pages;
-		}
-	}
-
 	/**
 	 * Get settings for the default section.
 	 *
diff --git a/plugins/woocommerce/includes/class-wc-emails.php b/plugins/woocommerce/includes/class-wc-emails.php
index b7fc234e24f..bcbd3c7a7f6 100644
--- a/plugins/woocommerce/includes/class-wc-emails.php
+++ b/plugins/woocommerce/includes/class-wc-emails.php
@@ -297,11 +297,9 @@ class WC_Emails {
 			'WC_Email_Customer_Reset_Password'       => __DIR__ . '/emails/class-wc-email-customer-reset-password.php',
 			'WC_Email_Customer_New_Account'          => __DIR__ . '/emails/class-wc-email-customer-new-account.php',
 			'WC_Email_Admin_Payment_Gateway_Enabled' => __DIR__ . '/emails/class-wc-email-admin-payment-gateway-enabled.php',
+			'WC_Email_Customer_POS_Completed_Order'  => __DIR__ . '/emails/class-wc-email-customer-pos-completed-order.php',
+			'WC_Email_Customer_POS_Refunded_Order'   => __DIR__ . '/emails/class-wc-email-customer-pos-refunded-order.php',
 		);
-		if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) {
-			$emails['WC_Email_Customer_POS_Completed_Order'] = __DIR__ . '/emails/class-wc-email-customer-pos-completed-order.php';
-			$emails['WC_Email_Customer_POS_Refunded_Order']  = __DIR__ . '/emails/class-wc-email-customer-pos-refunded-order.php';
-		}
 		if ( FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
 			$emails['WC_Email_Customer_Fulfillment_Created'] = __DIR__ . '/emails/class-wc-email-customer-fulfillment-created.php';
 			$emails['WC_Email_Customer_Fulfillment_Updated'] = __DIR__ . '/emails/class-wc-email-customer-fulfillment-updated.php';
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 8e333bc11f4..45ab3812aa9 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -335,6 +335,9 @@ class WC_Install {
 		'10.9.0'   => array(
 			'wc_update_1090_remove_task_list_reminder_bar_hidden_option',
 		),
+		'11.0.0'   => array(
+			'wc_update_1100_enable_point_of_sale_feature',
+		),
 	);

 	/**
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index e9ae31f42e5..0b8f37560c1 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -3544,3 +3544,18 @@ function wc_update_1080_backfill_email_template_sync_meta(): bool {
 function wc_update_1090_remove_task_list_reminder_bar_hidden_option() {
 	delete_option( 'woocommerce_task_list_reminder_bar_hidden' );
 }
+
+/**
+ * Set the stored value of the point_of_sale feature flag to enabled.
+ *
+ * The feature is deprecated as of 11.0.0 and always enabled in core, but the WooCommerce
+ * mobile apps read this option via the wc/v3 settings REST API to decide whether POS can
+ * be used, so the stored value must reflect the always-enabled state.
+ *
+ * @since 11.0.0
+ *
+ * @return void
+ */
+function wc_update_1100_enable_point_of_sale_feature() {
+	update_option( 'woocommerce_feature_point_of_sale_enabled', 'yes' );
+}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index abea0a1a209..461b2aab69d 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -2862,12 +2862,6 @@ parameters:
 			count: 1
 			path: includes/admin/class-wc-admin-settings.php

-		-
-			message: '#^Method WC_Admin_Settings\:\:reset_settings_pages_on_feature_change\(\) has no return type specified\.$#'
-			identifier: missingType.return
-			count: 1
-			path: includes/admin/class-wc-admin-settings.php
-
 		-
 			message: '#^Method WC_Admin_Settings\:\:save\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmails.php b/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmails.php
index 0d8f10b7278..168b71c6088 100644
--- a/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmails.php
+++ b/plugins/woocommerce/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmails.php
@@ -29,6 +29,8 @@ class WCTransactionalEmails {
 		'customer_new_account',
 		'customer_note',
 		'customer_on_hold_order',
+		'customer_pos_completed_order',
+		'customer_pos_refunded_order',
 		'customer_processing_order',
 		'customer_refunded_order',
 		'customer_partially_refunded_order',
@@ -71,11 +73,6 @@ class WCTransactionalEmails {
 	public static function get_core_transactional_emails() {
 		$emails = self::$core_transactional_emails;

-		if ( FeaturesUtil::feature_is_enabled( 'point_of_sale' ) ) {
-			$emails[] = 'customer_pos_completed_order';
-			$emails[] = 'customer_pos_refunded_order';
-		}
-
 		if ( FeaturesUtil::feature_is_enabled( 'fulfillments' ) ) {
 			$fulfillment_emails = array(
 				'customer_fulfillment_created',
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index 0354a8558e4..76cb1a11d4b 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -168,6 +168,7 @@ class FeaturesController {
 		add_action( self::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'display_email_improvements_feedback_notice' ), 10, 2 );
 		add_action( self::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'flag_abandoned_cart_recovery_enabled_notice' ), 10, 2 );
 		add_action( 'woocommerce_settings_advanced', array( $this, 'maybe_render_abandoned_cart_recovery_enabled_notice' ), 1 );
+		add_filter( 'woocommerce_settings-advanced', array( $this, 'add_point_of_sale_setting_for_rest_api' ), 10, 1 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
 	}

 	/**
@@ -548,20 +549,13 @@ class FeaturesController {
 					'Enable Point of Sale functionality in the WooCommerce mobile apps.',
 					'woocommerce'
 				),
+				'is_experimental'              => false,
 				'enabled_by_default'           => true,
-				'disable_ui'                   => false,
-
-				/*
-				* This is not truly a legacy feature (it is not a feature that pre-dates the FeaturesController),
-				* but we wish to handle compatibility checking in a similar fashion to legacy features. The
-				* rational for setting legacy to true is therefore similar to that of the 'order_attribution'
-				* feature.
-				*
-				* @see https://github.com/woocommerce/woocommerce/pull/39701#discussion_r1376976959
-				*/
+				'disable_ui'                   => true,
 				'skip_compatibility_checks'    => true,
 				'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
-				'is_experimental'              => true,
+				'deprecated_since'             => '11.0.0',
+				'deprecated_value'             => true,
 			),
 			'fulfillments'                       => array(
 				'name'                         => __( 'Order Fulfillments', 'woocommerce' ),
@@ -1336,6 +1330,34 @@ class FeaturesController {
 		return $sections;
 	}

+	/**
+	 * Handler for the 'woocommerce_settings-advanced' hook, which defines the settings
+	 * exposed in the wc/v3 settings REST API for the 'advanced' group. It appends the
+	 * Point of Sale feature flag setting.
+	 *
+	 * This is a compatibility shim for the WooCommerce mobile apps: app versions released
+	 * before the point_of_sale feature became always enabled (deprecated in 11.0.0) read and
+	 * write this setting via wc/v3/settings/advanced/woocommerce_feature_point_of_sale_enabled
+	 * to decide whether POS can be used. The setting is no longer rendered in the admin UI;
+	 * this shim can be removed once those app versions are no longer supported.
+	 *
+	 * @param array $settings The settings of the 'advanced' group, as exposed in the REST API.
+	 * @return array The updated settings array.
+	 *
+	 * @internal For exclusive usage of WooCommerce core, backwards compatibility not guaranteed.
+	 */
+	public function add_point_of_sale_setting_for_rest_api( $settings ): array {
+		$settings[] = array(
+			'id'          => 'woocommerce_feature_point_of_sale_enabled',
+			'option_key'  => 'woocommerce_feature_point_of_sale_enabled',
+			'label'       => __( 'Point of Sale', 'woocommerce' ),
+			'description' => __( 'Enable Point of Sale functionality in the WooCommerce mobile apps.', 'woocommerce' ),
+			'type'        => 'checkbox',
+			'default'     => 'yes',
+		);
+		return $settings;
+	}
+
 	/**
 	 * Handler for the 'woocommerce_get_settings_advanced' hook,
 	 * it adds the settings UI for all the existing features.
diff --git a/plugins/woocommerce/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySync.php b/plugins/woocommerce/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySync.php
index d88e0a85d5e..e6937cba37f 100644
--- a/plugins/woocommerce/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySync.php
+++ b/plugins/woocommerce/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySync.php
@@ -9,6 +9,8 @@ declare(strict_types=1);

 namespace Automattic\WooCommerce\Internal\ProductFeed\Integrations\POSCatalog;

+use Automattic\WooCommerce\Enums\ProductType;
+
 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
 }
@@ -36,6 +38,22 @@ class POSProductVisibilitySync {
 		add_action( 'woocommerce_new_product_variation', array( $this, 'inherit_parent_pos_visibility' ), 10, 2 );
 	}

+	/**
+	 * Whether POS visibility can be managed for the given product.
+	 *
+	 * Only simple and variable products that are not downloadable support the
+	 * "Available for POS" option; this is the single definition used by both the
+	 * product data meta box UI and the save logic.
+	 *
+	 * @since 11.0.0
+	 *
+	 * @param \WC_Product $product The product to check.
+	 * @return bool True if the product type supports POS visibility.
+	 */
+	public function is_product_supported( \WC_Product $product ): bool {
+		return $product->is_type( array( ProductType::SIMPLE, ProductType::VARIABLE ) ) && ! $product->is_downloadable();
+	}
+
 	/**
 	 * Set POS visibility for a product and its variations.
 	 *
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email-listing.spec.ts b/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email-listing.spec.ts
index 5c48a50a767..ce1c1e2250a 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email-listing.spec.ts
+++ b/plugins/woocommerce/tests/e2e-pw/tests/email/settings-email-listing.spec.ts
@@ -34,11 +34,6 @@ test.describe( 'WooCommerce Email Settings List View', () => {
 		baseURL,
 	} ) => {
 		await setBlockEmailEditorFeatureFlag( baseURL, 'yes' );
-		await setFeatureFlag(
-			baseURL,
-			'woocommerce_feature_point_of_sale_enabled',
-			'no'
-		);

 		// Navigate to WooCommerce Email Settings page
 		await page.goto( 'wp-admin/admin.php?page=wc-settings&tab=email' );
@@ -74,14 +69,16 @@ test.describe( 'WooCommerce Email Settings List View', () => {
 		// Check that Actions column exists
 		await expect( listViewLocator.getByText( 'Actions' ) ).toBeVisible();

-		// Check that first row shows Active status
-		const firstRow = listViewLocator.locator( 'tr' ).nth( 1 ); // nth(1) because nth(0) is header row
-		await expect( firstRow.locator( 'td' ).nth( 2 ) ).toHaveText(
+		// Target the "New order" row explicitly so the test is independent of list ordering.
+		const newOrderRow = listViewLocator.locator( 'tr', {
+			hasText: 'New order',
+		} );
+		await expect( newOrderRow.locator( 'td' ).nth( 2 ) ).toHaveText(
 			'Active'
 		);

-		// Open the first row more actions menu
-		await firstRow.locator( '.dataviews-all-actions-button' ).click();
+		// Open the row's more actions menu
+		await newOrderRow.locator( '.dataviews-all-actions-button' ).click();

 		// Check that the "Deactivate email" option is present and clickable
 		await expect(
@@ -92,12 +89,12 @@ test.describe( 'WooCommerce Email Settings List View', () => {
 			.click();

 		// Check that the email status is now Draft
-		await expect( firstRow.locator( 'td' ).nth( 2 ) ).toHaveText(
+		await expect( newOrderRow.locator( 'td' ).nth( 2 ) ).toHaveText(
 			'Inactive'
 		);

-		// Open the first row more actions menu again
-		await firstRow.locator( '.dataviews-all-actions-button' ).click();
+		// Open the row's more actions menu again
+		await newOrderRow.locator( '.dataviews-all-actions-button' ).click();

 		// Check that the "Activate email" option is present and clickable
 		await expect(
@@ -106,7 +103,7 @@ test.describe( 'WooCommerce Email Settings List View', () => {
 		await page.getByRole( 'menuitem', { name: 'Activate email' } ).click();

 		// Check that the email status is now Active again
-		await expect( firstRow.locator( 'td' ).nth( 2 ) ).toHaveText(
+		await expect( newOrderRow.locator( 'td' ).nth( 2 ) ).toHaveText(
 			'Active'
 		);

diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index f082213d01f..18e8ea2a1ea 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -323,4 +323,19 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
 		$this->assertSame( 'yes', get_option( 'woocommerce_analytics_scheduled_import' ) );
 		$this->assertFalse( get_option( 'woocommerce_analytics_immediate_import' ) );
 	}
+
+	/**
+	 * @testdox Migration sets the point_of_sale feature flag option to yes regardless of the previous value.
+	 */
+	public function test_wc_update_1100_enable_point_of_sale_feature(): void {
+		include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+		update_option( 'woocommerce_feature_point_of_sale_enabled', 'no' );
+		wc_update_1100_enable_point_of_sale_feature();
+		$this->assertSame( 'yes', get_option( 'woocommerce_feature_point_of_sale_enabled' ) );
+
+		delete_option( 'woocommerce_feature_point_of_sale_enabled' );
+		wc_update_1100_enable_point_of_sale_feature();
+		$this->assertSame( 'yes', get_option( 'woocommerce_feature_point_of_sale_enabled' ) );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailsTest.php b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailsTest.php
index 490549582a6..de6f5d95759 100644
--- a/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/EmailEditor/WCTransactionalEmails/WCTransactionalEmailsTest.php
@@ -54,6 +54,8 @@ class WCTransactionalEmailsTest extends \WC_Unit_Test_Case {
 		$this->assertContains( 'customer_new_account', $emails );
 		$this->assertContains( 'customer_completed_order', $emails );
 		$this->assertContains( 'customer_processing_order', $emails );
+		$this->assertContains( 'customer_pos_completed_order', $emails );
+		$this->assertContains( 'customer_pos_refunded_order', $emails );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
index 9973d23637c..fffcac03dd5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
@@ -1462,4 +1462,33 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
 		$this->assertNotContains( 'plugin/plugin.php', $compat_after['compatible'] );
 		$this->assertContains( 'plugin/plugin.php', $compat_after['uncertain'] );
 	}
+
+	/**
+	 * @testdox The point_of_sale feature is always enabled regardless of the stored option value.
+	 */
+	public function test_point_of_sale_feature_is_always_enabled(): void {
+		remove_action( 'woocommerce_register_feature_definitions', array( $this, 'register_dummy_features' ), 11 );
+
+		$sut = new FeaturesController();
+		$sut->init( wc_get_container()->get( LegacyProxy::class ), $this->fake_plugin_util );
+
+		update_option( 'woocommerce_feature_point_of_sale_enabled', 'no' );
+		$this->assertTrue( $sut->feature_is_enabled( 'point_of_sale' ), 'point_of_sale should be enabled even when the stored option is no' );
+
+		delete_option( 'woocommerce_feature_point_of_sale_enabled' );
+		$this->assertTrue( $sut->feature_is_enabled( 'point_of_sale' ), 'point_of_sale should be enabled when the stored option is absent' );
+	}
+
+	/**
+	 * @testdox The point_of_sale feature flag setting is exposed in the REST API advanced settings group for mobile app compatibility.
+	 */
+	public function test_point_of_sale_setting_is_added_for_rest_api(): void {
+		$settings = $this->sut->add_point_of_sale_setting_for_rest_api( array() );
+
+		$this->assertCount( 1, $settings );
+		$this->assertSame( 'woocommerce_feature_point_of_sale_enabled', $settings[0]['id'] );
+		$this->assertSame( 'woocommerce_feature_point_of_sale_enabled', $settings[0]['option_key'] );
+		$this->assertSame( 'checkbox', $settings[0]['type'] );
+		$this->assertSame( 'yes', $settings[0]['default'], 'The setting should default to yes so unset options read as enabled' );
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySyncTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySyncTest.php
index 06b390d4e34..355256faaa4 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySyncTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFeed/Integrations/POSCatalog/POSProductVisibilitySyncTest.php
@@ -202,4 +202,29 @@ class POSProductVisibilitySyncTest extends \WC_Unit_Test_Case {
 		$this->sut->set_product_pos_visibility( $product->get_id(), false );
 		remove_action( 'woocommerce_update_product_variation', array( $mock_callback, 'save' ) );
 	}
+
+	/**
+	 * @testdox Should report simple and variable products as supported for POS visibility.
+	 */
+	public function test_is_product_supported_returns_true_for_simple_and_variable_products(): void {
+		$simple   = WC_Helper_Product::create_simple_product();
+		$variable = WC_Helper_Product::create_variation_product();
+
+		$this->assertTrue( $this->sut->is_product_supported( $simple ), 'Simple products should be supported' );
+		$this->assertTrue( $this->sut->is_product_supported( $variable ), 'Variable products should be supported' );
+	}
+
+	/**
+	 * @testdox Should report downloadable, external and grouped products as not supported for POS visibility.
+	 */
+	public function test_is_product_supported_returns_false_for_unsupported_products(): void {
+		$downloadable = WC_Helper_Product::create_simple_product();
+		$downloadable->set_downloadable( true );
+		$external = WC_Helper_Product::create_external_product();
+		$grouped  = WC_Helper_Product::create_grouped_product();
+
+		$this->assertFalse( $this->sut->is_product_supported( $downloadable ), 'Downloadable products should not be supported' );
+		$this->assertFalse( $this->sut->is_product_supported( $external ), 'External products should not be supported' );
+		$this->assertFalse( $this->sut->is_product_supported( $grouped ), 'Grouped products should not be supported' );
+	}
 }