Commit c00eaa3c21 for woocommerce

commit c00eaa3c21fb94fc309be30772212601efa409bd
Author: Amit Raj <77401999+amitraj2203@users.noreply.github.com>
Date:   Fri Apr 25 15:10:30 2025 +0530

    [Experimental] Blockified Update Quantity Selector and Grouped Product Selector to use Stepper mode only (#57442)

    * refactor: remove input mode from quantity selector, allowing only stepper mode

    * feat: Add stepper mode and remove input mode

    * feat: Add changelog message

    * refactor: Quantity input handling in Add to Cart blocks to utilize utility methods for improved maintainability and readability

    * refactor: Consolidate quantity handling logic and remove redundant frontend file

    * refactor: Remove redundant quantity selector styles and consolidate into main style file

    * refactor: Introduce QuantitySelector component to consolidate the component between different blocks

    * refactor: Remove unused style import from quantity selector component

    * refactor: Update description in quantity selector block to clarify input functionality

    * refactor: Simplify type assertion for input element in getInputElementFromEvent function

    * refactor: Rename quantity action methods for consistency and clarity in AddtoCartWithOptions block

    * refactor: Consolidate quantity selector styles and remove unused files

    * refactor: Wrap all the styles in this file with the two blocks parent classes

    * refactor: Simplify quantity selector style assignment in downgrade notice

    * refactor: Remove unused script enqueue method from grouped product selector CTA

    * fix: Add check for $previous_product is an instance of WC_Product.

    * refactor: Remove isSiteEditor prop from QuantityStepper component

    * refactor: Simplify product retrieval in context methods and update validation logic

    * fix: Correct namespace casing for AddtoCartWithOptions in relevant files

    * fix: Linitng error

    ---------

    Co-authored-by: Vladimir Reznichenko <kalessil@gmail.com>

diff --git a/plugins/woocommerce/changelog/57442-quantity-selector-stepper-mode b/plugins/woocommerce/changelog/57442-quantity-selector-stepper-mode
new file mode 100644
index 0000000000..45fe20d7bb
--- /dev/null
+++ b/plugins/woocommerce/changelog/57442-quantity-selector-stepper-mode
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Update Quantity Selector and Grouped Product Selector Item CTA blocks to have only Stepper mode.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx
index 936819db5c..b419adde14 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/downgrade-notice/index.tsx
@@ -25,17 +25,8 @@ const downgradeToClassicAddToCartWithOptions = ( blockClientId: string ) => {
 		return false;
 	}

-	const foundQuantitySelectorBlock = findBlock( {
-		blocks,
-		findCondition: ( block ) =>
-			block.name ===
-			'woocommerce/add-to-cart-with-options-quantity-selector',
-	} );
-
 	const newBlock = createBlock( 'woocommerce/add-to-cart-form', {
-		quantitySelectorStyle:
-			foundQuantitySelectorBlock?.attributes?.quantitySelectorStyle ||
-			'input',
+		quantitySelectorStyle: 'input',
 	} );

 	dispatch( 'core/block-editor' ).replaceBlock(
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/quantity-stepper/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/quantity-stepper/index.tsx
new file mode 100644
index 0000000000..a7a96a7998
--- /dev/null
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/components/quantity-stepper/index.tsx
@@ -0,0 +1,20 @@
+const QuantityStepper = () => {
+	return (
+		<div className="quantity wc-block-components-quantity-selector">
+			<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">
+				-
+			</button>
+			<input
+				type="number"
+				value="1"
+				className="input-text qty text"
+				readOnly
+			/>
+			<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">
+				+
+			</button>
+		</div>
+	);
+};
+
+export default QuantityStepper;
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss
index 8d9d2f4bf7..9c2705234e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/editor.scss
@@ -28,3 +28,15 @@
 		color: inherit;
 	}
 }
+
+.wc-block-add-to-cart-with-options-grouped-product-selector-item-cta,
+.wc-block-add-to-cart-with-options__quantity-selector {
+	.quantity .qty {
+		// WooCommerce core styles: https://github.com/woocommerce/woocommerce/blob/c9f62609155825cd817976c7611b9b0415e90f2f/plugins/woocommerce/client/legacy/css/woocommerce.scss/#L111-L112
+		width: 3.631em;
+	}
+
+	.wc-block-components-quantity-selector {
+		margin: 0;
+	}
+}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
index ab43061bc1..efd899cc2a 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/frontend.ts
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import type { FormEvent } from 'react';
+import type { FormEvent, HTMLElementEvent } from 'react';
 import { store, getContext } from '@wordpress/interactivity';
 import type { Store as WooCommerce } from '@woocommerce/stores/woocommerce/cart';
 import type { CartVariationItem } from '@woocommerce/types';
@@ -23,6 +23,49 @@ const { state: wooState } = store< WooCommerce >(
 	{ lock: universalLock }
 );

+const getInputElementFromEvent = (
+	event: HTMLElementEvent< HTMLButtonElement >
+) => {
+	const target = event.target as HTMLButtonElement;
+
+	const inputElement = target.parentElement?.querySelector(
+		'.input-text.qty.text'
+	) as HTMLInputElement | null;
+
+	return inputElement;
+};
+
+const getInputData = ( event: HTMLElementEvent< HTMLButtonElement > ) => {
+	const inputElement = getInputElementFromEvent( event );
+
+	if ( ! inputElement ) {
+		return;
+	}
+
+	const parsedValue = parseInt( inputElement.value, 10 );
+	const parsedMinValue = parseInt( inputElement.min, 10 );
+	const parsedMaxValue = parseInt( inputElement.max, 10 );
+	const parsedStep = parseInt( inputElement.step, 10 );
+
+	const currentValue = isNaN( parsedValue ) ? 0 : parsedValue;
+	const minValue = isNaN( parsedMinValue ) ? 1 : parsedMinValue;
+	const maxValue = isNaN( parsedMaxValue ) ? undefined : parsedMaxValue;
+	const step = isNaN( parsedStep ) ? 1 : parsedStep;
+
+	return {
+		currentValue,
+		minValue,
+		maxValue,
+		step,
+		inputElement,
+	};
+};
+
+const dispatchChangeEvent = ( inputElement: HTMLInputElement ) => {
+	const event = new Event( 'change' );
+	inputElement.dispatchEvent( event );
+};
+
 const addToCartWithOptionsStore = store(
 	'woocommerce/add-to-cart-with-options',
 	{
@@ -57,6 +100,40 @@ const addToCartWithOptionsStore = store(
 					context.variation.splice( index, 1 );
 				}
 			},
+			increaseQuantity: (
+				event: HTMLElementEvent< HTMLButtonElement >
+			) => {
+				const inputData = getInputData( event );
+				if ( ! inputData ) {
+					return;
+				}
+				const { currentValue, maxValue, step, inputElement } =
+					inputData;
+				const newValue = currentValue + step;
+
+				if ( maxValue === undefined || newValue <= maxValue ) {
+					addToCartWithOptionsStore.actions.setQuantity( newValue );
+					inputElement.value = newValue.toString();
+					dispatchChangeEvent( inputElement );
+				}
+			},
+			decreaseQuantity: (
+				event: HTMLElementEvent< HTMLButtonElement >
+			) => {
+				const inputData = getInputData( event );
+				if ( ! inputData ) {
+					return;
+				}
+				const { currentValue, minValue, step, inputElement } =
+					inputData;
+				const newValue = currentValue - step;
+
+				if ( newValue >= minValue ) {
+					addToCartWithOptionsStore.actions.setQuantity( newValue );
+					inputElement.value = newValue.toString();
+					dispatchChangeEvent( inputElement );
+				}
+			},
 			*handleSubmit( event: FormEvent< HTMLFormElement > ) {
 				event.preventDefault();

diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx
index d970922bd8..d5139c590b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/edit.tsx
@@ -3,17 +3,16 @@
  */
 import { useProductDataContext } from '@woocommerce/shared-context';
 import { Disabled, Spinner } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
-import { isSiteEditorPage } from '@woocommerce/utils';
 import { useBlockProps } from '@wordpress/block-editor';
 import { __ } from '@wordpress/i18n';

+/**
+ * Internal dependencies
+ */
+import QuantityStepper from '../../components/quantity-stepper';
+
 const CTA = () => {
 	const { isLoading, product } = useProductDataContext();
-	const isSiteEditor = useSelect(
-		( select ) => isSiteEditorPage( select( 'core/edit-site' ) ),
-		[]
-	);

 	if ( isLoading ) {
 		return <Spinner />;
@@ -38,28 +37,7 @@ const CTA = () => {
 				/>
 			);
 		}
-		return (
-			<div className="quantity">
-				<input
-					style={
-						// In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
-						! isSiteEditor
-							? {
-									backgroundColor: '#ffffff',
-									lineHeight: 'normal',
-									minHeight: 'unset',
-									boxSizing: 'unset',
-									borderRadius: 'unset',
-							  }
-							: {}
-					}
-					type="number"
-					value="1"
-					className="input-text qty text"
-					readOnly
-				/>
-			</div>
-		);
+		return <QuantityStepper />;
 	}

 	return (
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss
index d2efa0ca32..3b58b72fdc 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/grouped-product-selector/product-item-cta/style.scss
@@ -4,14 +4,4 @@
 	.button {
 		display: block;
 	}
-
-	/**
-	* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
-	*
-	* @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
-	*/
-	.quantity .input-text {
-		font-size: var(--wp--preset--font-size--small);
-		padding: 0.9rem 1.1rem;
-	}
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx
index 53b2a3ad30..da19261136 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/index.tsx
@@ -13,6 +13,8 @@ import ProductTypeSelectorPlugin from './plugins';
 import metadata from './block.json';
 import AddToCartOptionsEdit from './edit';
 import { shouldBlockifiedAddToCartWithOptionsBeRegistered } from './utils';
+import '../../base/components/quantity-selector/style.scss';
+import './editor.scss';
 import './style.scss';
 import type { Attributes } from './types';

diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json
index 0352471572..57511ab4d5 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/block.json
@@ -1,15 +1,8 @@
 {
 	"name": "woocommerce/add-to-cart-with-options-quantity-selector",
 	"title": "Quantity Selector (Experimental)",
-	"description": "Display an input field to select the number of products to add to cart.",
+	"description": "Display an input field with stepper to select the number of products to add to cart.",
 	"category": "woocommerce-product-elements",
-	"attributes": {
-		"quantitySelectorStyle": {
-			"type": "string",
-			"enum": [ "input", "stepper" ],
-			"default": "input"
-		}
-	},
 	"keywords": [ "WooCommerce" ],
 	"usesContext": [ "postId" ],
 	"ancestor": [ "woocommerce/add-to-cart-with-options" ],
@@ -18,8 +11,5 @@
 	"supports": {
 		"interactivity": true
 	},
-	"$schema": "https://schemas.wp.org/trunk/block.json",
-	"viewScriptModule": "woocommerce/add-to-cart-with-options-quantity-selector",
-	"style": "file:../woocommerce/add-to-cart-with-options-quantity-selector-style.css",
-	"editorStyle": "file:../woocommerce/add-to-cart-with-options-quantity-selector-editor.css"
+	"$schema": "https://schemas.wp.org/trunk/block.json"
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx
index 7a8a628dcb..20acce4165 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/edit.tsx
@@ -1,153 +1,25 @@
 /**
  * External dependencies
  */
-import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
-import { __ } from '@wordpress/i18n';
-import { BlockEditProps } from '@wordpress/blocks';
-import { useSelect } from '@wordpress/data';
-import { isSiteEditorPage } from '@woocommerce/utils';
-import {
-	Disabled,
-	PanelBody,
-	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-	// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
-	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
-	__experimentalToggleGroupControl as ToggleGroupControl,
-	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-	// @ts-ignore - Ignoring because `__experimentalToggleGroupControl` is not yet in the type definitions.
-	// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
-	__experimentalToggleGroupControlOption as ToggleGroupControlOption,
-} from '@wordpress/components';
+import { useBlockProps } from '@wordpress/block-editor';
+import { Disabled } from '@wordpress/components';

-enum QuantitySelectorStyle {
-	Input = 'input',
-	Stepper = 'stepper',
-}
-
-interface Attributes {
-	className?: string;
-	quantitySelectorStyle: QuantitySelectorStyle;
-}
-
-const getHelpText = ( quantitySelectorStyle: QuantitySelectorStyle ) => {
-	if ( quantitySelectorStyle === QuantitySelectorStyle.Input ) {
-		return __(
-			'Shoppers can enter a number of items to add to cart.',
-			'woocommerce'
-		);
-	}
-	if ( quantitySelectorStyle === QuantitySelectorStyle.Stepper ) {
-		return __(
-			'Shoppers can use buttons to change the number of items to add to cart.',
-			'woocommerce'
-		);
-	}
-};
-
-const AddToCartWithOptionsQuantitySelectorEdit = (
-	props: BlockEditProps< Attributes >
-) => {
-	const { setAttributes } = props;
-	const { quantitySelectorStyle } = props.attributes;
-
-	const quantitySelectorStyleClass =
-		quantitySelectorStyle === QuantitySelectorStyle.Input
-			? 'wc-block-add-to-cart-with-options__quantity-selector--input'
-			: 'wc-block-add-to-cart-with-options__quantity-selector--stepper';
+/**
+ * Internal dependencies
+ */
+import QuantityStepper from '../components/quantity-stepper';

+const AddToCartWithOptionsQuantitySelectorEdit = () => {
 	const blockProps = useBlockProps( {
-		className: `wc-block-add-to-cart-with-options__quantity-selector ${ quantitySelectorStyleClass }`,
+		className: 'wc-block-add-to-cart-with-options__quantity-selector',
 	} );

-	const isSiteEditor = useSelect(
-		( select ) => isSiteEditorPage( select( 'core/edit-site' ) ),
-		[]
-	);
-
 	return (
-		<>
-			<InspectorControls>
-				<PanelBody title={ __( 'Settings', 'woocommerce' ) }>
-					<ToggleGroupControl
-						className="wc-block-editor-add-to-cart-with-options__quantity-selector"
-						__nextHasNoMarginBottom
-						value={ quantitySelectorStyle }
-						isBlock
-						onChange={ ( value: QuantitySelectorStyle ) => {
-							setAttributes( {
-								quantitySelectorStyle:
-									value as QuantitySelectorStyle,
-							} );
-						} }
-						help={ getHelpText( quantitySelectorStyle ) }
-					>
-						<ToggleGroupControlOption
-							label={ __( 'Input', 'woocommerce' ) }
-							value={ QuantitySelectorStyle.Input }
-						/>
-						<ToggleGroupControlOption
-							label={ __( 'Stepper', 'woocommerce' ) }
-							value={ QuantitySelectorStyle.Stepper }
-						/>
-					</ToggleGroupControl>
-				</PanelBody>
-			</InspectorControls>
-			<div { ...blockProps }>
-				<Disabled>
-					{ quantitySelectorStyle === QuantitySelectorStyle.Input && (
-						<div className="quantity">
-							<input
-								style={
-									// In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
-									! isSiteEditor
-										? {
-												backgroundColor: '#ffffff',
-												lineHeight: 'normal',
-												minHeight: 'unset',
-												boxSizing: 'unset',
-												borderRadius: 'unset',
-										  }
-										: {}
-								}
-								type="number"
-								value="1"
-								className="input-text qty text"
-								readOnly
-							/>
-						</div>
-					) }
-					{ quantitySelectorStyle ===
-						QuantitySelectorStyle.Stepper && (
-						<div className="quantity wc-block-components-quantity-selector">
-							<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">
-								-
-							</button>
-							<input
-								style={
-									// In the post editor, the editor isn't in an iframe, so WordPress styles are applied. We need to remove them.
-									! isSiteEditor
-										? {
-												backgroundColor: '#ffffff',
-												lineHeight: 'normal',
-												minHeight: 'unset',
-												boxSizing: 'unset',
-												borderRadius: 'unset',
-										  }
-										: {}
-								}
-								type="number"
-								value="1"
-								className="input-text qty text"
-								readOnly
-							/>
-							<button className="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">
-								+
-							</button>
-						</div>
-					) }
-				</Disabled>
-			</div>
-		</>
+		<div { ...blockProps }>
+			<Disabled>
+				<QuantityStepper />
+			</Disabled>
+		</div>
 	);
 };

diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/editor.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/editor.scss
deleted file mode 100644
index 72c70777d0..0000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/editor.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-.wc-block-add-to-cart-with-options__quantity-selector {
-	.quantity .qty {
-		// WooCommerce core styles: https://github.com/woocommerce/woocommerce/blob/c9f62609155825cd817976c7611b9b0415e90f2f/plugins/woocommerce/client/legacy/css/woocommerce.scss/#L111-L112
-		width: 3.631em;
-		text-align: center;
-	}
-
-	&.wc-block-add-to-cart-with-options__quantity-selector--input {
-		.quantity .qty {
-			padding-top: 0;
-			padding-bottom: 0;
-			height: 2.5rem;
-			width: 3rem;
-		}
-	}
-
-	&.wc-block-add-to-cart-with-options__quantity-selector--stepper {
-		.wc-block-components-quantity-selector {
-			margin: 0 4px 0 0;
-			width: unset !important;
-		}
-	}
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts
deleted file mode 100644
index e3d1b35eef..0000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/frontend.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-/**
- * External dependencies
- */
-import { store } from '@wordpress/interactivity';
-import type { HTMLElementEvent } from '@woocommerce/types';
-
-/**
- * Internal dependencies
- */
-import type { AddToCartWithOptionsStore } from '../frontend';
-
-// Stores are locked to prevent 3PD usage until the API is stable.
-const universalLock =
-	'I acknowledge that using a private store means my plugin will inevitably break on the next store release.';
-
-const getInputElementFromEvent = (
-	event: HTMLElementEvent< HTMLButtonElement >
-) => {
-	const target = event.target as HTMLButtonElement;
-
-	const inputElement = target.parentElement?.querySelector(
-		'.input-text.qty.text'
-	) as HTMLInputElement | null | undefined;
-
-	return inputElement;
-};
-
-const getInputData = ( event: HTMLElementEvent< HTMLButtonElement > ) => {
-	const inputElement = getInputElementFromEvent( event );
-
-	if ( ! inputElement ) {
-		return;
-	}
-
-	const parsedValue = parseInt( inputElement.value, 10 );
-	const parsedMinValue = parseInt( inputElement.min, 10 );
-	const parsedMaxValue = parseInt( inputElement.max, 10 );
-	const parsedStep = parseInt( inputElement.step, 10 );
-
-	const currentValue = isNaN( parsedValue ) ? 0 : parsedValue;
-	const minValue = isNaN( parsedMinValue ) ? 1 : parsedMinValue;
-	const maxValue = isNaN( parsedMaxValue ) ? undefined : parsedMaxValue;
-	const step = isNaN( parsedStep ) ? 1 : parsedStep;
-
-	return {
-		currentValue,
-		minValue,
-		maxValue,
-		step,
-		inputElement,
-	};
-};
-
-const dispatchChangeEvent = ( inputElement: HTMLInputElement ) => {
-	const event = new Event( 'change' );
-
-	inputElement.dispatchEvent( event );
-};
-
-const { actions: wooAddToCartWithOptionsActions } =
-	store< AddToCartWithOptionsStore >(
-		'woocommerce/add-to-cart-with-options',
-		{},
-		{ lock: universalLock }
-	);
-
-store( 'woocommerce/add-to-cart-with-options', {
-	actions: {
-		addQuantity: ( event: HTMLElementEvent< HTMLButtonElement > ) => {
-			const inputData = getInputData( event );
-			if ( ! inputData ) {
-				return;
-			}
-			const { currentValue, maxValue, step, inputElement } = inputData;
-			const newValue = currentValue + step;
-
-			if ( maxValue === undefined || newValue <= maxValue ) {
-				wooAddToCartWithOptionsActions?.setQuantity( newValue );
-				inputElement.value = newValue.toString();
-				dispatchChangeEvent( inputElement );
-			}
-		},
-		removeQuantity: ( event: HTMLElementEvent< HTMLButtonElement > ) => {
-			const inputData = getInputData( event );
-			if ( ! inputData ) {
-				return;
-			}
-			const { currentValue, minValue, step, inputElement } = inputData;
-			const newValue = currentValue - step;
-
-			if ( newValue >= minValue ) {
-				wooAddToCartWithOptionsActions?.setQuantity( newValue );
-				inputElement.value = newValue.toString();
-				dispatchChangeEvent( inputElement );
-			}
-		},
-	},
-} );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx
index 5ab61aefbf..8fe8626cbe 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/index.tsx
@@ -10,9 +10,6 @@ import { Icon, button } from '@wordpress/icons';
 import metadata from './block.json';
 import AddToCartWithOptionsQuantitySelectorEdit from './edit';
 import { shouldBlockifiedAddToCartWithOptionsBeRegistered } from '../utils';
-import '../../../base/components/quantity-selector/style.scss';
-import './style.scss';
-import './editor.scss';

 if ( shouldBlockifiedAddToCartWithOptionsBeRegistered ) {
 	registerBlockType( metadata, {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/style.scss
deleted file mode 100644
index 2f48c35415..0000000000
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/quantity-selector/style.scss
+++ /dev/null
@@ -1,59 +0,0 @@
-.wc-block-add-to-cart-with-options__quantity-selector {
-	/**
-	* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
-	*
-	* @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
-	*/
-	.input-text {
-		font-size: var(--wp--preset--font-size--small);
-		padding: 0.9rem 1.1rem;
-	}
-}
-
-// Stepper CSS
-.wc-block-add-to-cart-with-options__quantity-selector--stepper {
-	.wc-block-components-quantity-selector {
-		&::after {
-			border: 1px solid currentColor;
-			opacity: 0.3;
-		}
-
-		display: inline-flex;
-		width: unset;
-		margin-right: 0.5em;
-		margin-bottom: 0;
-
-		.input-text {
-			font-size: var(--wp--preset--font-size--small);
-		}
-
-		input[type="number"].input-text.qty.text {
-			-moz-appearance: textfield;
-			border: unset;
-			text-align: center;
-			order: 1;
-			font-weight: 600;
-			background-color: transparent;
-			color: currentColor;
-			margin: 0;
-			/**
-			* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
-			*
-			* @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
-			*/
-			padding: 0.9rem 0;
-			margin-right: unset;
-			font-size: var(--wp--preset--font-size--small);
-
-			&:focus {
-				border-radius: unset;
-			}
-		}
-
-		input[type="number"]::-webkit-inner-spin-button,
-		input[type="number"]::-webkit-outer-spin-button {
-			-webkit-appearance: none;
-			margin: 0;
-		}
-	}
-}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss
index dcab9495f0..6ed147502e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/add-to-cart-with-options/style.scss
@@ -1 +1,58 @@
-/* Styles here */
+.wc-block-add-to-cart-with-options__quantity-selector,
+.wc-block-add-to-cart-with-options-grouped-product-selector-item-cta {
+	/**
+	* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
+	*
+	* @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
+	*/
+	.quantity .input-text {
+		font-size: var(--wp--preset--font-size--small);
+		padding: 0.9rem 1.1rem;
+	}
+
+	// Stepper CSS
+	.wc-block-components-quantity-selector {
+		&::after {
+			border: 1px solid currentColor;
+			opacity: 0.3;
+		}
+
+		display: inline-flex;
+		width: unset;
+		margin-right: 0.5em;
+		margin-bottom: 0;
+
+		.input-text {
+			font-size: var(--wp--preset--font-size--small);
+		}
+
+		input[type="number"].input-text.qty.text {
+			-moz-appearance: textfield;
+			border: unset;
+			text-align: center;
+			order: 1;
+			font-weight: 600;
+			background-color: transparent;
+			color: currentColor;
+			margin: 0;
+			/**
+			* This is a base style for the input text element in WooCommerce that prevents inputs from appearing too small.
+			*
+			* @link https://github.com/woocommerce/woocommerce/blob/95ca53675f2817753d484583c96ca9ab9f725172/plugins/woocommerce/client/legacy/css/woocommerce-blocktheme.scss#L203-L206
+			*/
+			padding: 0.9rem 0;
+			margin-right: unset;
+			font-size: var(--wp--preset--font-size--small);
+
+			&:focus {
+				border-radius: unset;
+			}
+		}
+
+		input[type="number"]::-webkit-inner-spin-button,
+		input[type="number"]::-webkit-outer-spin-button {
+			-webkit-appearance: none;
+			margin: 0;
+		}
+	}
+}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php
index f7791f03e3..9b4b3cdbaa 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsGroupedProductSelectorItemCTA.php
@@ -3,6 +3,7 @@ declare(strict_types=1);

 namespace Automattic\WooCommerce\Blocks\BlockTypes;

+use Automattic\WooCommerce\Blocks\BlockTypes\AddtoCartWithOptions\Utils;
 use WP_Block;

 /**
@@ -30,31 +31,20 @@ class AddToCartWithOptionsGroupedProductSelectorItemCTA extends AbstractBlock {
 	private function get_quantity_selector_markup( $product ) {
 		ob_start();

-		woocommerce_quantity_input(
-			array(
-				/**
-				 * Filter the minimum quantity value allowed for the product.
-				 *
-				 * @since 2.0.0
-				 * @param int        $min_value Minimum quantity value.
-				 * @param WC_Product $product   Product object.
-				 */
-				'min_value'   => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ),
-				/**
-				 * Filter the maximum quantity value allowed for the product.
-				 *
-				 * @since 2.0.0
-				 * @param int        $max_value Maximum quantity value.
-				 * @param WC_Product $product   Product object.
-				 */
-				'max_value'   => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ),
-				'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
-			)
-		);
-
-		$quantity_selector_html = ob_get_clean();
-
-		return $quantity_selector_html;
+		woocommerce_quantity_input( Utils::get_quantity_input_args( $product ) );
+
+		$quantity_html = ob_get_clean();
+
+		// Modify the quantity input to add stepper buttons.
+		$product_name = $product->get_name();
+
+		$quantity_html = Utils::add_quantity_steppers( $quantity_html, $product_name );
+		$quantity_html = Utils::add_quantity_stepper_classes( $quantity_html );
+
+		// Add interactive data attribute for the stepper functionality.
+		$quantity_html = Utils::make_quantity_input_interactive( $quantity_html );
+
+		return $quantity_html;
 	}

 	/**
@@ -106,19 +96,13 @@ class AddToCartWithOptionsGroupedProductSelectorItemCTA extends AbstractBlock {
 	 * @return string Rendered block output.
 	 */
 	protected function render( $attributes, $content, $block ): string {
-		$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
-
 		global $product;
-
 		$previous_product = $product;

-		if ( ! empty( $post_id ) ) {
-			$product = wc_get_product( $post_id );
-		}
-
-		$markup = '';
+		$product = Utils::get_product_from_context( $block, $previous_product );
+		$markup  = '';

-		if ( $product instanceof \WC_Product ) {
+		if ( $product ) {
 			if ( ! $product->is_purchasable() || $product->has_options() || ! $product->is_in_stock() ) {
 				$markup = $this->get_button_markup( $product );
 			} elseif ( $product->is_sold_individually() ) {
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php
index 7e3ebb6731..ae7c8889d3 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptionsQuantitySelector.php
@@ -3,6 +3,7 @@ declare(strict_types=1);

 namespace Automattic\WooCommerce\Blocks\BlockTypes;

+use Automattic\WooCommerce\Blocks\BlockTypes\AddtoCartWithOptions\Utils;
 use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
 use Automattic\WooCommerce\Enums\ProductType;

@@ -20,83 +21,6 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
 	 */
 	protected $block_name = 'add-to-cart-with-options-quantity-selector';

-	/**
-	 * Get the block's attributes.
-	 *
-	 * @param array $attributes Block attributes. Default empty array.
-	 * @return array  Block attributes merged with defaults.
-	 */
-	private function parse_attributes( $attributes ) {
-		// These should match what's set in JS `registerBlockType`.
-		$defaults = array(
-			'quantitySelectorStyle' => 'input',
-		);
-
-		return wp_parse_args( $attributes, $defaults );
-	}
-
-
-	/**
-	 * Enqueue assets specific to this block.
-	 * We enqueue frontend scripts only if the quantitySelectorStyle is set to 'stepper'.
-	 *
-	 * @param array    $attributes Block attributes.
-	 * @param string   $content Block content.
-	 * @param WP_Block $block Block instance.
-	 */
-	protected function enqueue_assets( $attributes, $content, $block ) {
-		if ( ! isset( $attributes['quantitySelectorStyle'] ) || 'stepper' !== $attributes['quantitySelectorStyle'] ) {
-			return;
-		}
-
-		parent::enqueue_assets( $attributes, $content, $block );
-	}
-
-	/**
-	 * Add increment and decrement buttons to the quantity input field.
-	 *
-	 * @param string $product_html Quantity input HTML.
-	 * @param string $product_name Product name.
-	 * @return stringa Quantity input HTML with increment and decrement buttons.
-	 */
-	private function add_steppers( $product_html, $product_name ) {
-		// Regex pattern to match the <input> element with id starting with 'quantity_'.
-		$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
-		// Replacement string to add button BEFORE the matched <input> element.
-		/* translators: %s refers to the item name in the cart. */
-		$minus_button = '<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '"type="button" data-wp-on--click="actions.removeQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>$1';
-		// Replacement string to add button AFTER the matched <input> element.
-		/* translators: %s refers to the item name in the cart. */
-		$plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.addQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
-		$new_html    = preg_replace( $pattern, $minus_button, $product_html );
-		$new_html    = preg_replace( $pattern, $plus_button, $new_html );
-		return $new_html;
-	}
-
-	/**
-	 * Add classes to the Quantity Selector needed for the stepper style.
-	 *
-	 * @param string $product_html The Quantity Selector HTML.
-	 *
-	 * @return string The Quantity Selector HTML with classes added.
-	 */
-	private function add_stepper_classes( $product_html ) {
-		$html = new \WP_HTML_Tag_Processor( $product_html );
-
-		// Add classes to the form.
-		while ( $html->next_tag( array( 'class_name' => 'quantity' ) ) ) {
-			$html->add_class( 'wc-block-components-quantity-selector' );
-		}
-
-		$html = new \WP_HTML_Tag_Processor( $html->get_updated_html() );
-		while ( $html->next_tag( array( 'class_name' => 'input-text' ) ) ) {
-			$html->add_class( 'wc-block-components-quantity-selector__input' );
-		}
-
-		return $html->get_updated_html();
-	}
-
-
 	/**
 	 * Render the block.
 	 *
@@ -116,13 +40,9 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
 		global $product;
 		$previous_product = $product;

-		// Try to load the product from the block context, if not available,
-		// use the global $product.
-		$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
-		$post    = $post_id ? wc_get_product( $post_id ) : null;
-		if ( $post instanceof \WC_Product ) {
-			$product = $post;
-		} elseif ( ! $product instanceof \WC_Product ) {
+		$product = Utils::get_product_from_context( $block, $previous_product );
+
+		if ( ! $product ) {
 			return '';
 		}

@@ -145,39 +65,17 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {

 		wp_enqueue_script_module( $this->get_full_block_name() );

-		$is_stepper_style = isset( $attributes['quantitySelectorStyle'] ) && 'stepper' === $attributes['quantitySelectorStyle'];
-
 		ob_start();

-		woocommerce_quantity_input(
-			array(
-				/**
-				 * Filter the minimum quantity value allowed for the product.
-				 *
-				 * @since 2.0.0
-				 * @param int        $min_value Minimum quantity value.
-				 * @param WC_Product $product   Product object.
-				 */
-				'min_value'   => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ),
-				/**
-				 * Filter the maximum quantity value allowed for the product.
-				 *
-				 * @since 2.0.0
-				 * @param int        $max_value Maximum quantity value.
-				 * @param WC_Product $product   Product object.
-				 */
-				'max_value'   => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ),
-				'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
-			)
-		);
+		woocommerce_quantity_input( Utils::get_quantity_input_args( $product ) );

 		$product_html = ob_get_clean();

 		$product_name = $product->get_name();
-		$product_html = $is_stepper_style ? $this->add_steppers( $product_html, $product_name ) : $product_html;

-		$parsed_attributes  = $this->parse_attributes( $attributes );
-		$product_html       = $is_stepper_style ? $this->add_stepper_classes( $product_html ) : $product_html;
+		$product_html = Utils::add_quantity_steppers( $product_html, $product_name );
+		$product_html = Utils::add_quantity_stepper_classes( $product_html );
+
 		$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array(), array( 'extra_classes' ) );

 		$classes = implode(
@@ -186,7 +84,6 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
 				array(
 					'wp-block-add-to-cart-with-options-quantity-selector wc-block-add-to-cart-with-options__quantity-selector',
 					esc_attr( $classes_and_styles['classes'] ),
-					$is_stepper_style ? 'wc-block-add-to-cart-with-options__quantity-selector--stepper' : 'wc-block-add-to-cart-with-options__quantity-selector--input',
 				)
 			)
 		);
@@ -198,12 +95,7 @@ class AddToCartWithOptionsQuantitySelector extends AbstractBlock {
 			)
 		);

-		$form = sprintf(
-			'<div %1$s %2$s>%3$s</div>',
-			$wrapper_attributes,
-			$is_stepper_style ? 'data-wp-interactive="woocommerce/add-to-cart-with-options"' : '',
-			$product_html
-		);
+		$form = Utils::make_quantity_input_interactive( $product_html, $wrapper_attributes );

 		$product = $previous_product;

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddtoCartWithOptions/Utils.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddtoCartWithOptions/Utils.php
new file mode 100644
index 0000000000..a28c8bd288
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddtoCartWithOptions/Utils.php
@@ -0,0 +1,123 @@
+<?php
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Blocks\BlockTypes\AddtoCartWithOptions;
+
+/**
+ * Utility methods used for the Add to Cart with Options block.
+ * {@internal This class and its methods are not intended for public use.}
+ */
+class Utils {
+	/**
+	 * Add increment and decrement buttons to the quantity input field.
+	 *
+	 * @param string $quantity_html Quantity input HTML.
+	 * @param string $product_name Product name.
+	 * @return string Quantity input HTML with increment and decrement buttons.
+	 */
+	public static function add_quantity_steppers( $quantity_html, $product_name ) {
+		// Regex pattern to match the <input> element with id starting with 'quantity_'.
+		$pattern = '/(<input[^>]*id="quantity_[^"]*"[^>]*\/>)/';
+		// Replacement string to add button BEFORE the matched <input> element.
+		/* translators: %s refers to the item name in the cart. */
+		$minus_button = '<button aria-label="' . esc_attr( sprintf( __( 'Reduce quantity of %s', 'woocommerce' ), $product_name ) ) . '"type="button" data-wp-on--click="actions.decreaseQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--minus">-</button>$1';
+		// Replacement string to add button AFTER the matched <input> element.
+		/* translators: %s refers to the item name in the cart. */
+		$plus_button = '$1<button aria-label="' . esc_attr( sprintf( __( 'Increase quantity of %s', 'woocommerce' ), $product_name ) ) . '" type="button" data-wp-on--click="actions.increaseQuantity" class="wc-block-components-quantity-selector__button wc-block-components-quantity-selector__button--plus">+</button>';
+		$new_html    = preg_replace( $pattern, $minus_button, $quantity_html );
+		$new_html    = preg_replace( $pattern, $plus_button, $new_html );
+		return $new_html;
+	}
+
+	/**
+	 * Add classes to the Quantity Selector needed for the stepper style.
+	 *
+	 * @param string $quantity_html The Quantity Selector HTML.
+	 *
+	 * @return string The Quantity Selector HTML with classes added.
+	 */
+	public static function add_quantity_stepper_classes( $quantity_html ) {
+		$html = new \WP_HTML_Tag_Processor( $quantity_html );
+
+		// Add classes to the form.
+		while ( $html->next_tag( array( 'class_name' => 'quantity' ) ) ) {
+			$html->add_class( 'wc-block-components-quantity-selector' );
+		}
+
+		$html = new \WP_HTML_Tag_Processor( $html->get_updated_html() );
+		while ( $html->next_tag( array( 'class_name' => 'input-text' ) ) ) {
+			$html->add_class( 'wc-block-components-quantity-selector__input' );
+		}
+
+		return $html->get_updated_html();
+	}
+
+	/**
+	 * Get standardized quantity input arguments for WooCommerce quantity input.
+	 *
+	 * @param \WC_Product $product The product object.
+	 * @return array Arguments for woocommerce_quantity_input().
+	 */
+	public static function get_quantity_input_args( $product ) {
+		return array(
+			/**
+			 * Filter the minimum quantity value allowed for the product.
+			 *
+			 * @since 2.0.0
+			 * @param int        $min_value Minimum quantity value.
+			 * @param WC_Product $product   Product object.
+			 */
+			'min_value'   => apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ),
+			/**
+			 * Filter the maximum quantity value allowed for the product.
+			 *
+			 * @since 2.0.0
+			 * @param int        $max_value Maximum quantity value.
+			 * @param WC_Product $product   Product object.
+			 */
+			'max_value'   => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ),
+			'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // phpcs:ignore WordPress.Security.NonceVerification.Missing
+		);
+	}
+
+	/**
+	 * Make the quantity input interactive by wrapping it with the necessary data attribute.
+	 *
+	 * @param string $quantity_html The quantity HTML.
+	 * @param string $wrapper_attributes Optional wrapper attributes.
+	 * @return string The quantity HTML with interactive wrapper.
+	 */
+	public static function make_quantity_input_interactive( $quantity_html, $wrapper_attributes = '' ) {
+		if ( ! empty( $wrapper_attributes ) ) {
+			return sprintf(
+				'<div %1$s data-wp-interactive="woocommerce/add-to-cart-with-options">%2$s</div>',
+				$wrapper_attributes,
+				$quantity_html
+			);
+		}
+
+		return '<div data-wp-interactive="woocommerce/add-to-cart-with-options">' . $quantity_html . '</div>';
+	}
+
+	/**
+	 * Get product from block context.
+	 *
+	 * @param \WP_Block        $block The block instance.
+	 * @param \WC_Product|null $previous_product The previous product (usually from global scope).
+	 * @return \WC_Product|null The product instance or null if not found.
+	 */
+	public static function get_product_from_context( $block, $previous_product ) {
+		$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
+		$product = null;
+
+		if ( ! empty( $post_id ) ) {
+			$product = wc_get_product( $post_id );
+		}
+
+		if ( ! $product instanceof \WC_Product && $previous_product instanceof \WC_Product ) {
+			$product = $previous_product;
+		}
+
+		return $product instanceof \WC_Product ? $product : null;
+	}
+}