initial commit
This commit is contained in:
@ -0,0 +1,3 @@
|
||||
export * from './use-store-cart';
|
||||
export * from './use-store-cart-coupons';
|
||||
export * from './use-store-cart-item-quantity';
|
@ -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 );
|
||||
} );
|
||||
} );
|
@ -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 );
|
||||
} );
|
||||
} );
|
||||
} );
|
@ -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,
|
||||
};
|
||||
};
|
@ -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,
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
Reference in New Issue
Block a user