diff --git a/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js b/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js
index adaf84d472e..9ba96579089 100644
--- a/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js
+++ b/assets/js/atomic/blocks/product-elements/add-to-cart/product-types/variable/variation-attributes/attribute-select-control.js
@@ -6,10 +6,9 @@ import { decodeEntities } from '@wordpress/html-entities';
import { SelectControl } from 'wordpress-components';
import { useEffect } from 'react';
import classnames from 'classnames';
-import {
- ValidationInputError,
- useValidationContext,
-} from '@woocommerce/base-context';
+import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
+import { useDispatch, useSelect } from '@wordpress/data';
// Default option for select boxes.
const selectAnOption = {
@@ -32,8 +31,16 @@ const AttributeSelectControl = ( {
'woo-gutenberg-products-block'
),
} ) => {
- const { getValidationError, setValidationErrors, clearValidationError } =
- useValidationContext();
+ const { setValidationErrors, clearValidationError } = useDispatch(
+ VALIDATION_STORE_KEY
+ );
+
+ const { getValidationError } = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ getValidationError: store.getValidationError(),
+ };
+ } );
const errorId = attributeName;
const error = getValidationError( errorId ) || {};
diff --git a/assets/js/base/components/cart-checkout/address-form/address-form.tsx b/assets/js/base/components/cart-checkout/address-form/address-form.tsx
index 3b3a4e238be..4bec91362dc 100644
--- a/assets/js/base/components/cart-checkout/address-form/address-form.tsx
+++ b/assets/js/base/components/cart-checkout/address-form/address-form.tsx
@@ -10,7 +10,6 @@ import {
BillingStateInput,
ShippingStateInput,
} from '@woocommerce/base-components/state-input';
-import { useValidationContext } from '@woocommerce/base-context';
import { useEffect, useMemo } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { withInstanceId } from '@wordpress/compose';
@@ -22,6 +21,11 @@ import {
defaultAddressFields,
EnteredAddress,
} from '@woocommerce/settings';
+import { useSelect, useDispatch } from '@wordpress/data';
+import {
+ VALIDATION_STORE_KEY,
+ FieldValidationStatus,
+} from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -32,7 +36,9 @@ import prepareAddressFields from './prepare-address-fields';
// values without having set the country first, show an error.
const validateShippingCountry = (
values: EnteredAddress,
- setValidationErrors: ( errors: Record< string, unknown > ) => void,
+ setValidationErrors: (
+ errors: Record< string, FieldValidationStatus >
+ ) => void,
clearValidationError: ( error: string ) => void,
hasValidationError: boolean
): void => {
@@ -87,8 +93,14 @@ const AddressForm = ( {
type = 'shipping',
values,
}: AddressFormProps ): JSX.Element => {
- const { getValidationError, setValidationErrors, clearValidationError } =
- useValidationContext();
+ const { setValidationErrors, clearValidationError } = useDispatch(
+ VALIDATION_STORE_KEY
+ );
+
+ const getValidationError = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return store.getValidationError();
+ } );
const currentFields = useShallowEqual( fields );
diff --git a/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx b/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx
index 8daece96a6a..a4eafb9ed97 100644
--- a/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx
+++ b/assets/js/base/components/cart-checkout/shipping-calculator/address.tsx
@@ -5,8 +5,9 @@ import { __ } from '@wordpress/i18n';
import Button from '@woocommerce/base-components/button';
import { useState } from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
-import { useValidationContext } from '@woocommerce/base-context';
import type { EnteredAddress, AddressFields } from '@woocommerce/settings';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
+import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
@@ -25,12 +26,18 @@ const ShippingCalculatorAddress = ( {
addressFields,
}: ShippingCalculatorAddressProps ): JSX.Element => {
const [ address, setAddress ] = useState( initialAddress );
- const { hasValidationErrors, showAllValidationErrors } =
- useValidationContext();
+ const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
+
+ const { hasValidationErrors } = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ hasValidationErrors: store.hasValidationErrors,
+ };
+ } );
const validateSubmit = () => {
showAllValidationErrors();
- return ! hasValidationErrors;
+ return ! hasValidationErrors();
};
return (
diff --git a/assets/js/base/components/cart-checkout/totals/coupon/index.tsx b/assets/js/base/components/cart-checkout/totals/coupon/index.tsx
index 87b6d29ed41..1d31203a279 100644
--- a/assets/js/base/components/cart-checkout/totals/coupon/index.tsx
+++ b/assets/js/base/components/cart-checkout/totals/coupon/index.tsx
@@ -4,15 +4,14 @@
import { __ } from '@wordpress/i18n';
import { useState, useEffect, useRef } from '@wordpress/element';
import Button from '@woocommerce/base-components/button';
-import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
+import { Panel } from '@woocommerce/blocks-checkout';
import Label from '@woocommerce/base-components/label';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { withInstanceId } from '@wordpress/compose';
-import {
- ValidationInputError,
- useValidationContext,
-} from '@woocommerce/base-context';
-import { Panel } from '@woocommerce/blocks-checkout';
+import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
+import ValidationInputError from '@woocommerce/base-components/validation-input-error';
+import { useSelect } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -46,7 +45,16 @@ export const TotalsCoupon = ( {
}: TotalsCouponProps ): JSX.Element => {
const [ couponValue, setCouponValue ] = useState( '' );
const currentIsLoading = useRef( false );
- const { getValidationError, getValidationErrorId } = useValidationContext();
+ const { getValidationError, getValidationErrorId } = useSelect(
+ ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ getValidationError: store.getValidationError(),
+ getValidationErrorId: store.getValidationErrorId(),
+ };
+ }
+ );
+
const validationError = getValidationError( 'coupon' );
useEffect( () => {
diff --git a/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx b/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx
index cc4827e1bb5..d87bef2c6d4 100644
--- a/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx
+++ b/assets/js/base/components/cart-checkout/totals/coupon/stories/index.tsx
@@ -3,11 +3,9 @@
*/
import { useArgs } from '@storybook/client-api';
import { Story, Meta } from '@storybook/react';
-import {
- useValidationContext,
- ValidationContextProvider,
-} from '@woocommerce/base-context';
import { INTERACTION_TIMEOUT } from '@woocommerce/storybook-controls';
+import { useDispatch } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -52,7 +50,7 @@ LoadingState.args = {
};
export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
- const { setValidationErrors } = useValidationContext();
+ const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
setValidationErrors( { coupon: INVALID_COUPON_ERROR } );
@@ -61,10 +59,6 @@ export const ErrorState: Story< TotalsCouponProps > = ( args ) => {
ErrorState.decorators = [
( StoryComponent ) => {
- return (
-
-
-
- );
+ return ;
},
];
diff --git a/assets/js/base/components/combobox/index.tsx b/assets/js/base/components/combobox/index.tsx
index f0a492fa948..01d3a1fe181 100644
--- a/assets/js/base/components/combobox/index.tsx
+++ b/assets/js/base/components/combobox/index.tsx
@@ -6,11 +6,10 @@ import { __ } from '@wordpress/i18n';
import { useEffect, useRef } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import { ComboboxControl } from 'wordpress-components';
-import {
- ValidationInputError,
- useValidationContext,
-} from '@woocommerce/base-context';
+import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
import { isObject } from '@woocommerce/types';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -55,8 +54,13 @@ const Combobox = ( {
instanceId = '0',
autoComplete = 'off',
}: ComboboxProps ): JSX.Element => {
- const { getValidationError, setValidationErrors, clearValidationError } =
- useValidationContext();
+ const { setValidationErrors, clearValidationError } = useDispatch(
+ VALIDATION_STORE_KEY
+ );
+ const getValidationError = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return store.getValidationError();
+ } );
const controlRef = useRef< HTMLDivElement >( null );
const controlId = id || 'control-' + instanceId;
diff --git a/assets/js/base/components/country-input/stories/index.tsx b/assets/js/base/components/country-input/stories/index.tsx
index bc2ffa59475..bf2cc4c083c 100644
--- a/assets/js/base/components/country-input/stories/index.tsx
+++ b/assets/js/base/components/country-input/stories/index.tsx
@@ -2,11 +2,9 @@
* External dependencies
*/
import { Story, Meta } from '@storybook/react';
-import {
- useValidationContext,
- ValidationContextProvider,
-} from '@woocommerce/base-context';
+import { useDispatch } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -31,21 +29,16 @@ export default {
options: { table: { disable: true } },
value: { control: false },
},
- decorators: [
- ( StoryComponent ) => (
-
-
-
- ),
- ],
+ decorators: [ ( StoryComponent ) => ],
} as Meta< CountryInputWithCountriesProps >;
const Template: Story< CountryInputWithCountriesProps > = ( args ) => {
const [ selectedCountry, selectCountry ] = useState< CountryCode | '' >(
''
);
- const { clearValidationError, showValidationError } =
- useValidationContext();
+ const { clearValidationError, showValidationError } = useDispatch(
+ VALIDATION_STORE_KEY
+ );
useEffect( () => {
showValidationError( 'country' );
diff --git a/assets/js/base/components/state-input/state-input.tsx b/assets/js/base/components/state-input/state-input.tsx
index 9018bfb2316..4ef0eee4fe8 100644
--- a/assets/js/base/components/state-input/state-input.tsx
+++ b/assets/js/base/components/state-input/state-input.tsx
@@ -5,11 +5,11 @@ import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { useCallback, useMemo, useEffect, useRef } from '@wordpress/element';
import classnames from 'classnames';
+import { ValidatedTextInput } from '@woocommerce/base-components/text-input';
/**
* Internal dependencies
*/
-import { ValidatedTextInput } from '../text-input';
import Combobox from '../combobox';
import './style.scss';
import type { StateInputWithStatesProps } from './StateInputProps';
diff --git a/assets/js/base/components/text-input/validated-text-input.tsx b/assets/js/base/components/text-input/validated-text-input.tsx
index 536fed53c05..374393585d1 100644
--- a/assets/js/base/components/text-input/validated-text-input.tsx
+++ b/assets/js/base/components/text-input/validated-text-input.tsx
@@ -4,33 +4,21 @@
import { __ } from '@wordpress/i18n';
import { useCallback, useRef, useEffect, useState } from 'react';
import classnames from 'classnames';
-import {
- ValidationInputError,
- useValidationContext,
-} from '@woocommerce/base-context';
import { withInstanceId } from '@wordpress/compose';
import { isString } from '@woocommerce/types';
+import { dispatch, useSelect } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import TextInput from './text-input';
import './style.scss';
+import { ValidationInputError } from '../validation-input-error';
-interface ValidatedTextInputPropsWithId {
- instanceId?: string;
- id: string;
-}
-
-interface ValidatedTextInputPropsWithInstanceId {
- instanceId: string;
+interface ValidatedTextInputProps {
id?: string;
-}
-
-type ValidatedTextInputProps = (
- | ValidatedTextInputPropsWithId
- | ValidatedTextInputPropsWithInstanceId
- ) & {
+ instanceId: string;
className?: string;
ariaDescribedBy?: string;
errorId?: string;
@@ -39,7 +27,7 @@ type ValidatedTextInputProps = (
errorMessage?: string;
onChange: ( newValue: string ) => void;
value: string;
-};
+}
const ValidatedTextInput = ( {
className,
@@ -53,20 +41,26 @@ const ValidatedTextInput = ( {
errorMessage: passedErrorMessage = '',
value = '',
...rest
-}: ValidatedTextInputProps ) => {
+}: ValidatedTextInputProps ): JSX.Element => {
const [ isPristine, setIsPristine ] = useState( true );
const inputRef = useRef< HTMLInputElement >( null );
- const {
- getValidationError,
- hideValidationError,
- setValidationErrors,
- clearValidationError,
- getValidationErrorId,
- } = useValidationContext();
+
+ const { setValidationErrors, hideValidationError, clearValidationError } =
+ dispatch( VALIDATION_STORE_KEY );
const textInputId =
typeof id !== 'undefined' ? id : 'textinput-' + instanceId;
const errorIdString = errorId !== undefined ? errorId : textInputId;
+ const { getValidationError, getValidationErrorId } = useSelect(
+ ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ getValidationError: store.getValidationError(),
+ getValidationErrorId: store.getValidationErrorId(),
+ };
+ }
+ );
+
const validateInput = useCallback(
( errorsHidden = true ) => {
const inputObject = inputRef.current || null;
@@ -79,7 +73,7 @@ const ValidatedTextInput = ( {
if ( inputIsValid ) {
clearValidationError( errorIdString );
} else {
- setValidationErrors( {
+ const validationErrors = {
[ errorIdString ]: {
message:
inputObject.validationMessage ||
@@ -89,7 +83,8 @@ const ValidatedTextInput = ( {
),
hidden: errorsHidden,
},
- } );
+ };
+ setValidationErrors( validationErrors );
}
},
[ clearValidationError, errorIdString, setValidationErrors ]
@@ -129,17 +124,13 @@ const ValidatedTextInput = ( {
};
}, [ clearValidationError, errorIdString ] );
- // @todo - When useValidationContext is converted to TypeScript, remove this cast and use the correct type.
- const errorMessage = ( getValidationError( errorIdString ) || {} ) as {
- message?: string;
- hidden?: boolean;
- };
+ const errorMessage = getValidationError( errorIdString );
if ( isString( passedErrorMessage ) && passedErrorMessage !== '' ) {
errorMessage.message = passedErrorMessage;
}
- const hasError = errorMessage.message && ! errorMessage.hidden;
+ const hasError = errorMessage?.message && ! errorMessage?.hidden;
const describedBy =
showError && hasError && getValidationErrorId( errorIdString )
? getValidationErrorId( errorIdString )
diff --git a/assets/js/base/components/tsconfig.json b/assets/js/base/components/tsconfig.json
index 4488bff8cf0..6ba83ca004f 100644
--- a/assets/js/base/components/tsconfig.json
+++ b/assets/js/base/components/tsconfig.json
@@ -12,6 +12,7 @@
"../hocs",
"../../atomic/utils",
"../../atomic/blocks/component-init.js",
+ "../../data",
"../../shared/context"
],
"exclude": [ "**/test/**" ]
diff --git a/assets/js/base/components/validation-input-error/index.tsx b/assets/js/base/components/validation-input-error/index.tsx
new file mode 100644
index 00000000000..bd03afee153
--- /dev/null
+++ b/assets/js/base/components/validation-input-error/index.tsx
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+import { useSelect } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+interface ValidationInputErrorProps {
+ errorMessage?: string;
+ propertyName?: string;
+ elementId?: string;
+}
+
+export const ValidationInputError = ( {
+ errorMessage = '',
+ propertyName = '',
+ elementId = '',
+}: ValidationInputErrorProps ): JSX.Element | null => {
+ const { getValidationError, getValidationErrorId } = useSelect(
+ ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ getValidationError: store.getValidationError(),
+ getValidationErrorId: store.getValidationErrorId(),
+ };
+ }
+ );
+ const validationError = getValidationError( propertyName );
+
+ if ( ! errorMessage || typeof errorMessage !== 'string' ) {
+ const error = validationError || {};
+ if ( error.message && ! error.hidden ) {
+ errorMessage = error.message;
+ } else {
+ return null;
+ }
+ }
+
+ return (
+
+ );
+};
+
+export default ValidationInputError;
diff --git a/assets/js/base/context/providers/validation/components/validation-input-error/style.scss b/assets/js/base/components/validation-input-error/style.scss
similarity index 100%
rename from assets/js/base/context/providers/validation/components/validation-input-error/style.scss
rename to assets/js/base/components/validation-input-error/style.scss
diff --git a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts
index aa5cd0bf851..b3ff5960823 100644
--- a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts
+++ b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts
@@ -5,7 +5,10 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { useDispatch, useSelect } from '@wordpress/data';
-import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
+import {
+ CART_STORE_KEY as storeKey,
+ VALIDATION_STORE_KEY,
+} from '@woocommerce/block-data';
import { decodeEntities } from '@wordpress/html-entities';
import type { StoreCartCoupon } from '@woocommerce/types';
@@ -13,7 +16,6 @@ import type { StoreCartCoupon } from '@woocommerce/types';
* Internal dependencies
*/
import { useStoreCart } from './use-store-cart';
-import { useValidationContext } from '../../providers/validation';
/**
* This is a custom hook for loading the Store API /cart/coupons endpoint and an
@@ -27,7 +29,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
const { cartCoupons, cartIsLoading } = useStoreCart();
const { createErrorNotice } = useDispatch( 'core/notices' );
const { createNotice } = useDispatch( 'core/notices' );
- const { setValidationErrors } = useValidationContext();
+ const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
const {
applyCoupon,
diff --git a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts
index 309185629e4..5a1f73e3d79 100644
--- a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts
+++ b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts
@@ -12,11 +12,11 @@ import LoadingMask from '@woocommerce/base-components/loading-mask';
import type { PaymentMethodInterface } from '@woocommerce/types';
import { useSelect } from '@wordpress/data';
import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+import { ValidationInputError } from '@woocommerce/base-components/validation-input-error';
/**
* Internal dependencies
*/
-import { ValidationInputError } from '../../providers/validation';
import { useStoreCart } from '../cart/use-store-cart';
import { useStoreCartCoupons } from '../cart/use-store-cart-coupons';
import { useEmitResponse } from '../use-emit-response';
diff --git a/assets/js/base/context/hooks/use-validation.ts b/assets/js/base/context/hooks/use-validation.ts
index 8145d5a6dd3..a4902ba311f 100644
--- a/assets/js/base/context/hooks/use-validation.ts
+++ b/assets/js/base/context/hooks/use-validation.ts
@@ -6,27 +6,31 @@ import type {
ValidationData,
ValidationContextError,
} from '@woocommerce/type-defs/contexts';
-
-/**
- * Internal dependencies
- */
-import { useValidationContext } from '../providers/validation/';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Custom hook for setting for adding errors to the validation system.
*/
export const useValidation = (): ValidationData => {
const {
- hasValidationErrors,
- getValidationError,
clearValidationError,
hideValidationError,
setValidationErrors,
- } = useValidationContext();
+ } = useDispatch( VALIDATION_STORE_KEY );
+ const { hasValidationErrors, getValidationError } = useSelect(
+ ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ hasValidationErrors: store.hasValidationErrors,
+ getValidationError: store.getValidationError(),
+ };
+ }
+ );
const prefix = 'extensions-errors';
return {
- hasValidationErrors,
+ hasValidationErrors: hasValidationErrors(),
getValidationError: useCallback(
( validationErrorId: string ) =>
getValidationError( `${ prefix }-${ validationErrorId }` ),
diff --git a/assets/js/base/context/providers/add-to-cart-form/form-state/index.js b/assets/js/base/context/providers/add-to-cart-form/form-state/index.js
index 4f53876d59d..916dbd3b95e 100644
--- a/assets/js/base/context/providers/add-to-cart-form/form-state/index.js
+++ b/assets/js/base/context/providers/add-to-cart-form/form-state/index.js
@@ -15,6 +15,8 @@ import {
productSupportsAddToCartForm,
} from '@woocommerce/base-utils';
import { useDispatch } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
+
/**
* Internal dependencies
*/
@@ -28,7 +30,6 @@ import {
emitEventWithAbort,
reducer as emitReducer,
} from './event-emit';
-import { useValidationContext } from '../../validation';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import { removeNoticesByStatus } from '../../../../../utils/notices';
@@ -100,9 +101,12 @@ export const AddToCartFormStateContextProvider = ( {
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
const currentObservers = useShallowEqual( observers );
const { createErrorNotice } = useDispatch( 'core/notices' );
- const { setValidationErrors } = useValidationContext();
- const { isSuccessResponse, isErrorResponse, isFailResponse } =
- useEmitResponse();
+ const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
+ const {
+ isSuccessResponse,
+ isErrorResponse,
+ isFailResponse,
+ } = useEmitResponse();
/**
* @type {AddToCartFormEventRegistration}
diff --git a/assets/js/base/context/providers/add-to-cart-form/form/index.js b/assets/js/base/context/providers/add-to-cart-form/form/index.js
index 817d17405d3..16ae0ea744f 100644
--- a/assets/js/base/context/providers/add-to-cart-form/form/index.js
+++ b/assets/js/base/context/providers/add-to-cart-form/form/index.js
@@ -2,7 +2,6 @@
* Internal dependencies
*/
import { AddToCartFormStateContextProvider } from '../form-state';
-import { ValidationContextProvider } from '../../validation';
import FormSubmit from './submit';
/**
@@ -21,14 +20,12 @@ export const AddToCartFormContextProvider = ( {
showFormElements,
} ) => {
return (
-
-
- { children }
-
-
-
+
+ { children }
+
+
);
};
diff --git a/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js b/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js
index ba0c0736af9..601ac9acb6d 100644
--- a/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js
+++ b/assets/js/base/context/providers/add-to-cart-form/form/submit/index.js
@@ -6,13 +6,13 @@ import triggerFetch from '@wordpress/api-fetch';
import { useEffect, useCallback, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { triggerAddedToCartEvent } from '@woocommerce/base-utils';
-import { useDispatch } from '@wordpress/data';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import { useAddToCartFormContext } from '../../form-state';
-import { useValidationContext } from '../../../validation';
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
/**
@@ -30,15 +30,18 @@ const FormSubmit = () => {
isProcessing,
requestParams,
} = useAddToCartFormContext();
- const { hasValidationErrors, showAllValidationErrors } =
- useValidationContext();
+ const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
+ const hasValidationErrors = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return store.hasValidationErrors;
+ } );
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
const { receiveCart } = useStoreCart();
const [ isSubmitting, setIsSubmitting ] = useState( false );
const doSubmit = ! hasError && isProcessing;
const checkValidationContext = useCallback( () => {
- if ( hasValidationErrors ) {
+ if ( hasValidationErrors() ) {
showAllValidationErrors();
return {
type: 'error',
diff --git a/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx b/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx
index 7aad5b14623..bc4aa16c898 100644
--- a/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx
+++ b/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx
@@ -14,7 +14,10 @@ import {
import { usePrevious } from '@woocommerce/base-hooks';
import deprecated from '@wordpress/deprecated';
import { useDispatch, useSelect } from '@wordpress/data';
-import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+import {
+ CHECKOUT_STORE_KEY,
+ VALIDATION_STORE_KEY,
+} from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -22,7 +25,6 @@ import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import type { CheckoutEventsContextType } from './types';
import { useEventEmitters, reducer as emitReducer } from './event-emit';
import { STATUS } from '../../../../../data/checkout/constants';
-import { useValidationContext } from '../../validation';
import { useStoreEvents } from '../../../hooks/use-store-events';
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
import { useEmitResponse } from '../../../hooks/use-emit-response';
@@ -64,7 +66,7 @@ export const CheckoutEventsProvider = ( {
checkoutActions.setRedirectUrl( redirectUrl );
}
- const { setValidationErrors } = useValidationContext();
+ const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
const { createErrorNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
diff --git a/assets/js/base/context/providers/cart-checkout/checkout-processor.js b/assets/js/base/context/providers/cart-checkout/checkout-processor.js
index aad14e660ca..2a34e52c1e2 100644
--- a/assets/js/base/context/providers/cart-checkout/checkout-processor.js
+++ b/assets/js/base/context/providers/cart-checkout/checkout-processor.js
@@ -15,7 +15,10 @@ import {
formatStoreApiErrorMessage,
} from '@woocommerce/base-utils';
import { useDispatch, useSelect } from '@wordpress/data';
-import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+import {
+ CHECKOUT_STORE_KEY,
+ VALIDATION_STORE_KEY,
+} from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -25,9 +28,9 @@ import { useCheckoutEventsContext } from './checkout-events';
import { useShippingDataContext } from './shipping';
import { useCustomerDataContext } from './customer';
import { usePaymentMethodDataContext } from './payment-methods';
-import { useValidationContext } from '../validation';
import { useStoreCart } from '../../hooks/cart/use-store-cart';
import { useStoreNoticesContext } from '../store-notices';
+
/**
* CheckoutProcessor component.
*
@@ -58,7 +61,9 @@ const CheckoutProcessor = () => {
const { setHasError, processCheckoutResponse } =
useDispatch( CHECKOUT_STORE_KEY );
- const { hasValidationErrors } = useValidationContext();
+ const hasValidationErrors = useSelect(
+ ( select ) => select( VALIDATION_STORE_KEY ).hasValidationErrors
+ );
const { shippingErrorStatus } = useShippingDataContext();
const { billingAddress, shippingAddress } = useCustomerDataContext();
const { cartNeedsPayment, cartNeedsShipping, receiveCart } = useStoreCart();
@@ -87,7 +92,7 @@ const CheckoutProcessor = () => {
}, [ activePaymentMethod, expressPaymentMethods, paymentMethods ] );
const checkoutWillHaveError =
- ( hasValidationErrors && ! isExpressPaymentMethodActive ) ||
+ ( hasValidationErrors() && ! isExpressPaymentMethodActive ) ||
currentPaymentStatus.hasError ||
shippingErrorStatus.hasError;
@@ -128,7 +133,7 @@ const CheckoutProcessor = () => {
}, [ billingAddress, shippingAddress, redirectUrl ] );
const checkValidation = useCallback( () => {
- if ( hasValidationErrors ) {
+ if ( hasValidationErrors() ) {
return false;
}
if ( currentPaymentStatus.hasError ) {
diff --git a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx
index 11c334872e4..7761bd53c7b 100644
--- a/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx
+++ b/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx
@@ -12,7 +12,10 @@ import {
} from '@wordpress/element';
import { objectHasProp } from '@woocommerce/types';
import { useDispatch, useSelect } from '@wordpress/data';
-import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+import {
+ CHECKOUT_STORE_KEY,
+ VALIDATION_STORE_KEY,
+} from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -39,7 +42,6 @@ import {
emitEventWithAbort,
reducer as emitReducer,
} from './event-emit';
-import { useValidationContext } from '../../validation';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import { getCustomerPaymentMethods } from './utils';
@@ -77,9 +79,10 @@ export const PaymentMethodDataProvider = ( {
};
} );
const { isEditor, getPreviewData } = useEditorContext();
- const { setValidationErrors } = useValidationContext();
- const { createErrorNotice: addErrorNotice, removeNotice } =
- useDispatch( 'core/notices' );
+ const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
+ const { createErrorNotice: addErrorNotice, removeNotice } = useDispatch(
+ 'core/notices'
+ );
const {
isSuccessResponse,
isErrorResponse,
diff --git a/assets/js/base/context/providers/index.js b/assets/js/base/context/providers/index.js
index c0214fcb255..88dacc9223e 100644
--- a/assets/js/base/context/providers/index.js
+++ b/assets/js/base/context/providers/index.js
@@ -3,7 +3,6 @@ export * from './add-to-cart-form';
export * from './cart-checkout';
export * from './store-notices';
export * from './store-snackbar-notices';
-export * from './validation';
export * from './container-width-context';
export * from './editor-context';
export * from './query-state-context';
diff --git a/assets/js/base/context/providers/validation/components/index.js b/assets/js/base/context/providers/validation/components/index.js
deleted file mode 100644
index ab9c392a215..00000000000
--- a/assets/js/base/context/providers/validation/components/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './validation-input-error';
diff --git a/assets/js/base/context/providers/validation/components/validation-input-error/index.js b/assets/js/base/context/providers/validation/components/validation-input-error/index.js
deleted file mode 100644
index fbeca642859..00000000000
--- a/assets/js/base/context/providers/validation/components/validation-input-error/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * External dependencies
- */
-import PropTypes from 'prop-types';
-
-/**
- * Internal dependencies
- */
-import { useValidationContext } from '../../context';
-import './style.scss';
-
-export const ValidationInputError = ( {
- errorMessage = '',
- propertyName = '',
- elementId = '',
-} ) => {
- const { getValidationError, getValidationErrorId } = useValidationContext();
-
- if ( ! errorMessage || typeof errorMessage !== 'string' ) {
- const error = getValidationError( propertyName ) || {};
- if ( error.message && ! error.hidden ) {
- errorMessage = error.message;
- } else {
- return null;
- }
- }
-
- return (
-
- );
-};
-
-ValidationInputError.propTypes = {
- errorMessage: PropTypes.string,
- propertyName: PropTypes.string,
- elementId: PropTypes.string,
-};
-
-export default ValidationInputError;
diff --git a/assets/js/base/context/providers/validation/context.js b/assets/js/base/context/providers/validation/context.js
deleted file mode 100644
index 1ec1017d991..00000000000
--- a/assets/js/base/context/providers/validation/context.js
+++ /dev/null
@@ -1,257 +0,0 @@
-/**
- * External dependencies
- */
-import {
- createContext,
- useCallback,
- useContext,
- useState,
-} from '@wordpress/element';
-import { pickBy } from 'lodash';
-import isShallowEqual from '@wordpress/is-shallow-equal';
-
-/**
- * @typedef { import('@woocommerce/type-defs/contexts').ValidationContext } ValidationContext
- * @typedef {import('react')} React
- */
-
-const ValidationContext = createContext( {
- getValidationError: () => '',
- setValidationErrors: ( errors ) => void errors,
- clearValidationError: ( property ) => void property,
- clearAllValidationErrors: () => void null,
- hideValidationError: () => void null,
- showValidationError: () => void null,
- showAllValidationErrors: () => void null,
- hasValidationErrors: false,
- getValidationErrorId: ( errorId ) => errorId,
-} );
-
-/**
- * @return {ValidationContext} The context values for the validation context.
- */
-export const useValidationContext = () => {
- return useContext( ValidationContext );
-};
-
-/**
- * Validation context provider
- *
- * Any children of this context will be exposed to validation state and helpers
- * for tracking validation.
- *
- * @param {Object} props Incoming props for the component.
- * @param {JSX.Element} props.children What react elements are wrapped by this component.
- */
-export const ValidationContextProvider = ( { children } ) => {
- const [ validationErrors, updateValidationErrors ] = useState( {} );
-
- /**
- * This retrieves any validation error message that exists in state for the
- * given property name.
- *
- * @param {string} property The property the error message is for.
- *
- * @return {Object} The error object for the given property.
- */
- const getValidationError = useCallback(
- ( property ) => validationErrors[ property ],
- [ validationErrors ]
- );
-
- /**
- * Provides an id for the validation error that can be used to fill out
- * aria-describedby attribute values.
- *
- * @param {string} errorId The input css id the validation error is related
- * to.
- * @return {string} The id to use for the validation error container.
- */
- const getValidationErrorId = useCallback(
- ( errorId ) => {
- const error = validationErrors[ errorId ];
- if ( ! error || error.hidden ) {
- return '';
- }
- return `validate-error-${ errorId }`;
- },
- [ validationErrors ]
- );
-
- /**
- * Clears any validation error that exists in state for the given property
- * name.
- *
- * @param {string} property The name of the property to clear if exists in
- * validation error state.
- */
- const clearValidationError = useCallback(
- /**
- * Callback that is memoized.
- *
- * @param {string} property
- */
- ( property ) => {
- updateValidationErrors(
- /**
- * Callback for validation Errors handling.
- *
- * @param {Object} prevErrors
- */
- ( prevErrors ) => {
- if ( ! prevErrors[ property ] ) {
- return prevErrors;
- }
-
- const {
- // eslint-disable-next-line no-unused-vars -- this is intentional to omit the dynamic property from the returned object.
- [ property ]: clearedProperty,
- ...newErrors
- } = prevErrors;
- return newErrors;
- }
- );
- },
- []
- );
-
- /**
- * Clears the entire validation error state.
- */
- const clearAllValidationErrors = useCallback(
- () => void updateValidationErrors( {} ),
- []
- );
-
- /**
- * Used to record new validation errors in the state.
- *
- * @param {Object} newErrors An object where keys are the property names the
- * validation error is for and values are the
- * validation error message displayed to the user.
- */
- const setValidationErrors = useCallback( ( newErrors ) => {
- if ( ! newErrors ) {
- return;
- }
- updateValidationErrors( ( prevErrors ) => {
- newErrors = pickBy( newErrors, ( error, property ) => {
- if ( typeof error.message !== 'string' ) {
- return false;
- }
- if ( prevErrors.hasOwnProperty( property ) ) {
- return ! isShallowEqual( prevErrors[ property ], error );
- }
- return true;
- } );
- if ( Object.values( newErrors ).length === 0 ) {
- return prevErrors;
- }
- return {
- ...prevErrors,
- ...newErrors,
- };
- } );
- }, [] );
-
- /**
- * Used to update a validation error.
- *
- * @param {string} property The name of the property to update.
- * @param {Object} newError New validation error object.
- */
- const updateValidationError = useCallback( ( property, newError ) => {
- updateValidationErrors( ( prevErrors ) => {
- if ( ! prevErrors.hasOwnProperty( property ) ) {
- return prevErrors;
- }
- const updatedError = {
- ...prevErrors[ property ],
- ...newError,
- };
- return isShallowEqual( prevErrors[ property ], updatedError )
- ? prevErrors
- : {
- ...prevErrors,
- [ property ]: updatedError,
- };
- } );
- }, [] );
-
- /**
- * Given a property name and if an associated error exists, it sets its
- * `hidden` value to true.
- *
- * @param {string} property The name of the property to set the `hidden`
- * value to true.
- */
- const hideValidationError = useCallback(
- ( property ) =>
- void updateValidationError( property, {
- hidden: true,
- } ),
- [ updateValidationError ]
- );
-
- /**
- * Given a property name and if an associated error exists, it sets its
- * `hidden` value to false.
- *
- * @param {string} property The name of the property to set the `hidden`
- * value to false.
- */
- const showValidationError = useCallback(
- ( property ) =>
- void updateValidationError( property, {
- hidden: false,
- } ),
- [ updateValidationError ]
- );
-
- /**
- * Sets the `hidden` value of all errors to `false`.
- */
- const showAllValidationErrors = useCallback(
- () =>
- void updateValidationErrors( ( prevErrors ) => {
- const updatedErrors = {};
-
- Object.keys( prevErrors ).forEach( ( property ) => {
- if ( prevErrors[ property ].hidden ) {
- updatedErrors[ property ] = {
- ...prevErrors[ property ],
- hidden: false,
- };
- }
- } );
-
- if ( Object.values( updatedErrors ).length === 0 ) {
- return prevErrors;
- }
-
- return {
- ...prevErrors,
- ...updatedErrors,
- };
- } ),
- []
- );
-
- const context = {
- getValidationError,
- setValidationErrors,
- clearValidationError,
- clearAllValidationErrors,
- hideValidationError,
- showValidationError,
- showAllValidationErrors,
- hasValidationErrors: Object.keys( validationErrors ).length > 0,
- getValidationErrorId,
- };
-
- return (
-
- { children }
-
- );
-};
diff --git a/assets/js/base/context/providers/validation/index.js b/assets/js/base/context/providers/validation/index.js
deleted file mode 100644
index aba55551f67..00000000000
--- a/assets/js/base/context/providers/validation/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './context';
-export * from './components';
diff --git a/assets/js/blocks/cart/block.js b/assets/js/blocks/cart/block.js
index 98f1767f129..ac907d3cace 100644
--- a/assets/js/blocks/cart/block.js
+++ b/assets/js/blocks/cart/block.js
@@ -6,7 +6,6 @@ import { useStoreCart } from '@woocommerce/base-context/hooks';
import { useEffect } from '@wordpress/element';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import {
- ValidationContextProvider,
StoreNoticesContainer,
SnackbarNoticesContainer,
} from '@woocommerce/base-context';
@@ -39,9 +38,7 @@ const Cart = ( { children, attributes = {} } ) => {
hasDarkControls,
} }
>
-
- { children }
-
+ { children }
);
diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx
index 19a043789e1..4b96f45db57 100644
--- a/assets/js/blocks/checkout/block.tsx
+++ b/assets/js/blocks/checkout/block.tsx
@@ -6,8 +6,6 @@ import classnames from 'classnames';
import { createInterpolateElement, useEffect } from '@wordpress/element';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import {
- useValidationContext,
- ValidationContextProvider,
CheckoutProvider,
SnackbarNoticesContainer,
} from '@woocommerce/base-context';
@@ -17,8 +15,11 @@ import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
-import { useSelect } from '@wordpress/data';
-import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
+import { useDispatch, useSelect } from '@wordpress/data';
+import {
+ CHECKOUT_STORE_KEY,
+ VALIDATION_STORE_KEY,
+} from '@woocommerce/block-data';
/**
* Internal dependencies
@@ -120,8 +121,13 @@ const ScrollOnError = ( {
};
}
);
- const { hasValidationErrors, showAllValidationErrors } =
- useValidationContext();
+ const { hasValidationErrors } = useSelect( ( select ) => {
+ const store = select( VALIDATION_STORE_KEY );
+ return {
+ hasValidationErrors: store.hasValidationErrors(),
+ };
+ } );
+ const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
const hasErrorsToDisplay =
checkoutIsIdle &&
@@ -181,24 +187,21 @@ const Block = ( {
-
- { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
-
-
-
-
- { children }
-
-
-
-
-
-
+ { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
+
+
+
+
+ { children }
+
+
+
+
+
);
diff --git a/assets/js/data/checkout/types.ts b/assets/js/data/checkout/types.ts
index 94c2f6b0d92..64a0de63bc2 100644
--- a/assets/js/data/checkout/types.ts
+++ b/assets/js/data/checkout/types.ts
@@ -12,6 +12,7 @@ import type { CheckoutState } from './default-state';
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
import * as selectors from './selectors';
import * as actions from './actions';
+import { FieldValidationStatus } from '../types';
export type CheckoutAfterProcessingWithErrorEventData = {
redirectUrl: CheckoutState[ 'redirectUrl' ];
@@ -53,7 +54,9 @@ export type emitValidateEventType = ( {
setValidationErrors,
}: {
observers: EventObserversType;
- setValidationErrors: ( errors: Array< unknown > ) => void;
+ setValidationErrors: (
+ errors: Record< string, FieldValidationStatus >
+ ) => void;
} ) => ( {
dispatch,
registry,
diff --git a/assets/js/data/index.ts b/assets/js/data/index.ts
index 767f979cf01..4b4daa2626c 100644
--- a/assets/js/data/index.ts
+++ b/assets/js/data/index.ts
@@ -10,6 +10,7 @@ export { SCHEMA_STORE_KEY } from './schema';
export { COLLECTIONS_STORE_KEY } from './collections';
export { CART_STORE_KEY } from './cart';
export { CHECKOUT_STORE_KEY } from './checkout';
+export { VALIDATION_STORE_KEY } from './validation';
export { QUERY_STATE_STORE_KEY } from './query-state';
export * from './constants';
export * from './types';
diff --git a/assets/js/data/types.ts b/assets/js/data/types.ts
index 7529679a366..491ff5ed4ed 100644
--- a/assets/js/data/types.ts
+++ b/assets/js/data/types.ts
@@ -42,3 +42,8 @@ export function assertResponseIsValid(
}
throw new Error( 'Response not valid' );
}
+
+export interface FieldValidationStatus {
+ message: string;
+ hidden: boolean;
+}
diff --git a/assets/js/data/validation/action-types.ts b/assets/js/data/validation/action-types.ts
new file mode 100644
index 00000000000..bb5ab4ef57c
--- /dev/null
+++ b/assets/js/data/validation/action-types.ts
@@ -0,0 +1,8 @@
+export const ACTION_TYPES = {
+ SET_VALIDATION_ERRORS: 'SET_VALIDATION_ERRORS',
+ CLEAR_ALL_VALIDATION_ERRORS: 'CLEAR_ALL_VALIDATION_ERRORS',
+ CLEAR_VALIDATION_ERROR: 'CLEAR_VALIDATION_ERROR',
+ HIDE_VALIDATION_ERROR: 'HIDE_VALIDATION_ERROR',
+ SHOW_VALIDATION_ERROR: 'SHOW_VALIDATION_ERROR',
+ SHOW_ALL_VALIDATION_ERRORS: 'SHOW_ALL_VALIDATION_ERRORS',
+} as const;
diff --git a/assets/js/data/validation/actions.ts b/assets/js/data/validation/actions.ts
new file mode 100644
index 00000000000..8dedd7d7b6d
--- /dev/null
+++ b/assets/js/data/validation/actions.ts
@@ -0,0 +1,41 @@
+/**
+ * Internal dependencies
+ */
+import { ACTION_TYPES as types } from './action-types';
+import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
+import { FieldValidationStatus } from '../types';
+
+export const setValidationErrors = (
+ errors: Record< string, FieldValidationStatus >
+) => ( {
+ type: types.SET_VALIDATION_ERRORS,
+ errors,
+} );
+
+export const clearAllValidationErrors = () => ( {
+ type: types.CLEAR_ALL_VALIDATION_ERRORS,
+} );
+
+export const clearValidationError = ( error: string ) => ( {
+ type: types.CLEAR_VALIDATION_ERROR,
+ error,
+} );
+export const hideValidationError = ( error: string ) => ( {
+ type: types.HIDE_VALIDATION_ERROR,
+ error,
+} );
+export const showValidationError = ( error: string ) => ( {
+ type: types.SHOW_VALIDATION_ERROR,
+ error,
+} );
+export const showAllValidationErrors = () => ( {
+ type: types.SHOW_ALL_VALIDATION_ERRORS,
+} );
+export type ValidationAction = ReturnOrGeneratorYieldUnion<
+ | typeof setValidationErrors
+ | typeof clearAllValidationErrors
+ | typeof clearValidationError
+ | typeof hideValidationError
+ | typeof showValidationError
+ | typeof showAllValidationErrors
+>;
diff --git a/assets/js/data/validation/constants.ts b/assets/js/data/validation/constants.ts
new file mode 100644
index 00000000000..a08fec25aac
--- /dev/null
+++ b/assets/js/data/validation/constants.ts
@@ -0,0 +1 @@
+export const STORE_KEY = 'wc/store/validation';
diff --git a/assets/js/data/validation/index.ts b/assets/js/data/validation/index.ts
new file mode 100644
index 00000000000..8241a611614
--- /dev/null
+++ b/assets/js/data/validation/index.ts
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { createReduxStore, register } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import reducer from './reducers';
+import { STORE_KEY } from './constants';
+import * as actions from './actions';
+import * as selectors from './selectors';
+import { DispatchFromMap, SelectFromMap } from '../mapped-types';
+
+export const config = {
+ reducer,
+ selectors,
+ actions,
+ // TODO: Gutenberg with Thunks was released in WP 6.0. Once 6.1 is released, remove the experimental flag here
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore We pass this in case there is an older version of Gutenberg running.
+ __experimentalUseThunks: true,
+};
+
+const store = createReduxStore( STORE_KEY, config );
+register( store );
+
+export const VALIDATION_STORE_KEY = STORE_KEY;
+
+declare module '@wordpress/data' {
+ function dispatch(
+ key: typeof VALIDATION_STORE_KEY
+ ): DispatchFromMap< typeof actions >;
+ function select( key: typeof VALIDATION_STORE_KEY ): SelectFromMap<
+ typeof selectors
+ > & {
+ hasFinishedResolution: ( selector: string ) => boolean;
+ };
+}
diff --git a/assets/js/data/validation/reducers.ts b/assets/js/data/validation/reducers.ts
new file mode 100644
index 00000000000..0b857562e41
--- /dev/null
+++ b/assets/js/data/validation/reducers.ts
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import type { Reducer } from 'redux';
+import { pickBy } from 'lodash';
+import isShallowEqual from '@wordpress/is-shallow-equal';
+import { isString } from '@woocommerce/types';
+
+/**
+ * Internal dependencies
+ */
+import { ValidationAction } from './actions';
+import { ACTION_TYPES as types } from './action-types';
+import { FieldValidationStatus } from '../types';
+
+const reducer: Reducer< Record< string, FieldValidationStatus > > = (
+ state: Record< string, FieldValidationStatus > = {},
+ action: Partial< ValidationAction >
+) => {
+ const newState = { ...state };
+ switch ( action.type ) {
+ case types.SET_VALIDATION_ERRORS:
+ const newErrors = pickBy( action.errors, ( error, property ) => {
+ if ( typeof error.message !== 'string' ) {
+ return false;
+ }
+ if ( state.hasOwnProperty( property ) ) {
+ return ! isShallowEqual( state[ property ], error );
+ }
+ return true;
+ } );
+ if ( Object.values( newErrors ).length === 0 ) {
+ return state;
+ }
+ return { ...state, ...action.errors };
+ case types.CLEAR_ALL_VALIDATION_ERRORS:
+ return {};
+
+ case types.CLEAR_VALIDATION_ERROR:
+ if (
+ ! isString( action.error ) ||
+ ! newState.hasOwnProperty( action.error )
+ ) {
+ return newState;
+ }
+ delete newState[ action.error ];
+ return newState;
+ case types.HIDE_VALIDATION_ERROR:
+ if (
+ ! isString( action.error ) ||
+ ! newState.hasOwnProperty( action.error )
+ ) {
+ return newState;
+ }
+ newState[ action.error ].hidden = true;
+ return newState;
+ case types.SHOW_VALIDATION_ERROR:
+ if (
+ ! isString( action.error ) ||
+ ! newState.hasOwnProperty( action.error )
+ ) {
+ return newState;
+ }
+ newState[ action.error ].hidden = false;
+ return newState;
+ case types.SHOW_ALL_VALIDATION_ERRORS:
+ Object.keys( newState ).forEach( ( property ) => {
+ if ( newState[ property ].hidden ) {
+ newState[ property ].hidden = false;
+ }
+ } );
+ return { ...newState };
+
+ default:
+ return state;
+ }
+};
+
+export type State = ReturnType< typeof reducer >;
+export default reducer;
diff --git a/assets/js/data/validation/selectors.ts b/assets/js/data/validation/selectors.ts
new file mode 100644
index 00000000000..d2697e21670
--- /dev/null
+++ b/assets/js/data/validation/selectors.ts
@@ -0,0 +1,19 @@
+/**
+ * Internal dependencies
+ */
+import type { State } from './reducers';
+
+export const getValidationError = ( state: State ) => {
+ return ( errorId: string ) => state[ errorId ];
+};
+export const getValidationErrorId = ( state: State ) => {
+ return ( errorId: string ) => {
+ if ( ! state.hasOwnProperty( errorId ) || state[ errorId ].hidden ) {
+ return;
+ }
+ return `validate-error-${ errorId }`;
+ };
+};
+export const hasValidationErrors = ( state: State ) => {
+ return Object.keys( state ).length > 0;
+};
diff --git a/assets/js/data/validation/test/reducers.ts b/assets/js/data/validation/test/reducers.ts
new file mode 100644
index 00000000000..6049b4288e0
--- /dev/null
+++ b/assets/js/data/validation/test/reducers.ts
@@ -0,0 +1,248 @@
+/**
+ * Internal dependencies
+ */
+import reducer from '../reducers';
+import { FieldValidationStatus } from '../../types';
+import { ACTION_TYPES as types } from '.././action-types';
+import { ValidationAction } from '../actions';
+
+describe( 'Validation reducer', () => {
+ it( 'Sets a single validation error', () => {
+ const singleValidationAction: ValidationAction = {
+ type: types.SET_VALIDATION_ERRORS,
+ errors: {
+ singleValidationError: {
+ message: 'This is a single validation error message',
+ hidden: false,
+ },
+ },
+ };
+ const nextState = reducer( {}, singleValidationAction );
+ expect( nextState ).toEqual( {
+ singleValidationError: {
+ message: 'This is a single validation error message',
+ hidden: false,
+ },
+ } );
+ } );
+
+ it( 'Does not add new errors if the same error already exists in state', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ };
+ const existingErrorValidation: ValidationAction = {
+ type: types.SET_VALIDATION_ERRORS,
+ errors: {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ },
+ };
+ const nextState = reducer( state, existingErrorValidation );
+ expect( nextState ).toEqual( {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ } );
+ } );
+
+ it( 'Does not add new errors if error message is not string, but keeps existing errors', () => {
+ const integerErrorAction: ValidationAction = {
+ type: types.SET_VALIDATION_ERRORS,
+ errors: {
+ integerError: {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore ignoring because we're testing runtime errors with integers.
+ message: 1234,
+ hidden: false,
+ },
+ },
+ };
+ const nextState = reducer( {}, integerErrorAction );
+ expect( nextState ).not.toHaveProperty( 'integerError' );
+ } );
+
+ it( 'Updates existing error if message or hidden property changes', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingValidationError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ };
+ const updateExistingErrorAction: ValidationAction = {
+ type: types.SET_VALIDATION_ERRORS,
+ errors: {
+ existingValidationError: {
+ message: 'This is an existing error message',
+ hidden: true,
+ },
+ },
+ };
+ const nextState = reducer( state, updateExistingErrorAction );
+ expect( nextState ).toEqual( {
+ existingValidationError: {
+ message: 'This is an existing error message',
+ hidden: true,
+ },
+ } );
+ } );
+
+ it( 'Appends new errors to list of existing errors', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ };
+ const addNewError: ValidationAction = {
+ type: types.SET_VALIDATION_ERRORS,
+ errors: {
+ newError: {
+ message: 'This is a new error',
+ hidden: false,
+ },
+ },
+ };
+ const nextState = reducer( state, addNewError );
+ expect( nextState ).toEqual( {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ newError: {
+ message: 'This is a new error',
+ hidden: false,
+ },
+ } );
+ } );
+
+ it( 'Clears all validation errors', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ };
+ const clearAllErrors: ValidationAction = {
+ type: types.CLEAR_ALL_VALIDATION_ERRORS,
+ };
+ const nextState = reducer( state, clearAllErrors );
+ expect( nextState ).toEqual( {} );
+ } );
+
+ it( 'Clears a single validation error', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ testError: {
+ message: 'This is error should not be removed',
+ hidden: false,
+ },
+ };
+ const clearError: ValidationAction = {
+ type: types.CLEAR_VALIDATION_ERROR,
+ error: 'existingError',
+ };
+ const nextState = reducer( state, clearError );
+ expect( nextState ).not.toHaveProperty( 'existingError' );
+ expect( nextState ).toHaveProperty( 'testError' );
+ } );
+
+ it( 'Hides a single validation error', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ testError: {
+ message: 'This is error should not be removed',
+ hidden: false,
+ },
+ };
+ const testAction: ValidationAction = {
+ type: types.HIDE_VALIDATION_ERROR,
+ error: 'existingError',
+ };
+ const nextState = reducer( state, testAction );
+ expect( nextState ).toEqual( {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: true,
+ },
+ testError: {
+ message: 'This is error should not be removed',
+ hidden: false,
+ },
+ } );
+ } );
+
+ it( 'Shows a single validation error', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: true,
+ },
+ testError: {
+ message: 'This is error should not be removed',
+ hidden: true,
+ },
+ visibleError: {
+ message: 'This is error should remain visible',
+ hidden: false,
+ },
+ };
+ const testAction: ValidationAction = {
+ type: types.SHOW_VALIDATION_ERROR,
+ error: 'existingError',
+ };
+ const nextState = reducer( state, testAction );
+ expect( nextState ).toEqual( {
+ existingError: {
+ message: 'This is an existing error message',
+ hidden: false,
+ },
+ testError: {
+ message: 'This is error should not be removed',
+ hidden: true,
+ },
+ visibleError: {
+ message: 'This is error should remain visible',
+ hidden: false,
+ },
+ } );
+ } );
+
+ it( 'Shows all validation errors', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ firstExistingError: {
+ message: 'This is first existing error message',
+ hidden: true,
+ },
+ secondExistingError: {
+ message: 'This is the second existing error message',
+ hidden: true,
+ },
+ };
+ const showAllErrors: ValidationAction = {
+ type: types.SHOW_ALL_VALIDATION_ERRORS,
+ };
+ const nextState = reducer( state, showAllErrors );
+ expect( nextState ).toEqual( {
+ firstExistingError: {
+ message: 'This is first existing error message',
+ hidden: false,
+ },
+ secondExistingError: {
+ message: 'This is the second existing error message',
+ hidden: false,
+ },
+ } );
+ } );
+} );
diff --git a/assets/js/data/validation/test/selectors.ts b/assets/js/data/validation/test/selectors.ts
new file mode 100644
index 00000000000..a62d5cfc9d9
--- /dev/null
+++ b/assets/js/data/validation/test/selectors.ts
@@ -0,0 +1,56 @@
+/**
+ * Internal dependencies
+ */
+import {
+ getValidationErrorId,
+ getValidationError,
+ hasValidationErrors,
+} from '../selectors';
+import { FieldValidationStatus } from '../../types';
+
+describe( 'Validation selectors', () => {
+ it( 'Gets the validation error', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ validationError: {
+ message: 'This is a test message',
+ hidden: false,
+ },
+ };
+ const validationError = getValidationError( state )(
+ 'validationError'
+ );
+ expect( validationError ).toEqual( {
+ message: 'This is a test message',
+ hidden: false,
+ } );
+ } );
+
+ it( 'Gets the generated validation error ID', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ validationError: {
+ message: 'This is a test message',
+ hidden: false,
+ },
+ };
+ const validationErrorID = getValidationErrorId( state )(
+ 'validationError'
+ );
+ expect( validationErrorID ).toEqual( `validate-error-validationError` );
+ } );
+
+ it( 'Checks if state has any validation errors', () => {
+ const state: Record< string, FieldValidationStatus > = {
+ validationError: {
+ message: 'This is a test message',
+ hidden: false,
+ },
+ };
+ const validationErrors = hasValidationErrors( state );
+ expect( validationErrors ).toEqual( true );
+ const stateWithNoErrors: Record< string, FieldValidationStatus > = {};
+ const stateWithNoErrorsCheckResult = hasValidationErrors(
+ stateWithNoErrors
+ );
+ expect( stateWithNoErrorsCheckResult ).toEqual( false );
+ } );
+} );
diff --git a/assets/js/types/type-defs/payment-method-interface.ts b/assets/js/types/type-defs/payment-method-interface.ts
index f50f680497b..f7880339fb7 100644
--- a/assets/js/types/type-defs/payment-method-interface.ts
+++ b/assets/js/types/type-defs/payment-method-interface.ts
@@ -5,6 +5,7 @@
import type PaymentMethodLabel from '@woocommerce/base-components/cart-checkout/payment-method-label';
import type PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons';
import type LoadingMask from '@woocommerce/base-components/loading-mask';
+import type ValidationInputError from '@woocommerce/base-components/validation-input-error';
/**
* Internal dependencies
@@ -76,7 +77,7 @@ export interface ComponentProps {
// A component used for displaying payment method labels, including an icon.
PaymentMethodLabel: typeof PaymentMethodLabel;
// A container for holding validation errors
- ValidationInputError: () => JSX.Element | null;
+ ValidationInputError: typeof ValidationInputError;
}
export interface EmitResponseProps {