Cart Action Promises with success/reject handling (https://github.com/woocommerce/woocommerce-blocks/pull/8272)

* move thinks and create consistent promise rejection

* Remove notifyErrors

* Combine error handling

* Ensure thunk usage handles errors

* Use promise in coupon form

* onsubmit type

* receiveCartContents during checkout

* Update mocks

* receiveCartContents mocks/types

* Fix receiveCartContents tests

* Move thunks back to actions, add todo for follow up

* Sort actions alphabetically

* Null check is unnecessary
This commit is contained in:
Mike Jolley 2023-01-30 15:12:17 +00:00 committed by GitHub
parent dc283a0e0a
commit 1d45dacc3e
14 changed files with 201 additions and 212 deletions

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useState, useEffect, useRef } from '@wordpress/element';
import { useState } from '@wordpress/element';
import Button from '@woocommerce/base-components/button';
import LoadingMask from '@woocommerce/base-components/loading-mask';
import { withInstanceId } from '@wordpress/compose';
@ -35,35 +35,29 @@ export interface TotalsCouponProps {
/**
* Submit handler
*/
onSubmit?: ( couponValue: string ) => void;
onSubmit?: ( couponValue: string ) => Promise< boolean > | undefined;
}
export const TotalsCoupon = ( {
instanceId,
isLoading = false,
onSubmit,
displayCouponForm = false,
onSubmit = () => void 0,
}: TotalsCouponProps ): JSX.Element => {
const [ couponValue, setCouponValue ] = useState( '' );
const [ isCouponFormHidden, setIsCouponFormHidden ] = useState(
! displayCouponForm
);
const currentIsLoading = useRef( false );
const validationErrorKey = 'coupon';
const textInputId = `wc-block-components-totals-coupon__input-${ instanceId }`;
const formWrapperClass = classnames(
'wc-block-components-totals-coupon__content',
{
'screen-reader-text': isCouponFormHidden,
}
);
const { validationError, validationErrorId } = useSelect( ( select ) => {
const { validationErrorId } = useSelect( ( select ) => {
const store = select( VALIDATION_STORE_KEY );
return {
validationError: store.getValidationError( validationErrorKey ),
validationErrorId: store.getValidationErrorId( textInputId ),
};
} );
@ -77,18 +71,18 @@ export const TotalsCoupon = ( {
e: React.MouseEvent< HTMLButtonElement, MouseEvent >
) => {
e.preventDefault();
onSubmit( couponValue );
};
useEffect( () => {
if ( currentIsLoading.current !== isLoading ) {
if ( ! isLoading && couponValue && ! validationError ) {
setCouponValue( '' );
setIsCouponFormHidden( true );
}
currentIsLoading.current = isLoading;
if ( onSubmit !== undefined ) {
onSubmit( couponValue ).then( ( result ) => {
if ( result ) {
setCouponValue( '' );
setIsCouponFormHidden( true );
}
} );
} else {
setCouponValue( '' );
setIsCouponFormHidden( true );
}
}, [ isLoading, couponValue, validationError ] );
};
return (
<div className="wc-block-components-totals-coupon">

View File

@ -31,11 +31,12 @@ const Template: Story< TotalsCouponProps > = ( args ) => {
const onSubmit = ( code: string ) => {
args.onSubmit?.( code );
setArgs( { isLoading: true } );
setTimeout(
() => setArgs( { isLoading: false } ),
INTERACTION_TIMEOUT
);
return new Promise( ( resolve ) => {
setTimeout( () => {
setArgs( { isLoading: false } );
resolve( true );
}, INTERACTION_TIMEOUT );
} );
};
return <TotalsCoupon { ...args } onSubmit={ onSubmit } />;

View File

@ -26,6 +26,7 @@ describe( 'useStoreCart', () => {
let registry, renderer;
const receiveCartMock = () => {};
const receiveCartContentsMock = () => {};
const previewCartData = {
cartCoupons: previewCart.coupons,
@ -102,8 +103,9 @@ describe( 'useStoreCart', () => {
hasCalculatedShipping: true,
extensions: {},
errors: [],
receiveCart: undefined,
paymentRequirements: [],
receiveCart: undefined,
receiveCartContents: undefined,
};
const mockCartTotals = {
currency_code: 'USD',
@ -129,8 +131,9 @@ describe( 'useStoreCart', () => {
extensions: {},
isLoadingRates: false,
cartHasCalculatedShipping: true,
receiveCart: undefined,
paymentRequirements: [],
receiveCart: undefined,
receiveCartContents: undefined,
};
const getWrappedComponents = ( Component ) => (
@ -140,8 +143,15 @@ describe( 'useStoreCart', () => {
);
const getTestComponent = ( options ) => () => {
const { receiveCart, ...results } = useStoreCart( options );
return <div results={ results } receiveCart={ receiveCart } />;
const { receiveCart, receiveCartContents, ...results } =
useStoreCart( options );
return (
<div
results={ results }
receiveCart={ receiveCart }
receiveCartContents={ receiveCartContents }
/>
);
};
const setUpMocks = () => {
@ -190,12 +200,16 @@ describe( 'useStoreCart', () => {
);
} );
const { results, receiveCart } =
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
const { receiveCart: defaultReceiveCart, ...remaining } =
defaultCartData;
const {
receiveCart: defaultReceiveCart,
receiveCartContents: defaultReceiveCartContents,
...remaining
} = defaultCartData;
expect( results ).toEqual( remaining );
expect( receiveCart ).toEqual( defaultReceiveCart );
expect( receiveCartContents ).toEqual( defaultReceiveCartContents );
} );
it( 'return store data when shouldSelect is true', () => {
@ -209,11 +223,12 @@ describe( 'useStoreCart', () => {
);
} );
const { results, receiveCart } =
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
expect( results ).toEqual( mockStoreCartData );
expect( receiveCart ).toBeUndefined();
expect( receiveCartContents ).toBeUndefined();
} );
} );
@ -225,6 +240,7 @@ describe( 'useStoreCart', () => {
previewCart: {
...previewCart,
receiveCart: receiveCartMock,
receiveCartContents: receiveCartContentsMock,
},
},
} );
@ -239,11 +255,12 @@ describe( 'useStoreCart', () => {
);
} );
const { results, receiveCart } =
const { results, receiveCart, receiveCartContents } =
renderer.root.findByType( 'div' ).props; //eslint-disable-line testing-library/await-async-query
expect( results ).toEqual( previewCartData );
expect( receiveCart ).toEqual( receiveCartMock );
expect( receiveCartContents ).toEqual( receiveCartContentsMock );
} );
} );
} );

View File

@ -40,14 +40,12 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
[ createErrorNotice, createNotice ]
);
const { applyCoupon, removeCoupon, receiveApplyingCoupon } =
useDispatch( CART_STORE_KEY );
const { applyCoupon, removeCoupon } = useDispatch( CART_STORE_KEY );
const applyCouponWithNotices = ( couponCode: string ) => {
applyCoupon( couponCode )
.then( ( result ) => {
return applyCoupon( couponCode )
.then( () => {
if (
result === true &&
__experimentalApplyCheckoutFilter( {
filterName: 'showApplyCouponNotice',
defaultValue: true,
@ -71,6 +69,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
}
);
}
return Promise.resolve( true );
} )
.catch( ( error ) => {
setValidationErrors( {
@ -79,16 +78,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
hidden: false,
},
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
return Promise.resolve( false );
} );
};
const removeCouponWithNotices = ( couponCode: string ) => {
removeCoupon( couponCode )
.then( ( result ) => {
return removeCoupon( couponCode )
.then( () => {
if (
result === true &&
__experimentalApplyCheckoutFilter( {
filterName: 'showRemoveCouponNotice',
defaultValue: true,
@ -112,14 +109,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
}
);
}
return Promise.resolve( true );
} )
.catch( ( error ) => {
createErrorNotice( error.message, {
id: 'coupon-form',
context,
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
return Promise.resolve( false );
} );
};

View File

@ -3,7 +3,11 @@
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback, useState, useEffect } from '@wordpress/element';
import { CART_STORE_KEY, CHECKOUT_STORE_KEY } from '@woocommerce/block-data';
import {
CART_STORE_KEY,
CHECKOUT_STORE_KEY,
processErrorResponse,
} from '@woocommerce/block-data';
import { useDebounce } from 'use-debounce';
import { usePrevious } from '@woocommerce/base-hooks';
import {
@ -84,9 +88,12 @@ export const useStoreCartItemQuantity = (
);
const removeItem = useCallback( () => {
return cartItemKey
? removeItemFromCart( cartItemKey )
: Promise.resolve( false );
if ( cartItemKey ) {
return removeItemFromCart( cartItemKey ).catch( ( error ) => {
processErrorResponse( error );
} );
}
return Promise.resolve( false );
}, [ cartItemKey, removeItemFromCart ] );
// Observe debounced quantity value, fire action to update server on change.
@ -97,7 +104,11 @@ export const useStoreCartItemQuantity = (
Number.isFinite( previousDebouncedQuantity ) &&
previousDebouncedQuantity !== debouncedQuantity
) {
changeCartItemQuantity( cartItemKey, debouncedQuantity );
changeCartItemQuantity( cartItemKey, debouncedQuantity ).catch(
( error ) => {
processErrorResponse( error );
}
);
}
}, [
cartItemKey,

View File

@ -114,6 +114,7 @@ export const defaultCartData: StoreCart = {
cartHasCalculatedShipping: false,
paymentRequirements: EMPTY_PAYMENT_REQUIREMENTS,
receiveCart: () => undefined,
receiveCartContents: () => undefined,
extensions: EMPTY_EXTENSIONS,
};
@ -174,6 +175,10 @@ export const useStoreCart = (
typeof previewCart?.receiveCart === 'function'
? previewCart.receiveCart
: () => undefined,
receiveCartContents:
typeof previewCart?.receiveCartContents === 'function'
? previewCart.receiveCartContents
: () => undefined,
};
}
@ -185,7 +190,7 @@ export const useStoreCart = (
! store.hasFinishedResolution( 'getCartData' );
const isLoadingRates = store.isCustomerDataUpdating();
const { receiveCart } = dispatch( storeKey );
const { receiveCart, receiveCartContents } = dispatch( storeKey );
const billingAddress = decodeValues( cartData.billingAddress );
const shippingAddress = cartData.needsShipping
? decodeValues( cartData.shippingAddress )
@ -232,6 +237,7 @@ export const useStoreCart = (
cartHasCalculatedShipping: cartData.hasCalculatedShipping,
paymentRequirements: cartData.paymentRequirements,
receiveCart,
receiveCartContents,
};
},
[ shouldSelect ]

View File

@ -1,14 +1,16 @@
/**
* External dependencies
*/
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import {
CART_STORE_KEY as storeKey,
processErrorResponse,
} from '@woocommerce/block-data';
import { useSelect, useDispatch } from '@wordpress/data';
import { isObject } from '@woocommerce/types';
import { useEffect, useRef, useCallback } from '@wordpress/element';
import { deriveSelectedShippingRates } from '@woocommerce/base-utils';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { previewCart } from '@woocommerce/resource-previews';
import { useThrowError } from '@woocommerce/base-hooks';
/**
* Internal dependencies
@ -72,12 +74,11 @@ export const useShippingData = (): ShippingData => {
} as {
selectShippingRate: (
newShippingRateId: string,
packageId?: string | number
packageId?: string | number | undefined
) => Promise< unknown >;
};
// Selects a shipping rate, fires an event, and catch any errors.
const throwError = useThrowError();
const { dispatchCheckoutEvent } = useStoreEvents();
const selectShippingRate = useCallback(
(
@ -113,16 +114,10 @@ export const useShippingData = (): ShippingData => {
} );
} )
.catch( ( error ) => {
// Throw an error because an error when selecting a rate is problematic.
throwError( error );
processErrorResponse( error );
} );
},
[
dispatchSelectShippingRate,
dispatchCheckoutEvent,
throwError,
selectedRates,
]
[ dispatchSelectShippingRate, dispatchCheckoutEvent, selectedRates ]
);
return {

View File

@ -84,7 +84,8 @@ const CheckoutProcessor = () => {
select( CART_STORE_KEY ).getCustomerData()
);
const { cartNeedsPayment, cartNeedsShipping, receiveCart } = useStoreCart();
const { cartNeedsPayment, cartNeedsShipping, receiveCartContents } =
useStoreCart();
const {
activePaymentMethod,
@ -275,7 +276,8 @@ const CheckoutProcessor = () => {
)
.then( ( response: CheckoutResponseError ) => {
if ( response.data?.cart ) {
receiveCart( response.data.cart );
// We don't want to receive the address here because it will overwrite fields.
receiveCartContents( response.data.cart );
}
processErrorResponse( response );
__internalProcessCheckoutResponse( response );
@ -304,7 +306,7 @@ const CheckoutProcessor = () => {
shouldCreateAccount,
extensionData,
cartNeedsShipping,
receiveCart,
receiveCartContents,
__internalSetHasError,
__internalProcessCheckoutResponse,
] );

View File

@ -24,13 +24,6 @@ export const getNoticeContexts = () => {
return Object.values( noticeContexts );
};
const hasStoreNoticesContainer = ( container: string ): boolean => {
const containers = select(
'wc/store/store-notices'
).getRegisteredContainers();
return containers.includes( container );
};
/**
* Wrapper for @wordpress/notices createNotice.
*/
@ -54,19 +47,6 @@ export const createNotice = (
} );
};
/**
* Creates a notice only if the Store Notice Container is visible.
*/
export const createNoticeIfVisible = (
status: 'error' | 'warning' | 'info' | 'success',
message: string,
options: Partial< NoticeOptions >
) => {
if ( options?.context && hasStoreNoticesContainer( options.context ) ) {
createNotice( status, message, options );
}
};
/**
* Remove notices from all contexts.
*

View File

@ -23,8 +23,10 @@ import { ACTION_TYPES as types } from './action-types';
import { apiFetchWithHeaders } from '../shared-controls';
import { ReturnOrGeneratorYieldUnion } from '../mapped-types';
import { CartDispatchFromMap, CartResolveSelectFromMap } from './index';
import type { Thunks } from './thunks';
// Thunks are functions that can be dispatched, similar to actions creators
// @todo Many of the functions that return promises in this file need to be moved to thunks.ts.
export * from './thunks';
/**
@ -143,6 +145,7 @@ export const itemIsPendingDelete = (
cartItemKey,
isPendingDelete,
} as const );
/**
* Returns an action object to mark the cart data in the store as stale.
*
@ -197,6 +200,7 @@ export const applyExtensionCartUpdate =
return response;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
}
};
@ -210,8 +214,8 @@ export const applyExtensionCartUpdate =
export const applyCoupon =
( couponCode: string ) =>
async ( { dispatch } ) => {
dispatch.receiveApplyingCoupon( couponCode );
try {
dispatch.receiveApplyingCoupon( couponCode );
const { response } = await apiFetchWithHeaders( {
path: '/wc/store/v1/cart/apply-coupon',
method: 'POST',
@ -220,14 +224,14 @@ export const applyCoupon =
},
cache: 'no-store',
} );
dispatch.receiveApplyingCoupon( '' );
dispatch.receiveCart( response );
return response;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
} finally {
dispatch.receiveApplyingCoupon( '' );
}
return true;
};
/**
@ -240,9 +244,8 @@ export const applyCoupon =
export const removeCoupon =
( couponCode: string ) =>
async ( { dispatch } ) => {
dispatch.receiveRemovingCoupon( couponCode );
try {
dispatch.receiveRemovingCoupon( couponCode );
const { response } = await apiFetchWithHeaders( {
path: '/wc/store/v1/cart/remove-coupon',
method: 'POST',
@ -251,15 +254,14 @@ export const removeCoupon =
},
cache: 'no-store',
} );
dispatch.receiveCart( response );
return response;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
} finally {
dispatch.receiveRemovingCoupon( '' );
}
return true;
};
/**
@ -286,11 +288,12 @@ export const addItemToCart =
},
cache: 'no-store',
} );
dispatch.receiveCart( response );
triggerAddedToCartEvent( { preserveCartData: true } );
return response;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
}
};
@ -306,9 +309,8 @@ export const addItemToCart =
export const removeItemFromCart =
( cartItemKey: string ) =>
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
dispatch.itemIsPendingDelete( cartItemKey );
try {
dispatch.itemIsPendingDelete( cartItemKey );
const { response } = await apiFetchWithHeaders( {
path: `/wc/store/v1/cart/remove-item`,
data: {
@ -317,10 +319,11 @@ export const removeItemFromCart =
method: 'POST',
cache: 'no-store',
} );
dispatch.receiveCart( response );
return response;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
} finally {
dispatch.itemIsPendingDelete( cartItemKey, false );
}
@ -352,8 +355,8 @@ export const changeCartItemQuantity =
if ( cartItem?.quantity === quantity ) {
return;
}
dispatch.itemIsPendingQuantity( cartItemKey );
try {
dispatch.itemIsPendingQuantity( cartItemKey );
const { response } = await apiFetchWithHeaders( {
path: '/wc/store/v1/cart/update-item',
method: 'POST',
@ -363,10 +366,11 @@ export const changeCartItemQuantity =
},
cache: 'no-store',
} );
dispatch.receiveCart( response );
return response;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
} finally {
dispatch.itemIsPendingQuantity( cartItemKey, false );
}
@ -376,8 +380,7 @@ export const changeCartItemQuantity =
* Selects a shipping rate.
*
* @param {string} rateId The id of the rate being selected.
* @param {number | string} [packageId] The key of the packages that we will
* select within.
* @param {number | string} [packageId] The key of the packages that we will select within.
*/
export const selectShippingRate =
( rateId: string, packageId = 0 ) =>
@ -393,14 +396,14 @@ export const selectShippingRate =
},
cache: 'no-store',
} );
dispatch.receiveCart( response );
return response as CartResponse;
} catch ( error ) {
dispatch.receiveError( error );
return Promise.reject( error );
} finally {
dispatch.shippingRatesBeingSelected( false );
}
return true;
};
/**
@ -428,9 +431,8 @@ export const updateCustomerData =
editing = true
) =>
async ( { dispatch }: { dispatch: CartDispatchFromMap } ) => {
dispatch.updatingCustomerData( true );
try {
dispatch.updatingCustomerData( true );
const { response } = await apiFetchWithHeaders( {
path: '/wc/store/v1/cart/update-customer',
method: 'POST',
@ -442,35 +444,35 @@ export const updateCustomerData =
} else {
dispatch.receiveCart( response );
}
dispatch.updatingCustomerData( false );
return response;
} catch ( error ) {
dispatch.receiveError( error );
dispatch.updatingCustomerData( false );
return Promise.reject( error );
} finally {
dispatch.updatingCustomerData( false );
}
return Promise.resolve( true );
};
export type CartAction = ReturnOrGeneratorYieldUnion<
| typeof receiveCartContents
| typeof setBillingAddress
| typeof setShippingAddress
| typeof setErrorData
| typeof receiveApplyingCoupon
| typeof receiveRemovingCoupon
| typeof receiveCartItem
| typeof itemIsPendingQuantity
| typeof itemIsPendingDelete
| typeof updatingCustomerData
| typeof shippingRatesBeingSelected
| typeof setIsCartDataStale
| typeof updateCustomerData
| typeof removeItemFromCart
| typeof changeCartItemQuantity
type Actions =
| typeof addItemToCart
| typeof setCartData
| typeof applyCoupon
| typeof changeCartItemQuantity
| typeof itemIsPendingDelete
| typeof itemIsPendingQuantity
| typeof receiveApplyingCoupon
| typeof receiveCartContents
| typeof receiveCartItem
| typeof receiveRemovingCoupon
| typeof removeCoupon
| typeof removeItemFromCart
| typeof selectShippingRate
>;
| typeof setBillingAddress
| typeof setCartData
| typeof setErrorData
| typeof setIsCartDataStale
| typeof setShippingAddress
| typeof shippingRatesBeingSelected
| typeof updateCustomerData
| typeof updatingCustomerData;
export type CartAction = ReturnOrGeneratorYieldUnion< Actions | Thunks >;

View File

@ -1,38 +1,11 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { ApiErrorResponse, isApiErrorResponse } from '@woocommerce/types';
import { createNotice, DEFAULT_ERROR_MESSAGE } from '@woocommerce/base-utils';
import { createNotice } from '@woocommerce/base-utils';
import { decodeEntities } from '@wordpress/html-entities';
import { dispatch } from '@wordpress/data';
/**
* This function is used to notify the user of cart errors.
*/
export const notifyErrors = ( error: ApiErrorResponse | null = null ) => {
if ( error === null || ! isApiErrorResponse( error ) ) {
return;
}
let errorMessage = error.message || DEFAULT_ERROR_MESSAGE;
// Replace the generic invalid JSON message with something more user friendly.
if ( error.code === 'invalid_json' ) {
errorMessage = __(
'Something went wrong. Please contact us for assistance.',
'woo-gutenberg-products-block'
);
}
// Create a new notice with a consistent error ID.
createNotice( 'error', errorMessage, {
id: 'woocommerce_cart_data_request_error',
context: 'wc/cart',
isDismissible: true,
} );
};
/**
* This function is used to notify the user of cart item errors/conflicts
*/

View File

@ -2,8 +2,8 @@
* External dependencies
*/
import {
CartResponse,
Cart,
CartResponse,
ApiErrorResponse,
isApiErrorResponse,
} from '@woocommerce/types';
@ -13,7 +13,7 @@ import { camelCase, mapKeys } from 'lodash';
* Internal dependencies
*/
import { notifyQuantityChanges } from './notify-quantity-changes';
import { notifyErrors, notifyCartErrors } from './notify-errors';
import { notifyCartErrors } from './notify-errors';
import { CartDispatchFromMap, CartSelectFromMap } from './index';
/**
@ -46,7 +46,7 @@ export const receiveCart =
};
/**
* A thunk used in updating the store with cart errors retrieved from a request. This also notifies the shopper of any errors that occur.
* A thunk used in updating the store with cart errors retrieved from a request.
*/
export const receiveError =
( response: ApiErrorResponse | null = null ) =>
@ -57,7 +57,7 @@ export const receiveError =
if ( response.data?.cart ) {
dispatch.receiveCart( response?.data?.cart );
}
notifyErrors( response );
}
};
export type Thunks = typeof receiveCart | typeof receiveError;

View File

@ -1,11 +1,7 @@
/**
* External dependencies
*/
import {
createNotice,
createNoticeIfVisible,
DEFAULT_ERROR_MESSAGE,
} from '@woocommerce/base-utils';
import { createNotice, DEFAULT_ERROR_MESSAGE } from '@woocommerce/base-utils';
import { decodeEntities } from '@wordpress/html-entities';
import {
objectHasProp,
@ -82,6 +78,35 @@ export const getErrorDetails = (
);
};
/**
* Gets appropriate error context from error code.
*/
const getErrorContextFromCode = ( code: string ): string => {
switch ( code ) {
case 'woocommerce_rest_missing_email_address':
case 'woocommerce_rest_invalid_email_address':
return noticeContexts.CONTACT_INFORMATION;
default:
return noticeContexts.CART;
}
};
/**
* Gets appropriate error context from error param name.
*/
const getErrorContextFromParam = ( param: string ): string | undefined => {
switch ( param ) {
case 'invalid_email':
return noticeContexts.CONTACT_INFORMATION;
case 'billing_address':
return noticeContexts.BILLING_ADDRESS;
case 'shipping_address':
return noticeContexts.SHIPPING_ADDRESS;
default:
return undefined;
}
};
/**
* Processes the response for an invalid param error, with response code rest_invalid_param.
*/
@ -92,28 +117,13 @@ const processInvalidParamResponse = (
const errorDetails = getErrorDetails( response );
errorDetails.forEach( ( { code, message, id, param } ) => {
switch ( code ) {
case 'invalid_email':
createNotice( 'error', message, {
id,
context: context || noticeContexts.CONTACT_INFORMATION,
} );
return;
}
switch ( param ) {
case 'billing_address':
createNoticeIfVisible( 'error', message, {
id,
context: context || noticeContexts.BILLING_ADDRESS,
} );
break;
case 'shipping_address':
createNoticeIfVisible( 'error', message, {
id,
context: context || noticeContexts.SHIPPING_ADDRESS,
} );
break;
}
createNotice( 'error', message, {
id,
context:
context ||
getErrorContextFromParam( param ) ||
getErrorContextFromCode( code ),
} );
} );
};
@ -123,27 +133,27 @@ const processInvalidParamResponse = (
* This is where we can handle specific error codes and display notices in specific contexts.
*/
export const processErrorResponse = (
response: ApiErrorResponse,
context: string | undefined
response: ApiErrorResponse | null,
context?: string | undefined
) => {
if ( ! isApiErrorResponse( response ) ) {
return;
}
switch ( response.code ) {
case 'woocommerce_rest_missing_email_address':
case 'woocommerce_rest_invalid_email_address':
createNotice( 'error', response.message, {
id: response.code,
context: context || noticeContexts.CONTACT_INFORMATION,
} );
break;
case 'rest_invalid_param':
processInvalidParamResponse( response, context );
break;
default:
createNotice( 'error', response.message || DEFAULT_ERROR_MESSAGE, {
id: response.code,
context: context || noticeContexts.CHECKOUT,
} );
if ( response.code === 'rest_invalid_param' ) {
return processInvalidParamResponse( response, context );
}
let errorMessage =
decodeEntities( response.message ) || DEFAULT_ERROR_MESSAGE;
// Replace the generic invalid JSON message with something more user friendly.
if ( response.code === 'invalid_json' ) {
errorMessage = DEFAULT_ERROR_MESSAGE;
}
createNotice( 'error', errorMessage, {
id: response.code,
context: context || getErrorContextFromCode( response.code ),
} );
};

View File

@ -31,8 +31,8 @@ export interface StoreCartItemQuantity {
export interface StoreCartCoupon {
appliedCoupons: CartResponseCouponItem[];
isLoading: boolean;
applyCoupon: ( coupon: string ) => void;
removeCoupon: ( coupon: string ) => void;
applyCoupon: ( coupon: string ) => Promise< boolean >;
removeCoupon: ( coupon: string ) => Promise< boolean >;
isApplyingCoupon: boolean;
isRemovingCoupon: boolean;
}
@ -58,6 +58,7 @@ export interface StoreCart {
cartHasCalculatedShipping: boolean;
paymentRequirements: string[];
receiveCart: ( cart: CartResponse ) => void;
receiveCartContents: ( cart: CartResponse ) => void;
}
export type Query = {