Add stricter observer type checks in payments thunk to improve resilience to bad observer responses (https://github.com/woocommerce/woocommerce-blocks/pull/8319)
* Allow observers to set billingAddress by returning billingData This is required since we didn't correctly deprecate billingData when we changed the name to billingAddress * Add tests for shippingAddress and paymentMethodData * Add mocked __internalSetPaymentMethodData to correct object It was in registry, but should be in dispatch as the action is on the same store as the thunk. Registry is used for actions on other stores. * Re-add FieldValidationStatus type * Add FieldValidationStatus back * Remove empty file * Import FieldValidationStatus from correct place * Remove import of deleted types file * Add isObserverResponse type guard * Use error constant instead of magic string in event emitter * Remove composite project tsconfig * Add ObserverResponse type * Add types to emitEventWithAbort * Check if paymentmethod data is an object before dispatching * Set types on observer responses * Add validationErrors type guards * Add tests for validation typeguards * Add validation errors as option on observer response * Add more granular observer response types * Check observer response has correct types before dispatching actions * Force type on deprecated billingData and shippingData * Remove unnecessary comment
This commit is contained in:
parent
3797418079
commit
f3588635d7
|
@ -22,10 +22,8 @@ import {
|
||||||
ShippingAddress,
|
ShippingAddress,
|
||||||
} from '@woocommerce/settings';
|
} from '@woocommerce/settings';
|
||||||
import { useSelect, useDispatch } from '@wordpress/data';
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
import {
|
import { VALIDATION_STORE_KEY } from '@woocommerce/block-data';
|
||||||
VALIDATION_STORE_KEY,
|
import { FieldValidationStatus } from '@woocommerce/types';
|
||||||
FieldValidationStatus,
|
|
||||||
} from '@woocommerce/block-data';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
|
|
@ -5,8 +5,11 @@ import {
|
||||||
getObserversByPriority,
|
getObserversByPriority,
|
||||||
isErrorResponse,
|
isErrorResponse,
|
||||||
isFailResponse,
|
isFailResponse,
|
||||||
|
ObserverResponse,
|
||||||
|
responseTypes,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import type { EventObserversType } from './types';
|
import type { EventObserversType } from './types';
|
||||||
|
import { isObserverResponse } from '../../../types/type-guards/observers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emits events on registered observers for the provided type and passes along
|
* Emits events on registered observers for the provided type and passes along
|
||||||
|
@ -64,13 +67,13 @@ export const emitEventWithAbort = async (
|
||||||
observers: EventObserversType,
|
observers: EventObserversType,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
data: unknown
|
data: unknown
|
||||||
): Promise< Array< unknown > > => {
|
): Promise< ObserverResponse[] > => {
|
||||||
const observerResponses = [];
|
const observerResponses: ObserverResponse[] = [];
|
||||||
const observersByType = getObserversByPriority( observers, eventType );
|
const observersByType = getObserversByPriority( observers, eventType );
|
||||||
for ( const observer of observersByType ) {
|
for ( const observer of observersByType ) {
|
||||||
try {
|
try {
|
||||||
const response = await Promise.resolve( observer.callback( data ) );
|
const response = await Promise.resolve( observer.callback( data ) );
|
||||||
if ( typeof response !== 'object' || response === null ) {
|
if ( ! isObserverResponse( response ) ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ( ! response.hasOwnProperty( 'type' ) ) {
|
if ( ! response.hasOwnProperty( 'type' ) ) {
|
||||||
|
@ -90,7 +93,7 @@ export const emitEventWithAbort = async (
|
||||||
// We don't handle thrown errors but just console.log for troubleshooting.
|
// We don't handle thrown errors but just console.log for troubleshooting.
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error( e );
|
console.error( e );
|
||||||
observerResponses.push( { type: 'error' } );
|
observerResponses.push( { type: responseTypes.ERROR } );
|
||||||
return observerResponses;
|
return observerResponses;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { isObject } from '@woocommerce/types';
|
import { FieldValidationStatus, isObject } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -42,6 +42,16 @@ export interface ResponseType extends Record< string, unknown > {
|
||||||
retry?: boolean;
|
retry?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observers of checkout/cart events can return a response object to indicate success/error/failure. They may also
|
||||||
|
* optionally pass metadata.
|
||||||
|
*/
|
||||||
|
export interface ObserverResponse {
|
||||||
|
type: responseTypes;
|
||||||
|
meta?: Record< string, unknown > | undefined;
|
||||||
|
validationErrors?: Record< string, FieldValidationStatus > | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const isResponseOf = (
|
const isResponseOf = (
|
||||||
response: unknown,
|
response: unknown,
|
||||||
type: string
|
type: string
|
||||||
|
@ -51,19 +61,27 @@ const isResponseOf = (
|
||||||
|
|
||||||
export const isSuccessResponse = (
|
export const isSuccessResponse = (
|
||||||
response: unknown
|
response: unknown
|
||||||
): response is ResponseType => {
|
): response is ObserverFailResponse => {
|
||||||
return isResponseOf( response, responseTypes.SUCCESS );
|
return isResponseOf( response, responseTypes.SUCCESS );
|
||||||
};
|
};
|
||||||
|
interface ObserverSuccessResponse extends ObserverResponse {
|
||||||
|
type: responseTypes.SUCCESS;
|
||||||
|
}
|
||||||
export const isErrorResponse = (
|
export const isErrorResponse = (
|
||||||
response: unknown
|
response: unknown
|
||||||
): response is ResponseType => {
|
): response is ObserverSuccessResponse => {
|
||||||
return isResponseOf( response, responseTypes.ERROR );
|
return isResponseOf( response, responseTypes.ERROR );
|
||||||
};
|
};
|
||||||
|
interface ObserverErrorResponse extends ObserverResponse {
|
||||||
|
type: responseTypes.ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ObserverFailResponse extends ObserverResponse {
|
||||||
|
type: responseTypes.FAIL;
|
||||||
|
}
|
||||||
export const isFailResponse = (
|
export const isFailResponse = (
|
||||||
response: unknown
|
response: unknown
|
||||||
): response is ResponseType => {
|
): response is ObserverErrorResponse => {
|
||||||
return isResponseOf( response, responseTypes.FAIL );
|
return isResponseOf( response, responseTypes.FAIL );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "../../../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {},
|
|
||||||
"include": [
|
|
||||||
".",
|
|
||||||
"../../blocks-registry/index.js",
|
|
||||||
"../../settings/shared/index.ts",
|
|
||||||
"../../settings/blocks/index.ts",
|
|
||||||
"../../base/hooks/index.js",
|
|
||||||
"../../base/utils/",
|
|
||||||
"../../utils",
|
|
||||||
"../../data/",
|
|
||||||
"../../types/",
|
|
||||||
"../components",
|
|
||||||
"../../blocks/cart-checkout-shared/payment-methods",
|
|
||||||
"../../settings/shared/default-address-fields.ts"
|
|
||||||
],
|
|
||||||
"exclude": [ "**/test/**" ]
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import type { Notice } from '@wordpress/notices/';
|
import type { Notice } from '@wordpress/notices/';
|
||||||
import { DataRegistry } from '@wordpress/data';
|
import { DataRegistry } from '@wordpress/data';
|
||||||
|
import { FieldValidationStatus } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -13,7 +14,6 @@ import type { PaymentState } from '../payment/default-state';
|
||||||
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
import type { DispatchFromMap, SelectFromMap } from '../mapped-types';
|
||||||
import * as selectors from './selectors';
|
import * as selectors from './selectors';
|
||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
import { FieldValidationStatus } from '../types';
|
|
||||||
|
|
||||||
export type CheckoutAfterProcessingWithErrorEventData = {
|
export type CheckoutAfterProcessingWithErrorEventData = {
|
||||||
redirectUrl: CheckoutState[ 'redirectUrl' ];
|
redirectUrl: CheckoutState[ 'redirectUrl' ];
|
||||||
|
|
|
@ -15,5 +15,4 @@ export { VALIDATION_STORE_KEY } from './validation';
|
||||||
export { QUERY_STATE_STORE_KEY } from './query-state';
|
export { QUERY_STATE_STORE_KEY } from './query-state';
|
||||||
export { STORE_NOTICES_STORE_KEY } from './store-notices';
|
export { STORE_NOTICES_STORE_KEY } from './store-notices';
|
||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './types';
|
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { store as noticesStore } from '@wordpress/notices';
|
import { store as noticesStore } from '@wordpress/notices';
|
||||||
import deprecated from '@wordpress/deprecated';
|
import deprecated from '@wordpress/deprecated';
|
||||||
import type { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
import type { BillingAddress, ShippingAddress } from '@woocommerce/settings';
|
||||||
|
import { isObject, isString, objectHasProp } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -14,6 +15,7 @@ import {
|
||||||
isFailResponse,
|
isFailResponse,
|
||||||
isSuccessResponse,
|
isSuccessResponse,
|
||||||
noticeContexts,
|
noticeContexts,
|
||||||
|
ObserverResponse,
|
||||||
} from '../../base/context/event-emit';
|
} from '../../base/context/event-emit';
|
||||||
import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit';
|
import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit';
|
||||||
import type { emitProcessingEventType } from './types';
|
import type { emitProcessingEventType } from './types';
|
||||||
|
@ -22,6 +24,8 @@ import {
|
||||||
isBillingAddress,
|
isBillingAddress,
|
||||||
isShippingAddress,
|
isShippingAddress,
|
||||||
} from '../../types/type-guards/address';
|
} from '../../types/type-guards/address';
|
||||||
|
import { isObserverResponse } from '../../types/type-guards/observers';
|
||||||
|
import { isValidValidationErrorsObject } from '../../types/type-guards/validation';
|
||||||
|
|
||||||
export const __internalSetExpressPaymentError = ( message?: string ) => {
|
export const __internalSetExpressPaymentError = ( message?: string ) => {
|
||||||
return ( { registry } ) => {
|
return ( { registry } ) => {
|
||||||
|
@ -57,8 +61,8 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
|
||||||
EMIT_TYPES.PAYMENT_PROCESSING,
|
EMIT_TYPES.PAYMENT_PROCESSING,
|
||||||
{}
|
{}
|
||||||
).then( ( observerResponses ) => {
|
).then( ( observerResponses ) => {
|
||||||
let successResponse,
|
let successResponse: ObserverResponse | undefined,
|
||||||
errorResponse,
|
errorResponse: ObserverResponse | undefined,
|
||||||
billingAddress: BillingAddress | undefined,
|
billingAddress: BillingAddress | undefined,
|
||||||
shippingAddress: ShippingAddress | undefined;
|
shippingAddress: ShippingAddress | undefined;
|
||||||
observerResponses.forEach( ( response ) => {
|
observerResponses.forEach( ( response ) => {
|
||||||
|
@ -86,12 +90,13 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
|
||||||
shippingData: shippingDataFromResponse,
|
shippingData: shippingDataFromResponse,
|
||||||
} = response?.meta || {};
|
} = response?.meta || {};
|
||||||
|
|
||||||
billingAddress = billingAddressFromResponse;
|
billingAddress = billingAddressFromResponse as BillingAddress;
|
||||||
shippingAddress = shippingAddressFromResponse;
|
shippingAddress =
|
||||||
|
shippingAddressFromResponse as ShippingAddress;
|
||||||
|
|
||||||
if ( billingDataFromResponse ) {
|
if ( billingDataFromResponse ) {
|
||||||
// Set this here so that old extensions still using billingData can set the billingAddress.
|
// Set this here so that old extensions still using billingData can set the billingAddress.
|
||||||
billingAddress = billingDataFromResponse;
|
billingAddress = billingDataFromResponse as BillingAddress;
|
||||||
deprecated(
|
deprecated(
|
||||||
'returning billingData from an onPaymentProcessing observer in WooCommerce Blocks',
|
'returning billingData from an onPaymentProcessing observer in WooCommerce Blocks',
|
||||||
{
|
{
|
||||||
|
@ -104,7 +109,8 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
|
||||||
|
|
||||||
if ( shippingDataFromResponse ) {
|
if ( shippingDataFromResponse ) {
|
||||||
// Set this here so that old extensions still using shippingData can set the shippingAddress.
|
// Set this here so that old extensions still using shippingData can set the shippingAddress.
|
||||||
shippingAddress = shippingDataFromResponse;
|
shippingAddress =
|
||||||
|
shippingDataFromResponse as ShippingAddress;
|
||||||
deprecated(
|
deprecated(
|
||||||
'returning shippingData from an onPaymentProcessing observer in WooCommerce Blocks',
|
'returning shippingData from an onPaymentProcessing observer in WooCommerce Blocks',
|
||||||
{
|
{
|
||||||
|
@ -119,9 +125,12 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
|
||||||
const { setBillingAddress, setShippingAddress } =
|
const { setBillingAddress, setShippingAddress } =
|
||||||
registry.dispatch( CART_STORE_KEY );
|
registry.dispatch( CART_STORE_KEY );
|
||||||
|
|
||||||
if ( successResponse && ! errorResponse ) {
|
if (
|
||||||
|
isObserverResponse( successResponse ) &&
|
||||||
|
successResponse &&
|
||||||
|
! errorResponse
|
||||||
|
) {
|
||||||
const { paymentMethodData } = successResponse?.meta || {};
|
const { paymentMethodData } = successResponse?.meta || {};
|
||||||
|
|
||||||
if ( billingAddress && isBillingAddress( billingAddress ) ) {
|
if ( billingAddress && isBillingAddress( billingAddress ) ) {
|
||||||
setBillingAddress( billingAddress );
|
setBillingAddress( billingAddress );
|
||||||
}
|
}
|
||||||
|
@ -131,16 +140,29 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
|
||||||
) {
|
) {
|
||||||
setShippingAddress( shippingAddress );
|
setShippingAddress( shippingAddress );
|
||||||
}
|
}
|
||||||
dispatch.__internalSetPaymentMethodData( paymentMethodData );
|
const paymentDataToSet = isObject( paymentMethodData )
|
||||||
|
? paymentMethodData
|
||||||
|
: {};
|
||||||
|
dispatch.__internalSetPaymentMethodData( paymentDataToSet );
|
||||||
dispatch.__internalSetPaymentSuccess();
|
dispatch.__internalSetPaymentSuccess();
|
||||||
} else if ( errorResponse && isFailResponse( errorResponse ) ) {
|
} else if ( isFailResponse( errorResponse ) ) {
|
||||||
if ( errorResponse.message && errorResponse.message.length ) {
|
if (
|
||||||
|
objectHasProp( errorResponse, 'message' ) &&
|
||||||
|
isString( errorResponse.message ) &&
|
||||||
|
errorResponse.message.length
|
||||||
|
) {
|
||||||
|
let context: string = noticeContexts.PAYMENTS;
|
||||||
|
if (
|
||||||
|
objectHasProp( errorResponse, 'messageContext' ) &&
|
||||||
|
isString( errorResponse.messageContext ) &&
|
||||||
|
errorResponse.messageContext.length
|
||||||
|
) {
|
||||||
|
context = errorResponse.messageContext;
|
||||||
|
}
|
||||||
createErrorNotice( errorResponse.message, {
|
createErrorNotice( errorResponse.message, {
|
||||||
id: 'wc-payment-error',
|
id: 'wc-payment-error',
|
||||||
isDismissible: false,
|
isDismissible: false,
|
||||||
context:
|
context,
|
||||||
errorResponse?.messageContext ||
|
|
||||||
noticeContexts.PAYMENTS,
|
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,20 +171,41 @@ export const __internalEmitPaymentProcessingEvent: emitProcessingEventType = (
|
||||||
setBillingAddress( billingAddress );
|
setBillingAddress( billingAddress );
|
||||||
}
|
}
|
||||||
dispatch.__internalSetPaymentFailed();
|
dispatch.__internalSetPaymentFailed();
|
||||||
dispatch.__internalSetPaymentMethodData( paymentMethodData );
|
|
||||||
} else if ( errorResponse ) {
|
const paymentDataToSet = isObject( paymentMethodData )
|
||||||
if ( errorResponse.message && errorResponse.message.length ) {
|
? paymentMethodData
|
||||||
|
: {};
|
||||||
|
dispatch.__internalSetPaymentMethodData( paymentDataToSet );
|
||||||
|
} else if ( isErrorResponse( errorResponse ) ) {
|
||||||
|
if (
|
||||||
|
objectHasProp( errorResponse, 'message' ) &&
|
||||||
|
isString( errorResponse.message ) &&
|
||||||
|
errorResponse.message.length
|
||||||
|
) {
|
||||||
|
let context: string = noticeContexts.PAYMENTS;
|
||||||
|
if (
|
||||||
|
objectHasProp( errorResponse, 'messageContext' ) &&
|
||||||
|
isString( errorResponse.messageContext ) &&
|
||||||
|
errorResponse.messageContext.length
|
||||||
|
) {
|
||||||
|
context = errorResponse.messageContext;
|
||||||
|
}
|
||||||
createErrorNotice( errorResponse.message, {
|
createErrorNotice( errorResponse.message, {
|
||||||
id: 'wc-payment-error',
|
id: 'wc-payment-error',
|
||||||
isDismissible: false,
|
isDismissible: false,
|
||||||
context:
|
context,
|
||||||
errorResponse?.messageContext ||
|
|
||||||
noticeContexts.PAYMENTS,
|
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch.__internalSetPaymentError();
|
dispatch.__internalSetPaymentError();
|
||||||
setValidationErrors( errorResponse?.validationErrors );
|
|
||||||
|
if (
|
||||||
|
isValidValidationErrorsObject(
|
||||||
|
errorResponse.validationErrors
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setValidationErrors( errorResponse.validationErrors );
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise there are no payment methods doing anything so
|
// otherwise there are no payment methods doing anything so
|
||||||
// just consider success
|
// just consider success
|
||||||
|
|
|
@ -5,7 +5,11 @@ import {
|
||||||
PlainPaymentMethods,
|
PlainPaymentMethods,
|
||||||
PlainExpressPaymentMethods,
|
PlainExpressPaymentMethods,
|
||||||
} from '@woocommerce/types';
|
} from '@woocommerce/types';
|
||||||
import type { EmptyObjectType, ObjectType } from '@woocommerce/types';
|
import type {
|
||||||
|
EmptyObjectType,
|
||||||
|
ObjectType,
|
||||||
|
FieldValidationStatus,
|
||||||
|
} from '@woocommerce/types';
|
||||||
import { DataRegistry } from '@wordpress/data';
|
import { DataRegistry } from '@wordpress/data';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,7 +18,6 @@ import { DataRegistry } from '@wordpress/data';
|
||||||
import type { EventObserversType } from '../../base/context/event-emit';
|
import type { EventObserversType } from '../../base/context/event-emit';
|
||||||
import type { DispatchFromMap } from '../mapped-types';
|
import type { DispatchFromMap } from '../mapped-types';
|
||||||
import * as actions from './actions';
|
import * as actions from './actions';
|
||||||
import { FieldValidationStatus } from '../types';
|
|
||||||
|
|
||||||
export interface CustomerPaymentMethodConfiguration {
|
export interface CustomerPaymentMethodConfiguration {
|
||||||
gateway: string;
|
gateway: string;
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import deprecated from '@wordpress/deprecated';
|
import deprecated from '@wordpress/deprecated';
|
||||||
|
import { FieldValidationStatus } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ACTION_TYPES as types } from './action-types';
|
import { ACTION_TYPES as types } from './action-types';
|
||||||
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
|
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
|
||||||
import { FieldValidationStatus } from '../types';
|
|
||||||
|
|
||||||
export const setValidationErrors = (
|
export const setValidationErrors = (
|
||||||
errors: Record< string, FieldValidationStatus >
|
errors: Record< string, FieldValidationStatus >
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
import type { Reducer } from 'redux';
|
import type { Reducer } from 'redux';
|
||||||
import { pickBy } from 'lodash';
|
import { pickBy } from 'lodash';
|
||||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||||
import { isString } from '@woocommerce/types';
|
import { isString, FieldValidationStatus } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { ValidationAction } from './actions';
|
import { ValidationAction } from './actions';
|
||||||
import { ACTION_TYPES as types } from './action-types';
|
import { ACTION_TYPES as types } from './action-types';
|
||||||
import { FieldValidationStatus } from '../types';
|
|
||||||
|
|
||||||
const reducer: Reducer< Record< string, FieldValidationStatus > > = (
|
const reducer: Reducer< Record< string, FieldValidationStatus > > = (
|
||||||
state: Record< string, FieldValidationStatus > = {},
|
state: Record< string, FieldValidationStatus > = {},
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { FieldValidationStatus } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import reducer from '../reducers';
|
import reducer from '../reducers';
|
||||||
import { FieldValidationStatus } from '../../types';
|
|
||||||
import { ACTION_TYPES as types } from '.././action-types';
|
import { ACTION_TYPES as types } from '.././action-types';
|
||||||
import { ValidationAction } from '../actions';
|
import { ValidationAction } from '../actions';
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { FieldValidationStatus } from '@woocommerce/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +11,6 @@ import {
|
||||||
getValidationError,
|
getValidationError,
|
||||||
hasValidationErrors,
|
hasValidationErrors,
|
||||||
} from '../selectors';
|
} from '../selectors';
|
||||||
import { FieldValidationStatus } from '../../types';
|
|
||||||
|
|
||||||
describe( 'Validation selectors', () => {
|
describe( 'Validation selectors', () => {
|
||||||
it( 'Gets the validation error', () => {
|
it( 'Gets the validation error', () => {
|
||||||
|
|
|
@ -17,3 +17,4 @@ export * from './utils';
|
||||||
export * from './taxes';
|
export * from './taxes';
|
||||||
export * from './attributes';
|
export * from './attributes';
|
||||||
export * from './stock-status';
|
export * from './stock-status';
|
||||||
|
export * from './validation';
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* An interface to describe the validity of a Checkout field. This is what will be stored in the wc/store/validation
|
||||||
|
* data store.
|
||||||
|
*/
|
||||||
|
export interface FieldValidationStatus {
|
||||||
|
/**
|
||||||
|
* The message to display to the user.
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
/**
|
||||||
|
* Whether this validation error should be hidden. Note, hidden errors still prevent checkout. Adding a hidden error
|
||||||
|
* allows required fields to be validated, but not show the error to the user until they interact with the input
|
||||||
|
* element, or try to submit the form.
|
||||||
|
*/
|
||||||
|
hidden: boolean;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { ObserverResponse } from '@woocommerce/base-context';
|
||||||
|
import { isObject, objectHasProp } from '@woocommerce/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the passed object is an ObserverResponse.
|
||||||
|
*/
|
||||||
|
export const isObserverResponse = (
|
||||||
|
response: unknown
|
||||||
|
): response is ObserverResponse => {
|
||||||
|
return isObject( response ) && objectHasProp( response, 'type' );
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
isValidFieldValidationStatus,
|
||||||
|
isValidValidationErrorsObject,
|
||||||
|
} from '../validation';
|
||||||
|
|
||||||
|
describe( 'validation type guards', () => {
|
||||||
|
describe( 'isValidFieldValidationStatus', () => {
|
||||||
|
it( 'identifies valid objects', () => {
|
||||||
|
const valid = {
|
||||||
|
message: 'message',
|
||||||
|
hidden: false,
|
||||||
|
};
|
||||||
|
expect( isValidFieldValidationStatus( valid ) ).toBe( true );
|
||||||
|
} );
|
||||||
|
it( 'identifies invalid objects', () => {
|
||||||
|
const invalid = {
|
||||||
|
message: 'message',
|
||||||
|
hidden: 'string',
|
||||||
|
};
|
||||||
|
expect( isValidFieldValidationStatus( invalid ) ).toBe( false );
|
||||||
|
const noMessage = {
|
||||||
|
hidden: false,
|
||||||
|
};
|
||||||
|
expect( isValidFieldValidationStatus( noMessage ) ).toBe( false );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
describe( 'isValidValidationErrorsObject', () => {
|
||||||
|
it( 'identifies valid objects', () => {
|
||||||
|
const valid = {
|
||||||
|
'billing.first-name': {
|
||||||
|
message: 'message',
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect( isValidValidationErrorsObject( valid ) ).toBe( true );
|
||||||
|
} );
|
||||||
|
it( 'identifies invalid objects', () => {
|
||||||
|
const invalid = {
|
||||||
|
'billing.first-name': {
|
||||||
|
message: 'message',
|
||||||
|
hidden: 'string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect( isValidValidationErrorsObject( invalid ) ).toBe( false );
|
||||||
|
const noMessage = {
|
||||||
|
'billing.first-name': {
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect( isValidValidationErrorsObject( noMessage ) ).toBe( false );
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
FieldValidationStatus,
|
||||||
|
isBoolean,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
objectHasProp,
|
||||||
|
} from '@woocommerce/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given status is a valid FieldValidationStatus.
|
||||||
|
*/
|
||||||
|
export const isValidFieldValidationStatus = (
|
||||||
|
status: unknown
|
||||||
|
): status is FieldValidationStatus => {
|
||||||
|
return (
|
||||||
|
isObject( status ) &&
|
||||||
|
objectHasProp( status, 'message' ) &&
|
||||||
|
objectHasProp( status, 'hidden' ) &&
|
||||||
|
isString( status.message ) &&
|
||||||
|
isBoolean( status.hidden )
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the passed object is a valid validation errors object. If this is true, it can be set on the
|
||||||
|
* wc/store/validation store without any issue.
|
||||||
|
*/
|
||||||
|
export const isValidValidationErrorsObject = (
|
||||||
|
errors: unknown
|
||||||
|
): errors is Record< string, FieldValidationStatus > => {
|
||||||
|
return (
|
||||||
|
isObject( errors ) &&
|
||||||
|
Object.entries( errors ).every(
|
||||||
|
( [ key, value ] ) =>
|
||||||
|
isString( key ) && isValidFieldValidationStatus( value )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue