diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/block.js b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/block.js
deleted file mode 100644
index 441e39da6a4..00000000000
--- a/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/block.js
+++ /dev/null
@@ -1,158 +0,0 @@
-/**
- * External dependencies
- */
-import { __ } from '@wordpress/i18n';
-import { Component } from '@wordpress/element';
-import { Disabled, PanelBody, Placeholder } from '@wordpress/components';
-import { InspectorControls } from '@wordpress/block-editor';
-import ServerSideRender from '@wordpress/server-side-render';
-import PropTypes from 'prop-types';
-import GridContentControl from '@woocommerce/editor-components/grid-content-control';
-import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control';
-import ProductCategoryControl from '@woocommerce/editor-components/product-category-control';
-import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control';
-import ProductStockControl from '@woocommerce/editor-components/product-stock-control';
-import { gridBlockPreview } from '@woocommerce/resource-previews';
-import { Icon, percent } from '@wordpress/icons';
-import { getSetting } from '@woocommerce/settings';
-
-const EmptyPlaceholder = () => (
- }
- label={ __( 'On Sale Products', 'woo-gutenberg-products-block' ) }
- className="wc-block-product-on-sale"
- >
- { __(
- 'This block shows on-sale products. There are currently no discounted products in your store.',
- 'woo-gutenberg-products-block'
- ) }
-
-);
-
-/**
- * Component to handle edit mode of "On Sale Products".
- */
-class ProductOnSaleBlock extends Component {
- getInspectorControls() {
- const { attributes, setAttributes } = this.props;
- const {
- categories,
- catOperator,
- columns,
- contentVisibility,
- rows,
- orderby,
- alignButtons,
- stockStatus,
- } = attributes;
-
- return (
-
-
-
-
-
-
- setAttributes( { contentVisibility: value } )
- }
- />
-
-
-
-
-
- {
- const ids = value.map( ( { id } ) => id );
- setAttributes( { categories: ids } );
- } }
- operator={ catOperator }
- onOperatorChange={ ( value = 'any' ) =>
- setAttributes( { catOperator: value } )
- }
- />
-
-
-
-
-
- );
- }
-
- render() {
- const { attributes, name } = this.props;
-
- if ( attributes.isPreview ) {
- return gridBlockPreview;
- }
-
- return (
- <>
- { this.getInspectorControls() }
-
-
-
- >
- );
- }
-}
-
-ProductOnSaleBlock.propTypes = {
- /**
- * The attributes for this block
- */
- attributes: PropTypes.object.isRequired,
- /**
- * The register block name.
- */
- name: PropTypes.string.isRequired,
- /**
- * A callback to update attributes
- */
- setAttributes: PropTypes.func.isRequired,
-};
-
-export default ProductOnSaleBlock;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/block.tsx
new file mode 100644
index 00000000000..6e8d9a9e1be
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/block.tsx
@@ -0,0 +1,64 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { Disabled, Placeholder } from '@wordpress/components';
+import ServerSideRender from '@wordpress/server-side-render';
+import { gridBlockPreview } from '@woocommerce/resource-previews';
+import { Icon, percent } from '@wordpress/icons';
+
+/**
+ * Internal dependencies
+ */
+import { Attributes } from './types';
+import { ProductOnSaleInspectorControls } from './inspector-controls';
+
+interface Props {
+ attributes: Attributes;
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+ name: string;
+}
+
+const EmptyPlaceholder = () => (
+ }
+ label={ __( 'On Sale Products', 'woo-gutenberg-products-block' ) }
+ className="wc-block-product-on-sale"
+ >
+ { __(
+ 'This block shows on-sale products. There are currently no discounted products in your store.',
+ 'woo-gutenberg-products-block'
+ ) }
+
+);
+
+/**
+ * Component to handle edit mode of "On Sale Products".
+ */
+const ProductOnSaleBlock: React.FunctionComponent< Props > = (
+ props: Props
+) => {
+ const { attributes, setAttributes, name } = props;
+
+ if ( attributes.isPreview ) {
+ return gridBlockPreview;
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default ProductOnSaleBlock;
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/inspector-controls.tsx b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/inspector-controls.tsx
new file mode 100644
index 00000000000..bfff4e4d7cb
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/inspector-controls.tsx
@@ -0,0 +1,109 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { PanelBody } from '@wordpress/components';
+import { InspectorControls } from '@wordpress/block-editor';
+import GridContentControl from '@woocommerce/editor-components/grid-content-control';
+import GridLayoutControl from '@woocommerce/editor-components/grid-layout-control';
+import ProductCategoryControl from '@woocommerce/editor-components/product-category-control';
+import ProductOrderbyControl from '@woocommerce/editor-components/product-orderby-control';
+import ProductStockControl from '@woocommerce/editor-components/product-stock-control';
+import { getSetting } from '@woocommerce/settings';
+
+/**
+ * Internal dependencies
+ */
+import { Attributes } from './types';
+
+interface ProductOnSaleInspectorControlsProps {
+ attributes: Attributes;
+ setAttributes: ( attributes: Record< string, unknown > ) => void;
+}
+
+export const ProductOnSaleInspectorControls = (
+ props: ProductOnSaleInspectorControlsProps
+) => {
+ const { attributes, setAttributes } = props;
+ const {
+ categories,
+ catOperator,
+ columns,
+ contentVisibility,
+ rows,
+ orderby,
+ alignButtons,
+ stockStatus,
+ } = attributes;
+
+ return (
+
+
+ ( 'min_columns', 1 ) }
+ maxColumns={ getSetting< number >( 'max_columns', 6 ) }
+ minRows={ getSetting< number >( 'min_rows', 1 ) }
+ maxRows={ getSetting< number >( 'max_rows', 6 ) }
+ />
+
+
+
+ setAttributes( { contentVisibility: value } )
+ }
+ />
+
+
+
+
+
+ {
+ const ids = value.map( ( { id } ) => id );
+ setAttributes( { categories: ids } );
+ } }
+ operator={ catOperator }
+ onOperatorChange={ ( value = 'any' ) =>
+ setAttributes( { catOperator: value } )
+ }
+ />
+
+
+
+
+
+ );
+};
diff --git a/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/types.ts b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/types.ts
new file mode 100644
index 00000000000..c6311c472a3
--- /dev/null
+++ b/plugins/woocommerce-blocks/assets/js/blocks/product-on-sale/types.ts
@@ -0,0 +1,25 @@
+export interface Attributes {
+ columns: number;
+ rows: number;
+ alignButtons: boolean;
+ contentVisibility: {
+ image: boolean;
+ title: boolean;
+ price: boolean;
+ rating: boolean;
+ button: boolean;
+ };
+ categories: Array< number >;
+ catOperator: string;
+ isPreview: boolean;
+ stockStatus: Array< string >;
+ editMode: boolean;
+ orderby:
+ | 'date'
+ | 'popularity'
+ | 'price_asc'
+ | 'price_desc'
+ | 'rating'
+ | 'title'
+ | 'menu_order';
+}