initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1,3 @@
export * from './use-store-cart';
export * from './use-store-cart-coupons';
export * from './use-store-cart-item-quantity';

View File

@ -0,0 +1,206 @@
/**
* External dependencies
*/
import TestRenderer, { act } from 'react-test-renderer';
import { createRegistry, RegistryProvider } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import * as mockUseStoreCart from '../use-store-cart';
import { useStoreCartItemQuantity } from '../use-store-cart-item-quantity';
jest.mock( '../use-store-cart', () => ( {
useStoreCart: jest.fn(),
} ) );
jest.mock( '@woocommerce/block-data', () => ( {
__esModule: true,
CART_STORE_KEY: 'test/store',
} ) );
// Make debounce instantaneous.
jest.mock( 'use-debounce', () => ( {
useDebounce: ( a ) => [ a ],
} ) );
describe( 'useStoreCartItemQuantity', () => {
let registry, renderer;
const getWrappedComponents = ( Component ) => (
<RegistryProvider value={ registry }>
<Component />
</RegistryProvider>
);
const getTestComponent = ( options ) => () => {
const props = useStoreCartItemQuantity( options );
return <div { ...props } />;
};
let mockRemoveItemFromCart;
let mockChangeCartItemQuantity;
const setupMocks = ( { isPendingDelete, isPendingQuantity } ) => {
mockRemoveItemFromCart = jest
.fn()
.mockReturnValue( { type: 'removeItemFromCartAction' } );
mockChangeCartItemQuantity = jest
.fn()
.mockReturnValue( { type: 'changeCartItemQuantityAction' } );
registry.registerStore( storeKey, {
reducer: () => ( {} ),
actions: {
removeItemFromCart: mockRemoveItemFromCart,
changeCartItemQuantity: mockChangeCartItemQuantity,
},
selectors: {
isItemPendingDelete: jest
.fn()
.mockReturnValue( isPendingDelete ),
isItemPendingQuantity: jest
.fn()
.mockReturnValue( isPendingQuantity ),
},
} );
};
beforeEach( () => {
registry = createRegistry();
renderer = null;
} );
afterEach( () => {
mockRemoveItemFromCart.mockReset();
mockChangeCartItemQuantity.mockReset();
} );
describe( 'with no errors and not pending', () => {
beforeEach( () => {
setupMocks( { isPendingDelete: false, isPendingQuantity: false } );
mockUseStoreCart.useStoreCart.mockReturnValue( {
cartErrors: {},
} );
} );
it( 'update quantity value should happen instantly', () => {
const TestComponent = getTestComponent( {
key: '123',
quantity: 1,
} );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { setItemQuantity, quantity } = renderer.root.findByType(
'div'
).props;
expect( quantity ).toBe( 1 );
act( () => {
setItemQuantity( 2 );
} );
const { quantity: newQuantity } = renderer.root.findByType(
'div'
).props;
expect( newQuantity ).toBe( 2 );
} );
it( 'removeItem should call the dispatch action', () => {
const TestComponent = getTestComponent( {
key: '123',
quantity: 1,
} );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { removeItem } = renderer.root.findByType( 'div' ).props;
act( () => {
removeItem();
} );
expect( mockRemoveItemFromCart ).toHaveBeenCalledWith( '123' );
} );
it( 'setItemQuantity should call the dispatch action', () => {
const TestComponent = getTestComponent( {
key: '123',
quantity: 1,
} );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { setItemQuantity } = renderer.root.findByType( 'div' ).props;
act( () => {
setItemQuantity( 2 );
} );
expect( mockChangeCartItemQuantity.mock.calls ).toEqual( [
[ '123', 2 ],
] );
} );
} );
it( 'should expose store errors', () => {
const mockCartErrors = [ { message: 'Test error' } ];
setupMocks( { isPendingDelete: false, isPendingQuantity: false } );
mockUseStoreCart.useStoreCart.mockReturnValue( {
cartErrors: mockCartErrors,
} );
const TestComponent = getTestComponent( {
key: '123',
quantity: 1,
} );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { cartItemQuantityErrors } = renderer.root.findByType(
'div'
).props;
expect( cartItemQuantityErrors ).toEqual( mockCartErrors );
} );
it( 'isPendingDelete should depend on the value provided by the store', () => {
setupMocks( { isPendingDelete: true, isPendingQuantity: false } );
mockUseStoreCart.useStoreCart.mockReturnValue( {
cartErrors: {},
} );
const TestComponent = getTestComponent( {
key: '123',
quantity: 1,
} );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { isPendingDelete } = renderer.root.findByType( 'div' ).props;
expect( isPendingDelete ).toBe( true );
} );
} );

View File

@ -0,0 +1,235 @@
/**
* External dependencies
*/
import TestRenderer, { act } from 'react-test-renderer';
import { createRegistry, RegistryProvider } from '@wordpress/data';
import { previewCart } from '@woocommerce/resource-previews';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
/**
* Internal dependencies
*/
import { defaultCartData, useStoreCart } from '../use-store-cart';
import { useEditorContext } from '../../../providers/editor-context';
jest.mock( '../../../providers/editor-context', () => ( {
useEditorContext: jest.fn(),
} ) );
jest.mock( '@woocommerce/block-data', () => ( {
...jest.requireActual( '@woocommerce/block-data' ),
__esModule: true,
CART_STORE_KEY: 'test/store',
} ) );
describe( 'useStoreCart', () => {
let registry, renderer;
const receiveCartMock = () => {};
const previewCartData = {
cartCoupons: previewCart.coupons,
cartItems: previewCart.items,
cartFees: previewCart.fees,
cartItemsCount: previewCart.items_count,
cartItemsWeight: previewCart.items_weight,
cartNeedsPayment: previewCart.needs_payment,
cartNeedsShipping: previewCart.needs_shipping,
cartTotals: previewCart.totals,
cartIsLoading: false,
cartItemErrors: [],
cartErrors: [],
billingAddress: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
email: '',
phone: '',
},
shippingAddress: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
phone: '',
},
shippingRates: previewCart.shipping_rates,
extensions: {},
shippingRatesLoading: false,
cartHasCalculatedShipping: true,
};
const mockCartItems = [ { key: '1', id: 1, name: 'Lorem Ipsum' } ];
const mockShippingAddress = {
city: 'New York',
};
const mockCartData = {
coupons: [],
items: mockCartItems,
fees: [],
itemsCount: 1,
itemsWeight: 10,
needsPayment: true,
needsShipping: true,
billingAddress: {},
shippingAddress: mockShippingAddress,
shippingRates: [],
hasCalculatedShipping: true,
extensions: {},
errors: [],
receiveCart: undefined,
paymentRequirements: [],
};
const mockCartTotals = {
currency_code: 'USD',
};
const mockCartIsLoading = false;
const mockCartErrors = [];
const mockStoreCartData = {
cartCoupons: [],
cartItems: mockCartItems,
cartItemErrors: [],
cartItemsCount: 1,
cartItemsWeight: 10,
cartNeedsPayment: true,
cartNeedsShipping: true,
cartTotals: mockCartTotals,
cartIsLoading: mockCartIsLoading,
cartErrors: mockCartErrors,
cartFees: [],
billingAddress: {},
shippingAddress: mockShippingAddress,
shippingRates: [],
extensions: {},
shippingRatesLoading: false,
cartHasCalculatedShipping: true,
receiveCart: undefined,
paymentRequirements: [],
};
const getWrappedComponents = ( Component ) => (
<RegistryProvider value={ registry }>
<Component />
</RegistryProvider>
);
const getTestComponent = ( options ) => () => {
const { receiveCart, ...results } = useStoreCart( options );
return <div results={ results } receiveCart={ receiveCart } />;
};
const setUpMocks = () => {
const mocks = {
selectors: {
getCartData: jest.fn().mockReturnValue( mockCartData ),
getCartErrors: jest.fn().mockReturnValue( mockCartErrors ),
getCartTotals: jest.fn().mockReturnValue( mockCartTotals ),
hasFinishedResolution: jest
.fn()
.mockReturnValue( ! mockCartIsLoading ),
isCustomerDataUpdating: jest.fn().mockReturnValue( false ),
},
};
registry.registerStore( storeKey, {
reducer: () => ( {} ),
selectors: mocks.selectors,
} );
};
beforeEach( () => {
registry = createRegistry();
renderer = null;
setUpMocks();
} );
afterEach( () => {
useEditorContext.mockReset();
} );
describe( 'in frontend', () => {
beforeEach( () => {
useEditorContext.mockReturnValue( {
isEditor: false,
} );
} );
it( 'return default data when shouldSelect is false', () => {
const TestComponent = getTestComponent( { shouldSelect: false } );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { results, receiveCart } = renderer.root.findByType(
'div'
).props;
const {
receiveCart: defaultReceiveCart,
...remaining
} = defaultCartData;
expect( results ).toEqual( remaining );
expect( receiveCart ).toEqual( defaultReceiveCart );
} );
it( 'return store data when shouldSelect is true', () => {
const TestComponent = getTestComponent( { shouldSelect: true } );
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { results, receiveCart } = renderer.root.findByType(
'div'
).props;
expect( results ).toEqual( mockStoreCartData );
expect( receiveCart ).toBeUndefined();
} );
} );
describe( 'in editor', () => {
beforeEach( () => {
useEditorContext.mockReturnValue( {
isEditor: true,
previewData: {
previewCart: {
...previewCart,
receiveCart: receiveCartMock,
},
},
} );
} );
it( 'return preview data in editor', () => {
const TestComponent = getTestComponent();
act( () => {
renderer = TestRenderer.create(
getWrappedComponents( TestComponent )
);
} );
const { results, receiveCart } = renderer.root.findByType(
'div'
).props;
expect( results ).toEqual( previewCartData );
expect( receiveCart ).toEqual( receiveCartMock );
} );
} );
} );

View File

@ -0,0 +1,126 @@
/** @typedef { import('@woocommerce/type-defs/hooks').StoreCartCoupon } StoreCartCoupon */
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { decodeEntities } from '@wordpress/html-entities';
import type { StoreCartCoupon } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { useStoreCart } from './use-store-cart';
import { useStoreSnackbarNotices } from '../use-store-snackbar-notices';
import { useValidationContext } from '../../providers/validation';
import { useStoreNotices } from '../use-store-notices';
/**
* This is a custom hook for loading the Store API /cart/coupons endpoint and an
* action for adding a coupon _to_ the cart.
* See also: https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/trunk/src/RestApi/StoreApi
*
* @return {StoreCartCoupon} An object exposing data and actions from/for the
* store api /cart/coupons endpoint.
*/
export const useStoreCartCoupons = (): StoreCartCoupon => {
const { cartCoupons, cartIsLoading } = useStoreCart();
const { addErrorNotice } = useStoreNotices();
const { addSnackbarNotice } = useStoreSnackbarNotices();
const { setValidationErrors } = useValidationContext();
const results: Pick<
StoreCartCoupon,
'applyCoupon' | 'removeCoupon' | 'isApplyingCoupon' | 'isRemovingCoupon'
> = useSelect(
( select, { dispatch } ) => {
const store = select( storeKey );
const isApplyingCoupon = store.isApplyingCoupon();
const isRemovingCoupon = store.isRemovingCoupon();
const {
applyCoupon,
removeCoupon,
receiveApplyingCoupon,
}: {
applyCoupon: ( coupon: string ) => Promise< boolean >;
removeCoupon: ( coupon: string ) => Promise< boolean >;
receiveApplyingCoupon: ( coupon: string ) => void;
} = dispatch( storeKey );
const applyCouponWithNotices = ( couponCode: string ) => {
applyCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
addSnackbarNotice(
sprintf(
/* translators: %s coupon code. */
__(
'Coupon code "%s" has been applied to your cart.',
'woo-gutenberg-products-block'
),
couponCode
),
{
id: 'coupon-form',
}
);
}
} )
.catch( ( error ) => {
setValidationErrors( {
coupon: {
message: decodeEntities( error.message ),
hidden: false,
},
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
} );
};
const removeCouponWithNotices = ( couponCode: string ) => {
removeCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
addSnackbarNotice(
sprintf(
/* translators: %s coupon code. */
__(
'Coupon code "%s" has been removed from your cart.',
'woo-gutenberg-products-block'
),
couponCode
),
{
id: 'coupon-form',
}
);
}
} )
.catch( ( error ) => {
addErrorNotice( error.message, {
id: 'coupon-form',
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
} );
};
return {
applyCoupon: applyCouponWithNotices,
removeCoupon: removeCouponWithNotices,
isApplyingCoupon,
isRemovingCoupon,
};
},
[ addErrorNotice, addSnackbarNotice ]
);
return {
appliedCoupons: cartCoupons,
isLoading: cartIsLoading,
...results,
};
};

View File

@ -0,0 +1,146 @@
/**
* External dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useCallback, useState, useEffect } from '@wordpress/element';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useDebounce } from 'use-debounce';
import { usePrevious } from '@woocommerce/base-hooks';
import { triggerFragmentRefresh } from '@woocommerce/base-utils';
import {
CartItem,
StoreCartItemQuantity,
isNumber,
isObject,
isString,
objectHasProp,
} from '@woocommerce/types';
/**
* Internal dependencies
*/
import { useStoreCart } from './use-store-cart';
import { useCheckoutContext } from '../../providers/cart-checkout';
/**
* Ensures the object passed has props key: string and quantity: number
*/
const cartItemHasQuantityAndKey = (
cartItem: unknown /* Object that may have quantity and key */
): cartItem is Partial< CartItem > =>
isObject( cartItem ) &&
objectHasProp( cartItem, 'key' ) &&
objectHasProp( cartItem, 'quantity' ) &&
isString( cartItem.key ) &&
isNumber( cartItem.quantity );
/**
* This is a custom hook for loading the Store API /cart/ endpoint and actions for removing or changing item quantity.
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/trunk/src/RestApi/StoreApi
*
* @param {CartItem} cartItem The cartItem to get quantity info from and will have quantity updated on.
* @return {StoreCartItemQuantity} An object exposing data and actions relating to cart items.
*/
export const useStoreCartItemQuantity = (
cartItem: CartItem | Record< string, unknown >
): StoreCartItemQuantity => {
const verifiedCartItem = { key: '', quantity: 1 };
if ( cartItemHasQuantityAndKey( cartItem ) ) {
verifiedCartItem.key = cartItem.key;
verifiedCartItem.quantity = cartItem.quantity;
}
const {
key: cartItemKey = '',
quantity: cartItemQuantity = 1,
} = verifiedCartItem;
const { cartErrors } = useStoreCart();
const { dispatchActions } = useCheckoutContext();
// Store quantity in hook state. This is used to keep the UI updated while server request is updated.
const [ quantity, setQuantity ] = useState< number >( cartItemQuantity );
const [ debouncedQuantity ] = useDebounce< number >( quantity, 400 );
const previousDebouncedQuantity = usePrevious( debouncedQuantity );
const { removeItemFromCart, changeCartItemQuantity } = useDispatch(
storeKey
);
// Track when things are already pending updates.
const isPending = useSelect(
( select ) => {
if ( ! cartItemKey ) {
return {
quantity: false,
delete: false,
};
}
const store = select( storeKey );
return {
quantity: store.isItemPendingQuantity( cartItemKey ),
delete: store.isItemPendingDelete( cartItemKey ),
};
},
[ cartItemKey ]
);
const removeItem = useCallback( () => {
return cartItemKey
? removeItemFromCart( cartItemKey ).then( () => {
triggerFragmentRefresh();
return true;
} )
: Promise.resolve( false );
}, [ cartItemKey, removeItemFromCart ] );
// Observe debounced quantity value, fire action to update server on change.
useEffect( () => {
if (
cartItemKey &&
isNumber( previousDebouncedQuantity ) &&
Number.isFinite( previousDebouncedQuantity ) &&
previousDebouncedQuantity !== debouncedQuantity
) {
changeCartItemQuantity( cartItemKey, debouncedQuantity );
}
}, [
cartItemKey,
changeCartItemQuantity,
debouncedQuantity,
previousDebouncedQuantity,
] );
useEffect( () => {
if ( isPending.delete ) {
dispatchActions.incrementCalculating();
} else {
dispatchActions.decrementCalculating();
}
return () => {
if ( isPending.delete ) {
dispatchActions.decrementCalculating();
}
};
}, [ dispatchActions, isPending.delete ] );
useEffect( () => {
if ( isPending.quantity || debouncedQuantity !== quantity ) {
dispatchActions.incrementCalculating();
} else {
dispatchActions.decrementCalculating();
}
return () => {
if ( isPending.quantity || debouncedQuantity !== quantity ) {
dispatchActions.decrementCalculating();
}
};
}, [ dispatchActions, isPending.quantity, debouncedQuantity, quantity ] );
return {
isPendingDelete: isPending.delete,
quantity,
setItemQuantity: setQuantity,
removeItem,
cartItemQuantityErrors: cartErrors,
};
};

View File

@ -0,0 +1,240 @@
/** @typedef { import('@woocommerce/type-defs/hooks').StoreCart } StoreCart */
/**
* External dependencies
*/
import { isEqual } from 'lodash';
import { useRef } from '@wordpress/element';
import {
CART_STORE_KEY as storeKey,
EMPTY_CART_COUPONS,
EMPTY_CART_ITEMS,
EMPTY_CART_FEES,
EMPTY_CART_ITEM_ERRORS,
EMPTY_CART_ERRORS,
EMPTY_SHIPPING_RATES,
EMPTY_TAX_LINES,
EMPTY_PAYMENT_REQUIREMENTS,
EMPTY_EXTENSIONS,
} from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import type {
StoreCart,
CartResponseTotals,
CartResponseFeeItem,
CartResponseBillingAddress,
CartResponseShippingAddress,
CartResponseCouponItem,
CartResponseCoupons,
} from '@woocommerce/types';
import {
emptyHiddenAddressFields,
fromEntriesPolyfill,
} from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import { useEditorContext } from '../../providers/editor-context';
declare module '@wordpress/html-entities' {
// eslint-disable-next-line @typescript-eslint/no-shadow
export function decodeEntities< T >( coupon: T ): T;
}
const defaultShippingAddress: CartResponseShippingAddress = {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
phone: '',
};
const defaultBillingAddress: CartResponseBillingAddress = {
...defaultShippingAddress,
email: '',
};
const defaultCartTotals: CartResponseTotals = {
total_items: '',
total_items_tax: '',
total_fees: '',
total_fees_tax: '',
total_discount: '',
total_discount_tax: '',
total_shipping: '',
total_shipping_tax: '',
total_price: '',
total_tax: '',
tax_lines: EMPTY_TAX_LINES,
currency_code: '',
currency_symbol: '',
currency_minor_unit: 2,
currency_decimal_separator: '',
currency_thousand_separator: '',
currency_prefix: '',
currency_suffix: '',
};
const decodeValues = (
object: Record< string, unknown >
): Record< string, unknown > =>
fromEntriesPolyfill(
Object.entries( object ).map( ( [ key, value ] ) => [
key,
decodeEntities( value ),
] )
);
/**
* @constant
* @type {StoreCart} Object containing cart data.
*/
export const defaultCartData: StoreCart = {
cartCoupons: EMPTY_CART_COUPONS,
cartItems: EMPTY_CART_ITEMS,
cartFees: EMPTY_CART_FEES,
cartItemsCount: 0,
cartItemsWeight: 0,
cartNeedsPayment: true,
cartNeedsShipping: true,
cartItemErrors: EMPTY_CART_ITEM_ERRORS,
cartTotals: defaultCartTotals,
cartIsLoading: true,
cartErrors: EMPTY_CART_ERRORS,
billingAddress: defaultBillingAddress,
shippingAddress: defaultShippingAddress,
shippingRates: EMPTY_SHIPPING_RATES,
shippingRatesLoading: false,
cartHasCalculatedShipping: false,
paymentRequirements: EMPTY_PAYMENT_REQUIREMENTS,
receiveCart: () => undefined,
extensions: EMPTY_EXTENSIONS,
};
/**
* This is a custom hook that is wired up to the `wc/store/cart` data
* store.
*
* @param {Object} options An object declaring the various
* collection arguments.
* @param {boolean} options.shouldSelect If false, the previous results will be
* returned and internal selects will not
* fire.
*
* @return {StoreCart} Object containing cart data.
*/
export const useStoreCart = (
options: { shouldSelect: boolean } = { shouldSelect: true }
): StoreCart => {
const { isEditor, previewData } = useEditorContext();
const previewCart = previewData?.previewCart;
const { shouldSelect } = options;
const currentResults = useRef();
const results: StoreCart = useSelect(
( select, { dispatch } ) => {
if ( ! shouldSelect ) {
return defaultCartData;
}
if ( isEditor ) {
return {
cartCoupons: previewCart.coupons,
cartItems: previewCart.items,
cartFees: previewCart.fees,
cartItemsCount: previewCart.items_count,
cartItemsWeight: previewCart.items_weight,
cartNeedsPayment: previewCart.needs_payment,
cartNeedsShipping: previewCart.needs_shipping,
cartItemErrors: EMPTY_CART_ITEM_ERRORS,
cartTotals: previewCart.totals,
cartIsLoading: false,
cartErrors: EMPTY_CART_ERRORS,
billingAddress: defaultBillingAddress,
shippingAddress: defaultShippingAddress,
extensions: EMPTY_EXTENSIONS,
shippingRates: previewCart.shipping_rates,
shippingRatesLoading: false,
cartHasCalculatedShipping:
previewCart.has_calculated_shipping,
paymentRequirements: previewCart.paymentRequirements,
receiveCart:
typeof previewCart?.receiveCart === 'function'
? previewCart.receiveCart
: () => undefined,
};
}
const store = select( storeKey );
const cartData = store.getCartData();
const cartErrors = store.getCartErrors();
const cartTotals = store.getCartTotals();
const cartIsLoading = ! store.hasFinishedResolution(
'getCartData'
);
const shippingRatesLoading = store.isCustomerDataUpdating();
const { receiveCart } = dispatch( storeKey );
const billingAddress = decodeValues( cartData.billingAddress );
const shippingAddress = cartData.needsShipping
? decodeValues( cartData.shippingAddress )
: billingAddress;
const cartFees =
cartData.fees.length > 0
? cartData.fees.map( ( fee: CartResponseFeeItem ) =>
decodeValues( fee )
)
: EMPTY_CART_FEES;
// Add a text property to the coupon to allow extensions to modify
// the text used to display the coupon, without affecting the
// functionality when it comes to removing the coupon.
const cartCoupons: CartResponseCoupons =
cartData.coupons.length > 0
? cartData.coupons.map(
( coupon: CartResponseCouponItem ) => ( {
...coupon,
label: coupon.code,
} )
)
: EMPTY_CART_COUPONS;
return {
cartCoupons,
cartItems: cartData.items,
cartFees,
cartItemsCount: cartData.itemsCount,
cartItemsWeight: cartData.itemsWeight,
cartNeedsPayment: cartData.needsPayment,
cartNeedsShipping: cartData.needsShipping,
cartItemErrors: cartData.errors,
cartTotals,
cartIsLoading,
cartErrors,
billingAddress: emptyHiddenAddressFields( billingAddress ),
shippingAddress: emptyHiddenAddressFields( shippingAddress ),
extensions: cartData.extensions,
shippingRates: cartData.shippingRates,
shippingRatesLoading,
cartHasCalculatedShipping: cartData.hasCalculatedShipping,
paymentRequirements: cartData.paymentRequirements,
receiveCart,
};
},
[ shouldSelect ]
);
if (
! currentResults.current ||
! isEqual( currentResults.current, results )
) {
currentResults.current = results;
}
return currentResults.current;
};