diff --git a/assets/js/atomic/blocks/index.js b/assets/js/atomic/blocks/index.js
index d62fb940f32..fd7a80536df 100644
--- a/assets/js/atomic/blocks/index.js
+++ b/assets/js/atomic/blocks/index.js
@@ -3,6 +3,7 @@
*/
import './product-elements/title';
import './product-elements/price';
+import './product-elements/price-v2';
import './product-elements/image';
import './product-elements/rating';
import './product-elements/rating-stars';
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/block.json b/assets/js/atomic/blocks/product-elements/price-v2/block.json
new file mode 100644
index 00000000000..6179874ad62
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/block.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "woocommerce/product-price-v2",
+ "title": "Price",
+ "keywords": [ "WooCommerce" ],
+ "category": "woocommerce-product-elements",
+ "textdomain": "woo-gutenberg-products-block",
+ "description": "Display the price of a product, including any discounts.",
+ "supports": {
+ "layout": {
+ "allowSwitching": false,
+ "allowInheriting": false,
+ "default": { "type": "flex" }
+ },
+ "html": false
+ },
+ "usesContext": [ "queryId", "postId" ],
+ "providesContext": {
+ "woocommerce/isDescendentOfSingleProductTemplate":
+ "isDescendentOfSingleProductTemplate",
+ "woocommerce/isDescendentOfSingleProductBlock":
+ "isDescendentOfSingleProductBlock",
+ "woocommerce/withSuperScriptStyle": "withSuperScriptStyle"
+ },
+ "attributes": {
+ "isDescendentOfSingleProductTemplate": {
+ "type": "boolean",
+ "default": false
+ },
+ "isDescendentOfSingleProductBlock": {
+ "type": "boolean",
+ "default": false
+ },
+ "productId": {
+ "type": "number",
+ "default": 0
+ },
+ "withSuperScriptStyle": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "styles": [
+ { "name": "default", "label": "Default", "isDefault": true },
+ { "name": "price-super", "label": "Superscript", "isDefault": false }
+ ]
+ }
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/constants.tsx b/assets/js/atomic/blocks/product-elements/price-v2/constants.tsx
new file mode 100644
index 00000000000..a8a5261b1fe
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/constants.tsx
@@ -0,0 +1,11 @@
+/**
+ * External dependencies
+ */
+import { currencyDollar, Icon } from '@wordpress/icons';
+
+export const BLOCK_ICON: JSX.Element = (
+
+);
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/edit.tsx b/assets/js/atomic/blocks/product-elements/price-v2/edit.tsx
new file mode 100644
index 00000000000..af47ca58b04
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/edit.tsx
@@ -0,0 +1,181 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+import { useEffect, useMemo } from '@wordpress/element';
+import { useSelect } from '@wordpress/data';
+import {
+ InnerBlockLayoutContextProvider,
+ useInnerBlockLayoutContext,
+ ProductDataContextProvider,
+} from '@woocommerce/shared-context';
+import { useStoreProducts } from '@woocommerce/base-context/hooks';
+
+/**
+ * Internal dependencies
+ */
+import { BLOCK_NAME as priceDiscountName } from './inner-blocks/discount';
+import { BLOCK_NAME as originalPriceName } from './inner-blocks/original-price';
+import { BLOCK_NAME as currentPriceName } from './inner-blocks/current-price';
+import { TEMPLATE } from './template';
+import './editor.scss';
+
+interface Attributes {
+ isDescendentOfSingleProductBlock: boolean;
+ isDescendentOfSingleProductTemplate: boolean;
+ withSuperScriptStyle: boolean;
+ productId?: number;
+}
+
+interface Context {
+ postId?: number;
+ queryId?: number;
+}
+
+interface Props {
+ context: Context;
+ attributes: Attributes;
+ setAttributes: ( attributes: Partial< Attributes > ) => void;
+}
+
+interface ContextProviderProps extends Props {
+ children: JSX.Element | JSX.Element[] | undefined;
+}
+
+type ProductIdProps = Partial< ContextProviderProps > & { productId: number };
+
+const deriveSuperScriptFromClass = ( className: string ): boolean => {
+ if ( className ) {
+ const classList = className.split( ' ' );
+ return classList.includes( 'is-style-price-super' );
+ }
+ return false;
+};
+
+const ProviderFromAPI = ( {
+ productId,
+ children,
+}: ProductIdProps ): JSX.Element => {
+ // TODO: this would be good to derive from the WP entity store at some point.
+ const { products, productsLoading } = useStoreProducts( {
+ include: productId,
+ } );
+ let product = null;
+ if ( products.length > 0 ) {
+ product =
+ products.find(
+ ( productIteration ) => productIteration.id === productId
+ ) || null;
+ }
+
+ return (
+
+ { children }
+
+ );
+};
+
+const DerivedProductDataContextProvider = ( {
+ context,
+ attributes,
+ setAttributes,
+ children,
+}: ContextProviderProps & { withSuperScript: boolean } ): JSX.Element => {
+ const { queryId, postId } = context;
+ const { productId } = attributes;
+ const isDescendentOfQueryLoop = Number.isFinite( queryId );
+ const id = isDescendentOfQueryLoop ? postId : productId;
+ const isDescendentOfSingleProductTemplate = useSelect(
+ ( select ) => {
+ const editSiteStore = select( 'core/edit-site' );
+ const editorPostId = editSiteStore?.getEditedPostId<
+ string | undefined
+ >();
+
+ return Boolean(
+ editorPostId?.includes( '//single-product' ) &&
+ ! isDescendentOfQueryLoop
+ );
+ },
+ [ isDescendentOfQueryLoop ]
+ );
+
+ useEffect(
+ () =>
+ setAttributes( {
+ isDescendentOfSingleProductTemplate,
+ } ),
+ [ isDescendentOfSingleProductTemplate, setAttributes ]
+ );
+ if ( id && id > 0 ) {
+ return { children };
+ }
+ return (
+
+ { children }
+
+ );
+};
+
+const EditBlock = ( {
+ context,
+ attributes,
+ setAttributes,
+ withSuperScript,
+}: Props & { withSuperScript: boolean } ): JSX.Element => {
+ const { parentClassName } = useInnerBlockLayoutContext();
+ return (
+
+
+
+
+
+ );
+};
+
+const Edit = ( {
+ setAttributes,
+ attributes,
+ ...props
+}: Props ): JSX.Element => {
+ const blockProps = useBlockProps();
+ const withSuperScript = useMemo(
+ () => deriveSuperScriptFromClass( blockProps.className ),
+ [ blockProps.className ]
+ );
+ useEffect( () => {
+ setAttributes( { withSuperScriptStyle: withSuperScript } );
+ }, [ withSuperScript, setAttributes ] );
+ return (
+
+
+
+
+
+ );
+};
+
+export default Edit;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/editor.scss b/assets/js/atomic/blocks/product-elements/price-v2/editor.scss
new file mode 100644
index 00000000000..8ad61d24194
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/editor.scss
@@ -0,0 +1,12 @@
+.wc-block-price-element {
+ .block-editor-inner-blocks {
+ .block-editor-block-list__layout {
+ display: flex;
+
+ .wp-block-woocommerce-original-price,
+ .wp-block-woocommerce-current-price {
+ margin-right: $gap-smaller;
+ }
+ }
+ }
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/index.ts b/assets/js/atomic/blocks/product-elements/price-v2/index.ts
new file mode 100644
index 00000000000..044b238dbee
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/index.ts
@@ -0,0 +1,24 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import save from './save';
+import { supports } from './supports';
+import metadata from './block.json';
+import { BLOCK_ICON as icon } from './constants';
+import './inner-blocks';
+
+const blockConfig = {
+ ...metadata,
+ icon: { src: icon },
+ supports: { ...supports, ...metadata.supports },
+ edit,
+ save,
+};
+
+registerBlockType( metadata.name, blockConfig );
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/block.json b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/block.json
new file mode 100644
index 00000000000..240792ea3c1
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/block.json
@@ -0,0 +1,19 @@
+{
+ "name": "woocommerce/current-price",
+ "version": "1.0.0",
+ "icon": "info",
+ "title": "Current Price",
+ "description": "Display the current price for the product",
+ "supports": {
+ "color": {
+ "background": true,
+ "text": true
+ }
+ },
+ "category": "woocommerce",
+ "keywords": [ "WooCommerce" ],
+ "usesContext": ["woocommerce/withSuperScriptStyle", "woocommerce/isDescendentOfSingleProductTemplate", "postId"],
+ "textdomain": "woo-gutenberg-products-block",
+ "apiVersion": 2,
+ "$schema": "https://schemas.wp.org/trunk/block.json"
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/block.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/block.tsx
new file mode 100644
index 00000000000..2d83b7e6a3d
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/block.tsx
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import type { HTMLAttributes } from 'react';
+import classnames from 'classnames';
+import { useStyleProps } from '@woocommerce/base-hooks';
+import { useInnerBlockLayoutContext } from '@woocommerce/shared-context';
+import ProductPrice from '@woocommerce/base-components/product-price';
+
+/**
+ * Internal dependencies
+ */
+import type { PriceProps } from '../../types';
+
+type Props = PriceProps & HTMLAttributes< HTMLDivElement >;
+
+const Block = ( {
+ attributes,
+ context,
+ rawPrice,
+ minPrice,
+ maxPrice,
+ currency,
+ isDescendentOfSingleProductTemplate,
+}: Props ): JSX.Element | null => {
+ const { className } = attributes;
+ const { className: stylesClassName, style } = useStyleProps( attributes );
+ const { parentClassName } = useInnerBlockLayoutContext();
+ const wrapperClassName = classnames(
+ className,
+ {
+ [ `${ parentClassName }__product-price` ]: parentClassName,
+ },
+ stylesClassName
+ );
+ if ( ! rawPrice && ! isDescendentOfSingleProductTemplate ) {
+ return ;
+ }
+
+ const pricePreview = '3000';
+ const priceClassName = classnames( {
+ [ `${ parentClassName }__product-current-price__value` ]:
+ parentClassName,
+ } );
+
+ return (
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/edit.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/edit.tsx
new file mode 100644
index 00000000000..9687453ada5
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/edit.tsx
@@ -0,0 +1,52 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import type { HTMLAttributes, CSSProperties } from 'react';
+import { useProductDataContext } from '@woocommerce/shared-context';
+import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
+
+/**
+ * Internal dependencies
+ */
+import PriceBlock from './block';
+
+interface Attributes {
+ style: CSSProperties;
+}
+
+type Props = {
+ attributes: Attributes;
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ context?: { 'woocommerce/isDescendentOfSingleProductTemplate': boolean };
+} & HTMLAttributes< HTMLDivElement >;
+
+const CurrentPriceEdit = ( { attributes, context }: Props ): JSX.Element => {
+ const blockProps = useBlockProps();
+ const { product } = useProductDataContext();
+ const isDescendentOfSingleProductTemplate =
+ context && context[ 'woocommerce/isDescendentOfSingleProductTemplate' ];
+ const currentPrice = product?.prices?.price;
+ const currency = isDescendentOfSingleProductTemplate
+ ? getCurrencyFromPriceResponse()
+ : getCurrencyFromPriceResponse( product?.prices );
+ const blockAttrs = {
+ attributes,
+ currency,
+ context,
+ rawPrice: currentPrice,
+ minPrice: product?.prices?.price_range?.min_amount,
+ maxPrice: product?.prices?.price_range?.max_amount,
+ priceType: 'current',
+ isDescendentOfSingleProductTemplate,
+ };
+ return (
+ <>
+
+ >
+ );
+};
+
+export default CurrentPriceEdit;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/index.ts b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/index.ts
new file mode 100644
index 00000000000..2da5e528913
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/current-price/index.ts
@@ -0,0 +1,30 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import sharedConfig from '../../../shared/config';
+import metadata from './block.json';
+import { supports } from '../../supports';
+
+const { ancestor, ...configuration } = sharedConfig;
+
+const blockConfig = {
+ ...configuration,
+ ...metadata,
+ edit,
+ supports: {
+ ...supports,
+ ...metadata.supports,
+ ...configuration.supports,
+ context: '',
+ },
+};
+
+registerBlockType( metadata.name, blockConfig );
+
+export const BLOCK_NAME = metadata.name;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/block.json b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/block.json
new file mode 100644
index 00000000000..7a5acd04f42
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/block.json
@@ -0,0 +1,25 @@
+{
+ "name": "woocommerce/discount",
+ "version": "1.0.0",
+ "icon": "info",
+ "title": "Discount",
+ "description": "Display the discounted amount for a product.",
+ "category": "woocommerce",
+ "keywords": [ "WooCommerce" ],
+ "textdomain": "woo-gutenberg-products-block",
+ "usesContext": ["woocommerce/isDescendentOfSingleProductTemplate" ,"postId"],
+ "supports": {
+ "layout": {
+ "allowSwitching": false,
+ "allowInheriting": false,
+ "default": { "type": "flex" }
+ },
+ "html": false,
+ "color": {
+ "background": true,
+ "text": true
+ }
+ },
+ "apiVersion": 2,
+ "$schema": "https://schemas.wp.org/trunk/block.json"
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/edit.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/edit.tsx
new file mode 100644
index 00000000000..85e7b24b807
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/edit.tsx
@@ -0,0 +1,55 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';
+import { useProductDataContext } from '@woocommerce/shared-context';
+import { useStyleProps } from '@woocommerce/base-hooks';
+import { useEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { discountAmountName } from './inner-blocks';
+import { TEMPLATE } from './template';
+import './editor.scss';
+
+export const Edit = ( { attributes, setAttributes, context } ): JSX.Element => {
+ const { style, className } = useStyleProps( attributes );
+ const { product } = useProductDataContext();
+ const originalPrice = product?.prices?.regular_price;
+ const currentPrice = product?.prices?.price;
+ const isDescendentOfSingleProductTemplate =
+ context && context[ 'woocommerce/isDescendentOfSingleProductTemplate' ];
+
+ const showPrice =
+ ( originalPrice && currentPrice !== originalPrice ) ||
+ isDescendentOfSingleProductTemplate;
+ useEffect( () => {
+ if ( ! attributes?.style ) {
+ setAttributes( { style: { spacing: { blockGap: '0' } } } );
+ }
+ }, [ attributes, setAttributes ] );
+ return (
+ <>
+ { showPrice && (
+
+
+
+ ) }
+ >
+ );
+};
+
+export const Save = (): JSX.Element => {
+ return (
+
+
+
+ );
+};
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/editor.scss b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/editor.scss
new file mode 100644
index 00000000000..66bb3dd5b79
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/editor.scss
@@ -0,0 +1,3 @@
+.wp-block-woocommerce-discount-amount + p {
+ margin: 0;
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/index.ts b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/index.ts
new file mode 100644
index 00000000000..40171006b72
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/index.ts
@@ -0,0 +1,29 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { Edit as edit, Save as save } from './edit';
+import sharedConfig from '../../../shared/config';
+import { supports } from '../../supports';
+import metadata from './block.json';
+
+const { ancestor, ...configuration } = sharedConfig;
+
+const blockConfig = {
+ ...configuration,
+ ...metadata,
+ supports: {
+ ...supports,
+ ...metadata.supports,
+ },
+ edit,
+ save,
+};
+
+registerBlockType( metadata.name, blockConfig );
+
+export const BLOCK_NAME = metadata.name;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/block.json b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/block.json
new file mode 100644
index 00000000000..28817262461
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/block.json
@@ -0,0 +1,14 @@
+{
+ "name": "woocommerce/discount-amount",
+ "version": "1.0.0",
+ "icon": "info",
+ "title": "Discount Amount",
+ "description": "Display the discounted amount value.",
+ "category": "woocommerce",
+ "usesContext": ["woocommerce/isDescendentOfSingleProductTemplate"],
+ "keywords": [ "WooCommerce" ],
+ "textdomain": "woo-gutenberg-products-block",
+ "ancestor": [ "woocommerce/discount" ],
+ "apiVersion": 2,
+ "$schema": "https://schemas.wp.org/trunk/block.json"
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/block.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/block.tsx
new file mode 100644
index 00000000000..7835b21c450
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/block.tsx
@@ -0,0 +1,84 @@
+/**
+ * External dependencies
+ */
+import type { HTMLAttributes } from 'react';
+import classnames from 'classnames';
+import { useStyleProps } from '@woocommerce/base-hooks';
+import { useInnerBlockLayoutContext } from '@woocommerce/shared-context';
+import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
+
+/**
+ * Internal dependencies
+ */
+import { PriceProps } from '../../../../types';
+
+type Props = PriceProps & HTMLAttributes< HTMLDivElement >;
+
+const calculateDiscountAmount = ( originalPrice, currentPrice ) => {
+ // todo: this assumes currentPrice is lower than originalPrice
+ return parseInt( originalPrice, 10 ) - parseInt( currentPrice, 10 );
+};
+
+const calculateDiscountPercentage = ( originalPrice, currentPrice ) => {
+ const discountAmount = calculateDiscountAmount(
+ originalPrice,
+ currentPrice
+ );
+ return Math.floor(
+ ( discountAmount / parseInt( originalPrice, 10 ) ) * 100
+ );
+};
+
+const Block = ( {
+ attributes,
+ originalPrice,
+ currentPrice,
+ currency,
+ isDescendentOfSingleProductTemplate,
+}: Props ): JSX.Element | null => {
+ // todo: need to setup discountType and showDiscount as attributes/context
+ // from the parent block.
+ const {
+ className,
+ discountType = 'percentage',
+ showDiscount = true,
+ } = attributes;
+ const { className: stylesClassName, style } = useStyleProps( attributes );
+ const { parentClassName } = useInnerBlockLayoutContext();
+ if ( ! showDiscount ) {
+ return null;
+ }
+ const wrapperClassName = classnames(
+ className,
+ {
+ [ `${ parentClassName }__product-price` ]: parentClassName,
+ },
+ stylesClassName
+ );
+ const priceClassName = classnames( {
+ [ `${ parentClassName }__product-price_discount` ]: parentClassName,
+ } );
+ // todo: lift all the preview pricing up to the parent block (across all the price inner blocks)
+ const oPrice = isDescendentOfSingleProductTemplate ? '5000' : originalPrice;
+ const cPrice = isDescendentOfSingleProductTemplate ? '3000' : currentPrice;
+ const DisplayedDiscount =
+ discountType === 'percentage' ? (
+
+ { calculateDiscountPercentage( oPrice, cPrice ) }%
+
+ ) : (
+
+ );
+
+ return (
+
+ { DisplayedDiscount }
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/edit.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/edit.tsx
new file mode 100644
index 00000000000..7b3b576e580
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/edit.tsx
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import type { HTMLAttributes, CSSProperties } from 'react';
+import { useProductDataContext } from '@woocommerce/shared-context';
+import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
+
+/**
+ * Internal dependencies
+ */
+import DiscountBlock from './block';
+
+interface Attributes {
+ style: CSSProperties;
+}
+
+type Props = {
+ attributes: Attributes;
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ context?: { 'woocommerce/isDescendentOfSingleProductTemplate': boolean };
+} & HTMLAttributes< HTMLDivElement >;
+
+const DiscountEdit = ( { attributes, context }: Props ): JSX.Element => {
+ const blockProps = useBlockProps();
+ const { product } = useProductDataContext();
+ const isDescendentOfSingleProductTemplate =
+ context && context[ 'woocommerce/isDescendentOfSingleProductTemplate' ];
+ const originalPrice = product?.prices?.regular_price;
+ const currentPrice = product?.prices?.price;
+ const showPrice =
+ ( originalPrice && currentPrice !== originalPrice ) ||
+ isDescendentOfSingleProductTemplate;
+ const currency = isDescendentOfSingleProductTemplate
+ ? getCurrencyFromPriceResponse()
+ : getCurrencyFromPriceResponse( product?.prices );
+ const blockAttrs = {
+ attributes,
+ currency,
+ context,
+ originalPrice,
+ currentPrice,
+ isDescendentOfSingleProductTemplate,
+ };
+ return (
+ <>
+ { showPrice && (
+
+
+
+ ) }
+ >
+ );
+};
+
+export default DiscountEdit;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/index.ts b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/index.ts
new file mode 100644
index 00000000000..be7063e7eb1
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/discount-amount/index.ts
@@ -0,0 +1,27 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import sharedConfig from '../../../../../shared/config';
+import metadata from './block.json';
+
+const { ancestor, ...configuration } = sharedConfig;
+
+const blockConfig = {
+ ...configuration,
+ ...metadata,
+ edit,
+ supports: {
+ ...configuration.supports,
+ context: '',
+ },
+};
+
+registerBlockType( metadata.name, blockConfig );
+
+export const BLOCK_NAME = metadata.name;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/index.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/index.tsx
new file mode 100644
index 00000000000..9f20e96872f
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/inner-blocks/index.tsx
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import './discount-amount/index.ts';
+
+export { BLOCK_NAME as discountAmountName } from './discount-amount/index';
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/template.ts b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/template.ts
new file mode 100644
index 00000000000..58153b03b92
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/discount/template.ts
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { InnerBlockTemplate } from '@wordpress/blocks';
+import { _x, sprintf } from '@wordpress/i18n';
+
+// Some hackery to both make translation clearer and to allow for RTL languages
+// in default template layout.
+const translatedContent = sprintf(
+ // translators: %s: discount amount. The rendered value will only allow for placeholder at beginning or end of string.
+ _x(
+ '%s off',
+ 'post text next to discount amount',
+ 'woo-gutenberg-products-block'
+ ),
+ ':placeholder:'
+);
+
+const templateContent = translatedContent.replace( ':placeholder:', '' );
+
+const RTLtemplate: InnerBlockTemplate[] = [
+ [ 'core/paragraph', { content: templateContent }, [] ],
+ [ 'woocommerce/discount-amount', {}, [] ],
+];
+
+const LTRtemplate: InnerBlockTemplate[] = [
+ [ 'woocommerce/discount-amount', {}, [] ],
+ [ 'core/paragraph', { content: templateContent }, [] ],
+];
+
+export const TEMPLATE: InnerBlockTemplate[] =
+ translatedContent.indexOf( ':placeholder:' ) === 0
+ ? LTRtemplate
+ : RTLtemplate;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/index.ts b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/index.ts
new file mode 100644
index 00000000000..2277fbc12b6
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/index.ts
@@ -0,0 +1,6 @@
+/**
+ * Internal dependencies
+ */
+import './original-price';
+import './current-price';
+import './discount';
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/block.json b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/block.json
new file mode 100644
index 00000000000..2c155fafaaa
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/block.json
@@ -0,0 +1,19 @@
+{
+ "name": "woocommerce/original-price",
+ "version": "1.0.0",
+ "icon": "info",
+ "title": "Original Price",
+ "description": "Display the original price for the product",
+ "supports": {
+ "color": {
+ "background": true,
+ "text": true
+ }
+ },
+ "category": "woocommerce",
+ "keywords": [ "WooCommerce" ],
+ "usesContext": ["woocommerce/withSuperScriptStyle", "woocommerce/isDescendentOfSingleProductTemplate", "postId"],
+ "textdomain": "woo-gutenberg-products-block",
+ "apiVersion": 2,
+ "$schema": "https://schemas.wp.org/trunk/block.json"
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/block.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/block.tsx
new file mode 100644
index 00000000000..f76794252d5
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/block.tsx
@@ -0,0 +1,59 @@
+/**
+ * External dependencies
+ */
+import type { HTMLAttributes } from 'react';
+import classnames from 'classnames';
+import { useStyleProps } from '@woocommerce/base-hooks';
+import { useInnerBlockLayoutContext } from '@woocommerce/shared-context';
+import ProductPrice from '@woocommerce/base-components/product-price';
+
+/**
+ * Internal dependencies
+ */
+import type { PriceProps } from '../../types';
+
+type Props = PriceProps & HTMLAttributes< HTMLDivElement >;
+
+const Block = ( {
+ attributes,
+ context,
+ rawPrice,
+ priceType,
+ currency,
+ isDescendentOfSingleProductTemplate,
+}: Props ): JSX.Element | null => {
+ const { className } = attributes;
+ const { className: stylesClassName, style } = useStyleProps( attributes );
+ const { parentClassName } = useInnerBlockLayoutContext();
+ const wrapperClassName = classnames(
+ className,
+ {
+ [ `${ parentClassName }__product-price` ]: parentClassName,
+ },
+ stylesClassName
+ );
+ if ( ! rawPrice && ! isDescendentOfSingleProductTemplate ) {
+ return ;
+ }
+
+ const pricePreview = '5000';
+ const priceClassName = classnames( {
+ [ `${ parentClassName }__product-${ priceType }-price__value` ]:
+ parentClassName,
+ } );
+
+ return (
+
+ );
+};
+
+export default Block;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/edit.tsx b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/edit.tsx
new file mode 100644
index 00000000000..6ebfbc16b92
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/edit.tsx
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import { useBlockProps } from '@wordpress/block-editor';
+import type { HTMLAttributes, CSSProperties } from 'react';
+import { useProductDataContext } from '@woocommerce/shared-context';
+import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
+
+/**
+ * Internal dependencies
+ */
+import PriceBlock from './block';
+
+interface Attributes {
+ style: CSSProperties;
+}
+
+type Props = {
+ attributes: Attributes;
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ context?: { 'woocommerce/isDescendentOfSingleProductTemplate': boolean };
+} & HTMLAttributes< HTMLDivElement >;
+
+const OriginalPriceEdit = ( { attributes, context }: Props ): JSX.Element => {
+ const blockProps = useBlockProps();
+ const { product } = useProductDataContext();
+ const isDescendentOfSingleProductTemplate =
+ context && context[ 'woocommerce/isDescendentOfSingleProductTemplate' ];
+ const originalPrice = product?.prices?.regular_price;
+ const currentPrice = product?.prices?.price;
+ const showPrice =
+ ( originalPrice && currentPrice !== originalPrice ) ||
+ isDescendentOfSingleProductTemplate;
+ const currency = isDescendentOfSingleProductTemplate
+ ? getCurrencyFromPriceResponse()
+ : getCurrencyFromPriceResponse( product?.prices );
+ const blockAttrs = {
+ attributes,
+ currency,
+ context,
+ rawPrice: originalPrice,
+ priceType: 'original',
+ isDescendentOfSingleProductTemplate,
+ };
+ return (
+ <>
+ { showPrice && (
+
+ ) }
+ >
+ );
+};
+
+export default OriginalPriceEdit;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/index.ts b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/index.ts
new file mode 100644
index 00000000000..422736eed57
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/index.ts
@@ -0,0 +1,31 @@
+/**
+ * External dependencies
+ */
+import { registerBlockType } from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+import sharedConfig from '../../../shared/config';
+import metadata from './block.json';
+import { supports } from '../../supports';
+import './style.scss';
+
+const { ancestor, ...configuration } = sharedConfig;
+
+const blockConfig = {
+ ...configuration,
+ ...metadata,
+ edit,
+ supports: {
+ ...metadata.supports,
+ ...configuration.supports,
+ ...supports,
+ context: '',
+ },
+};
+
+registerBlockType( metadata.name, blockConfig );
+
+export const BLOCK_NAME = metadata.name;
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/style.scss b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/style.scss
new file mode 100644
index 00000000000..42b5ecee2df
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/inner-blocks/original-price/style.scss
@@ -0,0 +1,3 @@
+.wp-block-woocommerce-original-price {
+ background-color: #c1d21c;
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/save.tsx b/assets/js/atomic/blocks/product-elements/price-v2/save.tsx
new file mode 100644
index 00000000000..87b0ec215fb
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/save.tsx
@@ -0,0 +1,8 @@
+/**
+ * External dependencies
+ */
+import { useInnerBlocksProps, useBlockProps } from '@wordpress/block-editor';
+
+export default function save() {
+ return ;
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/style.scss b/assets/js/atomic/blocks/product-elements/price-v2/style.scss
new file mode 100644
index 00000000000..9796974b866
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/style.scss
@@ -0,0 +1,8 @@
+.wp-block-woocommerce-product-price-v2 {
+ display: flex;
+
+ .wp-block-woocommerce-original-price,
+ .wp-block-woocommerce-current-price {
+ margin-right: $gap-smaller;
+ }
+}
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/supports.ts b/assets/js/atomic/blocks/product-elements/price-v2/supports.ts
new file mode 100644
index 00000000000..2f5caecfe36
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/supports.ts
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import { isFeaturePluginBuild } from '@woocommerce/block-settings';
+import { __experimentalGetSpacingClassesAndStyles } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import sharedConfig from '../shared/config';
+
+/**
+ * todo: can use __experimentalDefaultControls to influence which controls are
+ * displayed by default
+ */
+
+export const supports = {
+ ...sharedConfig.supports,
+ ...( isFeaturePluginBuild() && {
+ color: {
+ text: true,
+ background: true,
+ link: false,
+ },
+ typography: {
+ fontSize: true,
+ lineHeight: true,
+ __experimentalFontFamily: true,
+ __experimentalFontWeight: true,
+ __experimentalFontStyle: true,
+ __experimentalLetterSpacing: true,
+ },
+ __experimentalSelector:
+ '.wp-block-woocommerce-product-price .wc-block-components-product-price',
+ } ),
+ ...( typeof __experimentalGetSpacingClassesAndStyles === 'function' && {
+ spacing: {
+ margin: true,
+ padding: true,
+ blockGap: true,
+ },
+ } ),
+};
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/template.ts b/assets/js/atomic/blocks/product-elements/price-v2/template.ts
new file mode 100644
index 00000000000..ef992eb9ea5
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/template.ts
@@ -0,0 +1,10 @@
+/**
+ * External dependencies
+ */
+import { InnerBlockTemplate } from '@wordpress/blocks';
+
+export const TEMPLATE: InnerBlockTemplate[] = [
+ [ 'woocommerce/original-price', {}, [] ],
+ [ 'woocommerce/current-price', {}, [] ],
+ [ 'woocommerce/discount', {}, [] ],
+];
diff --git a/assets/js/atomic/blocks/product-elements/price-v2/types.ts b/assets/js/atomic/blocks/product-elements/price-v2/types.ts
new file mode 100644
index 00000000000..397f6179819
--- /dev/null
+++ b/assets/js/atomic/blocks/product-elements/price-v2/types.ts
@@ -0,0 +1,37 @@
+/**
+ * External dependencies
+ */
+import type { CSSProperties } from 'react';
+import type { ProductPriceProps } from '@woocommerce/base-components/product-price';
+
+// old code
+export interface BlockAttributes {
+ productId?: number;
+ className?: string;
+ textAlign?: 'left' | 'center' | 'right';
+ isDescendentOfQueryLoop?: boolean;
+ isDescendentOfSingleProductTemplate?: boolean;
+}
+
+export interface PriceContext {
+ isDescendentOfSingleProductTemplate: boolean;
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'woocommerce/withSuperScriptStyle'?: boolean;
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'woocommerce/isDescendentOfSingleProductTemplate'?: boolean;
+}
+
+export interface PriceAttributes {
+ className?: string;
+ style: CSSProperties;
+}
+
+export interface PriceProps extends ProductPriceProps {
+ attributes: PriceAttributes;
+ context?: PriceContext;
+ rawPrice?: string;
+ minPrice?: string;
+ maxPrice?: string;
+ priceType: 'original' | 'current';
+ isDescendentOfSingleProductTemplate?: boolean;
+}
diff --git a/assets/js/base/components/formatted-monetary-amount/index.tsx b/assets/js/base/components/formatted-monetary-amount/index.tsx
index f4a5e97157a..956b74f9950 100644
--- a/assets/js/base/components/formatted-monetary-amount/index.tsx
+++ b/assets/js/base/components/formatted-monetary-amount/index.tsx
@@ -18,16 +18,42 @@ import './style.scss';
interface FormattedMonetaryAmountProps
extends Omit< NumberFormatProps, 'onValueChange' > {
className?: string;
- displayType?: NumberFormatProps[ 'displayType' ];
allowNegative?: boolean;
isAllowed?: ( formattedValue: NumberFormatValues ) => boolean;
value: number | string; // Value of money amount.
currency: Currency | Record< string, never >; // Currency configuration object.
onValueChange?: ( unit: number ) => void; // Function to call when value changes.
style?: React.CSSProperties | undefined;
- renderText?: ( value: string ) => JSX.Element;
+ renderText?: ( value: string ) => ReactElement | string;
+ withSuperScript?: boolean;
}
+const applySuperscript =
+ ( currency: FormattedMonetaryAmountProps[ 'currency' ] ) =>
+ ( formattedValue: string ): ReactElement | string => {
+ if ( ! currency.decimalSeparator ) {
+ return formattedValue;
+ }
+ const pattern = new RegExp(
+ `^(.*)([${ currency.decimalSeparator }])(([^${ currency.decimalSeparator }]*))$`
+ );
+ const matches = formattedValue.match( pattern );
+ if ( ! matches ) {
+ return formattedValue;
+ }
+ return (
+ <>
+ <>{ matches[ 1 ] }>
+
+
+ ${ currency.decimalSeparator }
+
+ { matches[ 3 ] }
+
+ >
+ );
+ };
+
/**
* Formats currency data into the expected format for NumberFormat.
*/
@@ -44,12 +70,7 @@ const currencyToNumberFormat = (
};
};
-type CustomFormattedMonetaryAmountProps = Omit<
- FormattedMonetaryAmountProps,
- 'currency'
-> & {
- currency: Currency | Record< string, never >;
-};
+const defaultRenderText = ( formattedAmount: string ) => formattedAmount;
/**
* FormattedMonetaryAmount component.
@@ -62,8 +83,10 @@ const FormattedMonetaryAmount = ( {
currency,
onValueChange,
displayType = 'text',
+ withSuperScript = false,
+ renderText,
...props
-}: CustomFormattedMonetaryAmountProps ): ReactElement | null => {
+}: FormattedMonetaryAmountProps ): JSX.Element | null => {
const value =
typeof rawValue === 'string' ? parseInt( rawValue, 10 ) : rawValue;
@@ -100,6 +123,12 @@ const FormattedMonetaryAmount = ( {
}
: () => void 0;
+ if ( ! renderText ) {
+ renderText = withSuperScript
+ ? applySuperscript( currency )
+ : defaultRenderText;
+ }
+
return (
);
};
diff --git a/assets/js/base/components/formatted-monetary-amount/style.scss b/assets/js/base/components/formatted-monetary-amount/style.scss
index c8f02bd1bee..a7821118ea5 100644
--- a/assets/js/base/components/formatted-monetary-amount/style.scss
+++ b/assets/js/base/components/formatted-monetary-amount/style.scss
@@ -1,3 +1,12 @@
.wc-block-components-formatted-money-amount {
white-space: nowrap;
}
+
+.formatted-money-amount-superscript {
+ vertical-align: super;
+ font-size: .5em;
+}
+
+.formatted-money-amount-separator {
+ @include visually-hidden();
+}
diff --git a/assets/js/base/components/product-price/index.tsx b/assets/js/base/components/product-price/index.tsx
index 10fa139b3b7..9120fc98b18 100644
--- a/assets/js/base/components/product-price/index.tsx
+++ b/assets/js/base/components/product-price/index.tsx
@@ -17,7 +17,7 @@ interface PriceRangeProps {
/**
* Currency configuration object
*/
- currency: Currency | Record< string, never > | undefined;
+ currency: Currency | Record< string, never >;
/**
* The maximum price for the range
*/
@@ -38,6 +38,11 @@ interface PriceRangeProps {
* **Note:** this excludes the dash in between the elements
*/
priceStyle?: React.CSSProperties | undefined;
+ /**
+ * If true, the decimal separator will be excluded and the amount after it
+ * will be wrapped in a superscript element.
+ */
+ withSuperScript: boolean | undefined;
}
const PriceRange = ( {
@@ -46,6 +51,7 @@ const PriceRange = ( {
minPrice,
priceClassName,
priceStyle = {},
+ withSuperScript = false,
}: PriceRangeProps ) => {
return (
<>
@@ -69,6 +75,7 @@ const PriceRange = ( {
currency={ currency }
value={ minPrice }
style={ priceStyle }
+ withSuperScript={ withSuperScript }
/>
—
>
@@ -89,7 +97,7 @@ interface SalePriceProps {
/**
* Currency configuration object
*/
- currency: Currency | Record< string, never > | undefined;
+ currency: Currency | Record< string, never >;
/**
* CSS class to be applied to the regular price container
*
@@ -121,7 +129,7 @@ interface SalePriceProps {
/**
* The new price during the sale
*/
- price: number | string | undefined;
+ price: number | string;
}
const SalePrice = ( {
@@ -248,6 +256,11 @@ export interface ProductPriceProps {
'marginTop' | 'marginRight' | 'marginBottom' | 'marginLeft'
>
| undefined;
+ /**
+ * If true, the decimal separator will be excluded and the amount after it
+ * will be wrapped in a superscript element.
+ */
+ withSuperScript: boolean | undefined;
}
const ProductPrice = ( {
@@ -264,6 +277,7 @@ const ProductPrice = ( {
regularPriceClassName,
regularPriceStyle,
style,
+ withSuperScript = false,
}: ProductPriceProps ): JSX.Element => {
const wrapperClassName = classNames(
className,
@@ -310,6 +324,7 @@ const ProductPrice = ( {
minPrice={ minPrice }
priceClassName={ priceClassName }
priceStyle={ priceStyle }
+ withSuperScript={ withSuperScript }
/>
);
} else if ( price ) {
@@ -322,6 +337,7 @@ const ProductPrice = ( {
currency={ currency }
value={ price }
style={ priceStyle }
+ withSuperScript={ withSuperScript }
/>
);
}
diff --git a/assets/js/shared/context/product-data-context.tsx b/assets/js/shared/context/product-data-context.tsx
index deb6f2d8d0e..7f6979060fe 100644
--- a/assets/js/shared/context/product-data-context.tsx
+++ b/assets/js/shared/context/product-data-context.tsx
@@ -70,7 +70,7 @@ export const useProductDataContext = () => useContext( ProductDataContext );
interface ProductDataContextProviderProps {
product: ProductResponseItem | null;
- children: JSX.Element | JSX.Element[];
+ children: JSX.Element | JSX.Element[] | undefined;
isLoading: boolean;
}
diff --git a/assets/js/types/type-defs/hooks.ts b/assets/js/types/type-defs/hooks.ts
index a9ee2aa58ef..53ee0fc8e07 100644
--- a/assets/js/types/type-defs/hooks.ts
+++ b/assets/js/types/type-defs/hooks.ts
@@ -63,9 +63,10 @@ export interface StoreCart {
}
export type Query = {
- catalog_visibility: 'catalog';
- per_page: number;
- page: number;
- orderby: string;
- order: string;
+ catalog_visibility?: 'catalog';
+ per_page?: number;
+ page?: number;
+ orderby?: string;
+ order?: string;
+ include?: string | number;
};
diff --git a/src/BlockTypes/ProductPriceCurrentPrice.php b/src/BlockTypes/ProductPriceCurrentPrice.php
new file mode 100644
index 00000000000..9f62e80ab30
--- /dev/null
+++ b/src/BlockTypes/ProductPriceCurrentPrice.php
@@ -0,0 +1,68 @@
+context['postId'];
+ $product = wc_get_product( $post_id );
+
+ if ( ! $product ) {
+ return null;
+ }
+
+ $classname = $attributes['className'] ?? '';
+ $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
+
+ // For variable products, show the price range.
+ if ( ! $product->is_type( 'variable' ) ) {
+ $price_display = wc_price( wc_get_price_to_display( $product ) ) . $product->get_price_suffix();
+ } else {
+ $prices = $product->get_variation_prices( 'min' );
+ $max_price = wc_price( $product->get_variation_price( 'max', true ) ) . $product->get_price_suffix();
+ $min_price = wc_price( current( $prices['price'] ) ) . $product->get_price_suffix();
+ $price_display = $min_price . ' - ' . $max_price;
+ }
+
+ return sprintf(
+ '%4$s
',
+ esc_attr( $classes_and_styles['classes'] ),
+ esc_attr( $classname ),
+ esc_attr( $classes_and_styles['styles'] ),
+ $price_display
+ );
+ }
+}
diff --git a/src/BlockTypes/ProductPriceDiscount.php b/src/BlockTypes/ProductPriceDiscount.php
new file mode 100644
index 00000000000..2ff3af3e3c3
--- /dev/null
+++ b/src/BlockTypes/ProductPriceDiscount.php
@@ -0,0 +1,91 @@
+ 0 ) {
+ $percentage_discount = floor( ( $this->calculate_discount_value( $was_price, $is_price ) / $was_price ) * 100 );
+ } else {
+ $percentage_discount = 0;
+ }
+ return $percentage_discount;
+ }
+
+ /**
+ * Include and render the block.
+ *
+ * @param array $attributes Block attributes. Default empty array.
+ * @param string $content Block content. Default empty string.
+ * @param WP_Block $block Block instance.
+ * @return string Rendered block type output.
+ */
+ protected function render( $attributes, $content, $block ) {
+ $post_id = $block->context['postId'];
+ $product = wc_get_product( $post_id );
+ if ( ! $product || ! $product->is_on_sale() ) {
+ return null;
+ }
+
+ $classname = $attributes['className'] ?? '';
+ $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
+
+ if ( ! $product->is_type( 'variable' ) ) {
+ $percentage_discount = $this->calculate_discount_percentage( $product->get_regular_price(), wc_get_price_to_display( $product ) );
+ } else {
+ $prices = $product->get_variation_prices( 'min' );
+ $percentage_discount = $this->calculate_discount_percentage( $product->get_variation_price( 'max', true ), current( $prices['price'] ) );
+ }
+
+ return sprintf(
+ '%4$s
',
+ esc_attr( $classes_and_styles['classes'] ),
+ esc_attr( $classname ),
+ esc_attr( $classes_and_styles['styles'] ),
+ $percentage_discount . '%'
+ ) . $content;
+ }
+}
diff --git a/src/BlockTypes/ProductPriceOriginalPrice.php b/src/BlockTypes/ProductPriceOriginalPrice.php
new file mode 100644
index 00000000000..0c2d019c55d
--- /dev/null
+++ b/src/BlockTypes/ProductPriceOriginalPrice.php
@@ -0,0 +1,65 @@
+context['postId'];
+ $product = wc_get_product( $post_id );
+
+ if ( ! $product || ! $product->is_on_sale() ) {
+ return null;
+ }
+
+ $classname = $attributes['className'] ?? '';
+ $classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
+
+ if ( ! $product->is_type( 'variable' ) ) {
+ $display_price = wc_price( $product->get_regular_price() ) . $product->get_price_suffix();
+ } else {
+ // For variable products, show the max price of the variations on sale.
+ $display_price = wc_price( $product->get_variation_price( 'max', true ) ) . $product->get_price_suffix();
+ }
+
+ return sprintf(
+ '%4$s
',
+ esc_attr( $classes_and_styles['classes'] ),
+ esc_attr( $classname ),
+ esc_attr( $classes_and_styles['styles'] ),
+ $display_price
+ );
+ }
+}
diff --git a/src/BlockTypesController.php b/src/BlockTypesController.php
index 21d1a062335..c961e8ac571 100644
--- a/src/BlockTypesController.php
+++ b/src/BlockTypesController.php
@@ -217,6 +217,9 @@ protected function get_block_types() {
'ProductDetails',
'SingleProduct',
'StockFilter',
+ 'ProductPriceDiscount',
+ 'ProductPriceCurrentPrice',
+ 'ProductPriceOriginalPrice',
];
$block_types = array_merge(