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' );
+ }
}