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