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:
parent
dc283a0e0a
commit
1d45dacc3e
|
@ -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">
|
||||
|
|
|
@ -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 } />;
|
||||
|
|
|
@ -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 );
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -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 );
|
||||
} );
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
] );
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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 >;
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ),
|
||||
} );
|
||||
};
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue