Commit 22c36b44b6a for woocommerce
commit 22c36b44b6aa13704562603bcc82eff01c7502a9
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Fri Mar 6 16:22:42 2026 +0000
Output incompatible extension data on front end if user is admin (#62242)
* Output incompatible extension data on front end if user is admin
* Create IncompatibleExtensionsFrontendNotice component
* Show IncompatibleExtensionsFrontendNotice on cart/checkout blocks
* eslint fix
* Add tests to cover notice addition
* Add tests to verify extension data isn't accidentally output
* Add changelog
* PHPStan fixes for changed lines
* Fix other phpstan errors in Checkout
* Apply suggestion from @senadir
* Use correct dirname for plugin ids
* Remove unncecessary useEffect
* Share notice dismissal between cart and checkout
* Lint fix after merge conflict
* Update tests after data structure change
---------
Co-authored-by: Seghir Nadir <nadir.seghir@gmail.com>
diff --git a/plugins/woocommerce/changelog/fix-move-notice-to-frontend b/plugins/woocommerce/changelog/fix-move-notice-to-frontend
new file mode 100644
index 00000000000..f45885c9b99
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-move-notice-to-frontend
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Add notice to Cart/Checkout blocks front-end to show notices about incompatible plugins to admin users.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/incompatible-extensions-notice.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/incompatible-extensions-notice.tsx
new file mode 100644
index 00000000000..a95e4572b41
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/incompatible-extensions-notice.tsx
@@ -0,0 +1,110 @@
+/**
+ * External dependencies
+ */
+import { __, sprintf } from '@wordpress/i18n';
+import { getSetting, CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
+import NoticeBanner from '@woocommerce/base-components/notice-banner';
+import { useLocalStorageState } from '@woocommerce/base-hooks';
+
+const areArraysEqual = ( a: string[], b: string[] ): boolean => {
+ if ( a.length !== b.length ) return false;
+ const unique = new Set( [ ...a, ...b ] );
+ return unique.size === a.length;
+};
+
+interface IncompatibleExtension {
+ id: string;
+ title: string;
+}
+
+const getIncompatibleExtensions = (): {
+ extensions: Record< string, string >;
+ slugs: string[];
+} => {
+ const extensions: Record< string, string > = {};
+ const data = getSetting< IncompatibleExtension[] >(
+ 'incompatibleExtensions',
+ []
+ );
+ data.forEach( ( ext ) => {
+ extensions[ ext.id ] = ext.title;
+ } );
+ return { extensions, slugs: Object.keys( extensions ) };
+};
+
+interface Props {
+ block: 'woocommerce/cart' | 'woocommerce/checkout';
+}
+
+/**
+ * Shows a notice to admin users on the frontend when there are incompatible extensions.
+ */
+export const IncompatibleExtensionsFrontendNotice = ( {
+ block,
+}: Props ): JSX.Element | null => {
+ const [ dismissedSlugs, setDismissedSlugs ] = useLocalStorageState<
+ string[]
+ >( 'wc-blocks_dismissed_incompatible_extensions_notices', [] );
+
+ const { extensions, slugs } = getIncompatibleExtensions();
+ const count = slugs.length;
+
+ const isDismissedAndUpToDate = areArraysEqual( dismissedSlugs, slugs );
+
+ const shouldShow =
+ CURRENT_USER_IS_ADMIN && count > 0 && ! isDismissedAndUpToDate;
+
+ if ( ! shouldShow ) {
+ return null;
+ }
+
+ const dismissNotice = () => {
+ setDismissedSlugs( slugs );
+ };
+
+ const extensionNames = Object.values( extensions );
+ const blockLabel =
+ block === 'woocommerce/cart'
+ ? __( 'Cart', 'woocommerce' )
+ : __( 'Checkout', 'woocommerce' );
+
+ const message =
+ count === 1
+ ? sprintf(
+ /* translators: %1$s is extension name, %2$s is block name */
+ __(
+ '%1$s may not be compatible with the %2$s block.',
+ 'woocommerce'
+ ),
+ extensionNames[ 0 ],
+ blockLabel
+ )
+ : sprintf(
+ /* translators: %s is block name */
+ __(
+ 'Some extensions may not be compatible with the %s block:',
+ 'woocommerce'
+ ),
+ blockLabel
+ );
+
+ return (
+ <NoticeBanner
+ status="warning"
+ isDismissible={ true }
+ onRemove={ dismissNotice }
+ >
+ { message }
+ { count > 1 && (
+ <ul style={ { margin: '0.5em 0 0 1.5em', padding: 0 } }>
+ { extensionNames.map( ( name ) => (
+ <li key={ name }>{ name }</li>
+ ) ) }
+ </ul>
+ ) }
+ <em>
+ { __( '(Only administrators see this notice)', 'woocommerce' ) }
+ </em>
+ </NoticeBanner>
+ );
+};
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/test/incompatible-extensions-notice.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/test/incompatible-extensions-notice.tsx
new file mode 100644
index 00000000000..1dbafe10ce3
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart-checkout-shared/test/incompatible-extensions-notice.tsx
@@ -0,0 +1,218 @@
+/**
+ * External dependencies
+ */
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { getSetting } from '@woocommerce/settings';
+import { useLocalStorageState } from '@woocommerce/base-hooks';
+
+/**
+ * Internal dependencies
+ */
+import { IncompatibleExtensionsFrontendNotice } from '../incompatible-extensions-notice';
+
+jest.mock( '@woocommerce/settings', () => ( {
+ getSetting: jest.fn(),
+ CURRENT_USER_IS_ADMIN: true,
+} ) );
+
+jest.mock( '@woocommerce/base-hooks', () => ( {
+ useLocalStorageState: jest.fn(),
+} ) );
+
+jest.mock( '@woocommerce/base-components/notice-banner', () => ( {
+ __esModule: true,
+ default: ( {
+ children,
+ onRemove,
+ status,
+ }: {
+ children: React.ReactNode;
+ onRemove: () => void;
+ status: string;
+ } ) => (
+ <div data-testid="notice-banner" data-status={ status }>
+ { children }
+ <button onClick={ onRemove } data-testid="dismiss-button">
+ Dismiss
+ </button>
+ </div>
+ ),
+} ) );
+
+const mockGetSetting = getSetting as jest.MockedFunction< typeof getSetting >;
+const mockUseLocalStorageState = useLocalStorageState as jest.MockedFunction<
+ typeof useLocalStorageState
+>;
+
+describe( 'IncompatibleExtensionsFrontendNotice', () => {
+ beforeEach( () => {
+ jest.clearAllMocks();
+ mockUseLocalStorageState.mockReturnValue( [ [], jest.fn() ] );
+ } );
+
+ // Note: Testing CURRENT_USER_IS_ADMIN=false requires module re-mocking which
+ // conflicts with testing-library hooks. The admin check is a simple boolean
+ // guard at the top of the component, so we rely on the other tests to verify
+ // the component works correctly when the admin check passes.
+
+ describe( 'when there are no incompatible extensions', () => {
+ beforeEach( () => {
+ mockGetSetting.mockReturnValue( [] );
+ } );
+
+ it( 'should not render', () => {
+ const { container } = render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+ expect( container ).toBeEmptyDOMElement();
+ expect(
+ screen.queryByText(
+ 'may not be compatible with the Checkout block'
+ )
+ ).not.toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'when there is one incompatible extension', () => {
+ beforeEach( () => {
+ mockGetSetting.mockReturnValue( [
+ { id: 'test-plugin', title: 'Test Plugin' },
+ ] );
+ } );
+
+ it( 'should render notice with extension name for checkout', () => {
+ render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+
+ expect( screen.getByTestId( 'notice-banner' ) ).toBeInTheDocument();
+ expect( screen.getByTestId( 'notice-banner' ) ).toHaveAttribute(
+ 'data-status',
+ 'warning'
+ );
+ expect(
+ screen.getByText(
+ /Test Plugin may not be compatible with the Checkout block/
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText( /Only administrators see this notice/ )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should render notice with extension name for cart', () => {
+ render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/cart" />
+ );
+
+ expect(
+ screen.getByText(
+ /Test Plugin may not be compatible with the Cart block/
+ )
+ ).toBeInTheDocument();
+ } );
+
+ it( 'should not render a list', () => {
+ render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+
+ expect( screen.queryByRole( 'list' ) ).not.toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'when there are multiple incompatible extensions', () => {
+ beforeEach( () => {
+ mockGetSetting.mockReturnValue( [
+ { id: 'plugin-one', title: 'Plugin One' },
+ { id: 'plugin-two', title: 'Plugin Two' },
+ ] );
+ } );
+
+ it( 'should render notice with list of extensions', () => {
+ render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+
+ expect(
+ screen.getByText(
+ /Some extensions may not be compatible with the Checkout block/
+ )
+ ).toBeInTheDocument();
+ expect( screen.getByRole( 'list' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Plugin One' ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Plugin Two' ) ).toBeInTheDocument();
+ } );
+ } );
+
+ describe( 'dismissal behavior', () => {
+ const mockSetDismissedNotices = jest.fn();
+
+ beforeEach( () => {
+ mockGetSetting.mockReturnValue( [
+ { id: 'test-plugin', title: 'Test Plugin' },
+ ] );
+ mockUseLocalStorageState.mockReturnValue( [
+ [],
+ mockSetDismissedNotices,
+ ] );
+ } );
+
+ it( 'should call setDismissedNotices when dismissed', async () => {
+ const user = userEvent.setup();
+ render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+
+ await user.click( screen.getByTestId( 'dismiss-button' ) );
+
+ expect( mockSetDismissedNotices ).toHaveBeenCalledWith( [
+ 'test-plugin',
+ ] );
+ } );
+
+ it( 'should not render when already dismissed with same extensions', () => {
+ mockUseLocalStorageState.mockReturnValue( [
+ [ 'test-plugin' ],
+ mockSetDismissedNotices,
+ ] );
+
+ const { container } = render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+
+ expect( container ).toBeEmptyDOMElement();
+ } );
+
+ it( 'should render when dismissed but extensions changed', () => {
+ mockGetSetting.mockReturnValue( [
+ { id: 'test-plugin', title: 'Test Plugin' },
+ { id: 'new-plugin', title: 'New Plugin' },
+ ] );
+ mockUseLocalStorageState.mockReturnValue( [
+ [ 'test-plugin' ],
+ mockSetDismissedNotices,
+ ] );
+
+ render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
+ );
+
+ expect( screen.getByTestId( 'notice-banner' ) ).toBeInTheDocument();
+ } );
+
+ it( 'should not render for cart when notice is dismissed (shared dismissal)', () => {
+ mockUseLocalStorageState.mockReturnValue( [
+ [ 'test-plugin' ],
+ mockSetDismissedNotices,
+ ] );
+
+ const { container } = render(
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/cart" />
+ );
+
+ expect( container ).toBeEmptyDOMElement();
+ } );
+ } );
+} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/block.js b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/block.js
index 178f0603c72..2f42502cf37 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/block.js
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/block.js
@@ -20,6 +20,7 @@ import { reloadPage } from '@woocommerce/blocks/checkout/utils';
* Internal dependencies
*/
import { CartBlockContext } from './context';
+import { IncompatibleExtensionsFrontendNotice } from '../cart-checkout-shared/incompatible-extensions-notice';
import './style.scss';
const Cart = ( { children, attributes = {} } ) => {
@@ -79,6 +80,7 @@ const Block = ( { attributes, children, scrollToTop } ) => (
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreNoticesContainer context={ noticeContexts.CART } />
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/cart" />
<SlotFillProvider>
<CartProvider>
<CartEventsProvider>
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/block.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/block.tsx
index 5a156176e35..6d9d295366d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/block.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/checkout/block.tsx
@@ -27,6 +27,7 @@ import CheckoutOrderError from './checkout-order-error';
import { LOGIN_TO_CHECKOUT_URL, isLoginRequired, reloadPage } from './utils';
import type { Attributes } from './types';
import { CheckoutBlockContext } from './context';
+import { IncompatibleExtensionsFrontendNotice } from '../cart-checkout-shared/incompatible-extensions-notice';
const MustLoginPrompt = () => {
return (
@@ -165,6 +166,7 @@ const Block = ( {
<StoreNoticesContainer
context={ [ noticeContexts.CHECKOUT, noticeContexts.CART ] }
/>
+ <IncompatibleExtensionsFrontendNotice block="woocommerce/checkout" />
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
<SlotFillProvider>
<CheckoutProvider>
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
index deba60cd3c1..cd3973ae2cd 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
@@ -244,6 +244,31 @@ class Cart extends AbstractBlock {
$this->asset_data_registry->add( 'isBlockTheme', wp_is_block_theme() );
$this->asset_data_registry->add( 'shippingMethodsExist', CartCheckoutUtils::shipping_methods_exist() > 0 );
+ $is_block_editor = $this->is_block_editor();
+
+ // Check `current_user_can` so we can show notices about incompatible extensions in the front-end to admins too.
+ if ( ( $is_block_editor || current_user_can( 'manage_woocommerce' ) ) && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) {
+ if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) && function_exists( 'get_plugins' ) ) {
+ $declared_extensions = \Automattic\WooCommerce\Utilities\FeaturesUtil::get_compatible_plugins_for_feature( 'cart_checkout_blocks' );
+ $all_plugins = \get_plugins();
+ $incompatible_extensions = array_reduce(
+ $declared_extensions['incompatible'],
+ function ( array $acc, $item ) use ( $all_plugins ) {
+ $plugin = $all_plugins[ $item ] ?? null;
+ $plugin_id = $plugin['TextDomain'] ?? dirname( $item );
+ $plugin_name = $plugin['Name'] ?? $plugin_id;
+ $acc[] = [
+ 'id' => $plugin_id,
+ 'title' => $plugin_name,
+ ];
+ return $acc;
+ },
+ []
+ );
+ $this->asset_data_registry->add( 'incompatibleExtensions', $incompatible_extensions );
+ }
+ }
+
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
index 4cbc98d0ef0..c7ed5870b08 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
@@ -499,7 +499,7 @@ class Checkout extends AbstractBlock {
$shipping_methods = WC()->shipping()->get_shipping_methods();
$formatted_shipping_methods = array_reduce(
$shipping_methods,
- function ( $acc, $method ) use ( $local_pickup_method_ids ) {
+ function ( array $acc, $method ) use ( $local_pickup_method_ids ) {
if ( in_array( $method->id, $local_pickup_method_ids, true ) ) {
return $acc;
}
@@ -527,7 +527,7 @@ class Checkout extends AbstractBlock {
$payment_methods = PaymentUtils::get_enabled_payment_gateways();
$formatted_payment_methods = array_reduce(
$payment_methods,
- function ( $acc, $method ) {
+ function ( array $acc, $method ) {
$acc[] = [
'id' => $method->id,
'title' => $method->get_method_title() !== '' ? $method->get_method_title() : $method->get_title(),
@@ -540,7 +540,8 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods );
}
- if ( $is_block_editor && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) {
+ // Check `current_user_can` so we can show notices about incompatible extensions in the front-end to admins too.
+ if ( ( $is_block_editor || current_user_can( 'manage_woocommerce' ) ) && ! $this->asset_data_registry->exists( 'incompatibleExtensions' ) ) {
if ( ! class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) || ! function_exists( 'get_plugins' ) ) {
return;
}
@@ -549,9 +550,9 @@ class Checkout extends AbstractBlock {
$all_plugins = \get_plugins(); // Note that `get_compatible_plugins_for_feature` calls `get_plugins` internally, so this is already in cache.
$incompatible_extensions = array_reduce(
$declared_extensions['incompatible'],
- function ( $acc, $item ) use ( $all_plugins ) {
+ function ( array $acc, $item ) use ( $all_plugins ) {
$plugin = $all_plugins[ $item ] ?? null;
- $plugin_id = $plugin['TextDomain'] ?? dirname( $item, 2 );
+ $plugin_id = $plugin['TextDomain'] ?? dirname( $item );
$plugin_name = $plugin['Name'] ?? $plugin_id;
$acc[] = [
'id' => $plugin_id,
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CartCheckoutIncompatibleExtensionsTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CartCheckoutIncompatibleExtensionsTest.php
new file mode 100644
index 00000000000..12279332f85
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/CartCheckoutIncompatibleExtensionsTest.php
@@ -0,0 +1,246 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use Automattic\WooCommerce\Blocks\Assets\Api;
+use Automattic\WooCommerce\Blocks\BlockTypes\Cart;
+use Automattic\WooCommerce\Blocks\BlockTypes\Checkout;
+use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
+use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AssetDataRegistryMock;
+use WP_UnitTestCase_Base;
+
+/**
+ * Tests for incompatibleExtensions data registration in Cart and Checkout blocks.
+ */
+class CartCheckoutIncompatibleExtensionsTest extends \WP_UnitTestCase {
+
+ /**
+ * Asset data registry mock.
+ *
+ * @var AssetDataRegistryMock
+ */
+ private $asset_data_registry;
+
+ /**
+ * Admin user ID.
+ *
+ * @var int
+ */
+ private $admin_id;
+
+ /**
+ * Customer user ID.
+ *
+ * @var int
+ */
+ private $customer_id;
+
+ /**
+ * Set up the test.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $asset_api = Package::container()->get( Api::class );
+ $this->asset_data_registry = new AssetDataRegistryMock( $asset_api );
+
+ $this->admin_id = WP_UnitTestCase_Base::factory()->user->create( array( 'role' => 'administrator' ) );
+ $this->customer_id = WP_UnitTestCase_Base::factory()->user->create( array( 'role' => 'customer' ) );
+ }
+
+ /**
+ * Clean up after the test.
+ *
+ * @return void
+ */
+ protected function tearDown(): void {
+ wp_set_current_user( 0 );
+ wp_delete_user( $this->admin_id );
+ wp_delete_user( $this->customer_id );
+ parent::tearDown();
+ }
+
+ /**
+ * Creates a Cart block instance for testing.
+ *
+ * @return Cart
+ */
+ private function create_cart_block(): Cart {
+ $asset_api = Package::container()->get( Api::class );
+ $integration_registry = new IntegrationRegistry();
+
+ // Create an anonymous subclass that skips initialization and exposes enqueue_data.
+ return new class( $asset_api, $this->asset_data_registry, $integration_registry ) extends Cart {
+ /**
+ * Skip block registration for unit tests.
+ */
+ protected function initialize() {}
+
+ /**
+ * Expose enqueue_data for testing.
+ *
+ * @param array $attributes Block attributes.
+ * @return void
+ */
+ public function test_enqueue_data( array $attributes = array() ): void {
+ $this->enqueue_data( $attributes );
+ }
+
+ /**
+ * Mock is_block_editor to return false (simulate frontend).
+ *
+ * @return bool
+ */
+ protected function is_block_editor(): bool {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Creates a Checkout block instance for testing.
+ *
+ * @return Checkout
+ */
+ private function create_checkout_block(): Checkout {
+ $asset_api = Package::container()->get( Api::class );
+ $integration_registry = new IntegrationRegistry();
+
+ // Create an anonymous subclass that skips initialization and exposes enqueue_data.
+ return new class( $asset_api, $this->asset_data_registry, $integration_registry ) extends Checkout {
+ /**
+ * Skip block registration for unit tests.
+ */
+ protected function initialize() {}
+
+ /**
+ * Expose enqueue_data for testing.
+ *
+ * @param array $attributes Block attributes.
+ * @return void
+ */
+ public function test_enqueue_data( array $attributes = array() ): void {
+ $this->enqueue_data( $attributes );
+ }
+
+ /**
+ * Mock is_block_editor to return false (simulate frontend).
+ *
+ * @return bool
+ */
+ protected function is_block_editor(): bool {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Test that incompatibleExtensions is registered for admin users on Cart frontend.
+ *
+ * This test verifies that admins have access to the incompatibleExtensions data
+ * on the frontend. We only check that the key exists (capability check),
+ * not the specific plugin data (that's FeaturesUtil's responsibility).
+ *
+ * @return void
+ */
+ public function test_cart_registers_incompatible_extensions_for_admin(): void {
+ wp_set_current_user( $this->admin_id );
+
+ $cart = $this->create_cart_block();
+ $cart->test_enqueue_data();
+
+ $data = $this->asset_data_registry->get();
+
+ $this->assertArrayHasKey( 'incompatibleExtensions', $data );
+ $this->assertIsArray( $data['incompatibleExtensions'] );
+ }
+
+ /**
+ * Test that incompatibleExtensions is NOT registered for customer users on Cart frontend.
+ *
+ * @return void
+ */
+ public function test_cart_does_not_register_incompatible_extensions_for_customer(): void {
+ wp_set_current_user( $this->customer_id );
+
+ $cart = $this->create_cart_block();
+ $cart->test_enqueue_data();
+
+ $data = $this->asset_data_registry->get();
+
+ $this->assertArrayNotHasKey( 'incompatibleExtensions', $data );
+ }
+
+ /**
+ * Test that incompatibleExtensions is NOT registered for logged out users on Cart frontend.
+ *
+ * @return void
+ */
+ public function test_cart_does_not_register_incompatible_extensions_for_guest(): void {
+ wp_set_current_user( 0 );
+
+ $cart = $this->create_cart_block();
+ $cart->test_enqueue_data();
+
+ $data = $this->asset_data_registry->get();
+
+ $this->assertArrayNotHasKey( 'incompatibleExtensions', $data );
+ }
+
+ /**
+ * Test that incompatibleExtensions is registered for admin users on Checkout frontend.
+ *
+ * This test verifies that admins have access to the incompatibleExtensions data
+ * on the frontend. We only check that the key exists (capability check),
+ * not the specific plugin data (that's FeaturesUtil's responsibility).
+ *
+ * @return void
+ */
+ public function test_checkout_registers_incompatible_extensions_for_admin(): void {
+ wp_set_current_user( $this->admin_id );
+
+ $checkout = $this->create_checkout_block();
+ $checkout->test_enqueue_data();
+
+ $data = $this->asset_data_registry->get();
+
+ $this->assertArrayHasKey( 'incompatibleExtensions', $data );
+ $this->assertIsArray( $data['incompatibleExtensions'] );
+ }
+
+ /**
+ * Test that incompatibleExtensions is NOT registered for customer users on Checkout frontend.
+ *
+ * @return void
+ */
+ public function test_checkout_does_not_register_incompatible_extensions_for_customer(): void {
+ wp_set_current_user( $this->customer_id );
+
+ $checkout = $this->create_checkout_block();
+ $checkout->test_enqueue_data();
+
+ $data = $this->asset_data_registry->get();
+
+ $this->assertArrayNotHasKey( 'incompatibleExtensions', $data );
+ }
+
+ /**
+ * Test that incompatibleExtensions is NOT registered for logged out users on Checkout frontend.
+ *
+ * @return void
+ */
+ public function test_checkout_does_not_register_incompatible_extensions_for_guest(): void {
+ wp_set_current_user( 0 );
+
+ $checkout = $this->create_checkout_block();
+ $checkout->test_enqueue_data();
+
+ $data = $this->asset_data_registry->get();
+
+ $this->assertArrayNotHasKey( 'incompatibleExtensions', $data );
+ }
+}