* type hooks

* type useStoreCart

* type the rest

* Remove old typedefs

* Specify that the return value from useDispatch is a Promise< void >

* Change return type of removeItem

* If ref.current is undefined then return value without checking shallowEq

* Revert "Specify that the return value from useDispatch is a Promise< void >"

This reverts commit 97863bd584d38024398913a79ce63fa6b964846a.

* Remove type parameter from removeItem and changeCartItemQuantity

* Change action return type to cater for generator functions

* Remove type parameter from addItemToCart

* Add mapped types file to help with actions that are generator functions

* Include addItemToCart in return types of cart actions

* Use custom DispatchFromMap

* Add todos for why we've redefined functionality that exists in Calypso

* update types

* remove fromEntries and use polyfill

* address review

* ignore ts no shadow

* fix test errors

Co-authored-by: Thomas Roberts <thomas.roberts@automattic.com>
This commit is contained in:
Seghir Nadir 2021-03-10 16:03:26 +01:00 committed by GitHub
parent 1a010d70fa
commit dff4772d82
29 changed files with 388 additions and 120 deletions

View File

@ -85,5 +85,13 @@ module.exports = {
'@typescript-eslint/no-shadow': [ 'error' ],
},
},
{
files: [ './assets/js/mapped-types.ts' ],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-shadow': 'off',
'no-shadow': 'off',
},
},
],
};

View File

@ -16,15 +16,13 @@ describe( 'assertValidContextValue', () => {
},
};
it.each`
testValue | expectedMessage | expectError
${ {} } | ${ 'expected' } | ${ true }
${ 10 } | ${ 'expected' } | ${ true }
${ { amountKetchup: 20 } } | ${ 'not expected' } | ${ false }
${ { amountKetchup: '10' } } | ${ 'expected' } | ${ true }
${ { cheeseburger: 'fries', amountKetchup: 20 } } | ${ 'not expected' } | ${ false }
testValue
${ {} }
${ 10 }
${ { amountKetchup: '10' } }
`(
'The value of $testValue is $expectedMessage to trigger an Error',
( { testValue, expectError } ) => {
'The value of $testValue is expected to trigger an Error',
( { testValue } ) => {
const invokeTest = () => {
assertValidContextValue(
contextName,
@ -32,11 +30,24 @@ describe( 'assertValidContextValue', () => {
testValue
);
};
if ( expectError ) {
expect( invokeTest ).toThrow();
} else {
expect( invokeTest ).not.toThrow();
}
expect( invokeTest ).toThrow();
}
);
it.each`
testValue
${ { amountKetchup: 20 } }
${ { cheeseburger: 'fries', amountKetchup: 20 } }
`(
'The value of $testValue is not expected to trigger an Error',
( { testValue } ) => {
const invokeTest = () => {
assertValidContextValue(
contextName,
validationMap,
testValue
);
};
expect( invokeTest ).not.toThrow();
}
);
} );

View File

@ -8,7 +8,7 @@ import { useSelect } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useValidationContext } from '@woocommerce/base-context';
import { decodeEntities } from '@wordpress/html-entities';
import type { StoreCartCoupon } from '@woocommerce/types';
/**
* Internal dependencies
*/
@ -23,12 +23,15 @@ import { useStoreNotices } from '../use-store-notices';
* @return {StoreCartCoupon} An object exposing data and actions from/for the
* store api /cart/coupons endpoint.
*/
export const useStoreCartCoupons = () => {
export const useStoreCartCoupons = (): StoreCartCoupon => {
const { cartCoupons, cartIsLoading } = useStoreCart();
const { addErrorNotice, addSnackbarNotice } = useStoreNotices();
const { setValidationErrors } = useValidationContext();
const results = useSelect(
const results: Pick<
StoreCartCoupon,
'applyCoupon' | 'removeCoupon' | 'isApplyingCoupon' | 'isRemovingCoupon'
> = useSelect(
( select, { dispatch } ) => {
const store = select( storeKey );
const isApplyingCoupon = store.isApplyingCoupon();
@ -37,9 +40,13 @@ export const useStoreCartCoupons = () => {
applyCoupon,
removeCoupon,
receiveApplyingCoupon,
}: {
applyCoupon: ( coupon: string ) => Promise< boolean >;
removeCoupon: ( coupon: string ) => Promise< boolean >;
receiveApplyingCoupon: ( coupon: string ) => void;
} = dispatch( storeKey );
const applyCouponWithNotices = ( couponCode ) => {
const applyCouponWithNotices = ( couponCode: string ) => {
applyCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
@ -70,7 +77,7 @@ export const useStoreCartCoupons = () => {
} );
};
const removeCouponWithNotices = ( couponCode ) => {
const removeCouponWithNotices = ( couponCode: string ) => {
removeCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {

View File

@ -2,23 +2,17 @@
* External dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { useState, useEffect, useCallback } from '@wordpress/element';
import { useState, useEffect } from '@wordpress/element';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useDebounce } from 'use-debounce';
import { useCheckoutContext } from '@woocommerce/base-context';
import { triggerFragmentRefresh } from '@woocommerce/base-utils';
import type { CartItem, StoreCartItemQuantity } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { useStoreCart } from './use-store-cart';
import { usePrevious } from '../use-previous';
/**
* @typedef {import('@woocommerce/type-defs/hooks').StoreCartItemQuantity} StoreCartItemQuantity
* @typedef {import('@woocommerce/type-defs/cart').CartItem} CartItem
*/
/**
* This is a custom hook for loading the Store API /cart/ endpoint and
* actions for removing or changing item quantity.
@ -30,23 +24,17 @@ import { usePrevious } from '../use-previous';
* @return {StoreCartItemQuantity} An object exposing data and actions relating
* to cart items.
*/
export const useStoreCartItemQuantity = ( cartItem ) => {
export const useStoreCartItemQuantity = (
cartItem: CartItem
): StoreCartItemQuantity => {
const { key: cartItemKey = '', quantity: cartItemQuantity = 1 } = cartItem;
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, changeQuantity ] = useState( cartItemQuantity );
// Quantity can be updated from somewhere else, this is to keep it in sync.
useEffect( () => {
changeQuantity( ( currentQuantity ) =>
cartItemQuantity !== currentQuantity
? cartItemQuantity
: currentQuantity
);
}, [ cartItemQuantity ] );
const [ debouncedQuantity ] = useDebounce( quantity, 400 );
const [ quantity, changeQuantity ] = useState< number >( cartItemQuantity );
const [ debouncedQuantity ] = useDebounce< number >( quantity, 400 );
const previousDebouncedQuantity = usePrevious( debouncedQuantity );
const { removeItemFromCart, changeCartItemQuantity } = useDispatch(
storeKey
@ -78,13 +66,13 @@ export const useStoreCartItemQuantity = ( cartItem ) => {
);
const previousIsPendingDelete = usePrevious( isPendingDelete );
const removeItem = useCallback( () => {
const removeItem = () => {
return cartItemKey
? removeItemFromCart( cartItemKey ).then( () => {
triggerFragmentRefresh();
} )
: false;
}, [ cartItemKey, removeItemFromCart ] );
};
// Observe debounced quantity value, fire action to update server on change.
useEffect( () => {

View File

@ -7,9 +7,20 @@ import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import { useEditorContext } from '@woocommerce/base-context';
import { decodeEntities } from '@wordpress/html-entities';
import { mapValues } from 'lodash';
import type {
StoreCart,
CartResponseTotals,
CartResponseFeeItem,
CartResponseBillingAddress,
CartResponseShippingAddress,
} from '@woocommerce/types';
import { fromEntriesPolyfill } from '@woocommerce/base-utils';
const defaultShippingAddress = {
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: '',
@ -21,20 +32,48 @@ const defaultShippingAddress = {
country: '',
};
const defaultBillingAddress = {
const defaultBillingAddress: CartResponseBillingAddress = {
...defaultShippingAddress,
email: '',
phone: '',
};
const decodeValues = ( object ) =>
mapValues( object, ( value ) => decodeEntities( value ) );
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: [],
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 = {
export const defaultCartData: StoreCart = {
cartCoupons: [],
cartItems: [],
cartFees: [],
@ -43,7 +82,7 @@ export const defaultCartData = {
cartNeedsPayment: true,
cartNeedsShipping: true,
cartItemErrors: [],
cartTotals: {},
cartTotals: defaultCartTotals,
cartIsLoading: true,
cartErrors: [],
billingAddress: defaultBillingAddress,
@ -52,7 +91,7 @@ export const defaultCartData = {
shippingRatesLoading: false,
cartHasCalculatedShipping: false,
paymentRequirements: [],
receiveCart: () => {},
receiveCart: () => undefined,
extensions: {},
};
@ -68,12 +107,14 @@ export const defaultCartData = {
*
* @return {StoreCart} Object containing cart data.
*/
export const useStoreCart = ( options = { shouldSelect: true } ) => {
export const useStoreCart = (
options: { shouldSelect: boolean } = { shouldSelect: true }
): StoreCart => {
const { isEditor, previewData } = useEditorContext();
const previewCart = previewData?.previewCart || {};
const { shouldSelect } = options;
const results = useSelect(
const results: StoreCart = useSelect(
( select, { dispatch } ) => {
if ( ! shouldSelect ) {
return defaultCartData;
@ -103,7 +144,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
receiveCart:
typeof previewCart?.receiveCart === 'function'
? previewCart.receiveCart
: () => {},
: () => undefined,
};
}
@ -120,7 +161,7 @@ export const useStoreCart = ( options = { shouldSelect: true } ) => {
const shippingAddress = cartData.needsShipping
? decodeValues( cartData.shippingAddress )
: billingAddress;
const cartFees = cartData.fees.map( ( fee ) =>
const cartFees = cartData.fees.map( ( fee: CartResponseFeeItem ) =>
decodeValues( fee )
);
return {

View File

@ -7,7 +7,15 @@ import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useDebounce } from 'use-debounce';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { formatStoreApiErrorMessage } from '@woocommerce/base-utils';
import type {
CartResponseBillingAddress,
CartResponseShippingAddress,
} from '@woocommerce/types';
declare type CustomerData = {
billingData: CartResponseBillingAddress;
shippingAddress: CartResponseShippingAddress;
};
/**
* Internal dependencies
*/
@ -18,7 +26,12 @@ import { useStoreCart } from '../cart';
/**
* This is a custom hook for syncing customer address data (billing and shipping) with the server.
*/
export const useCustomerData = () => {
export const useCustomerData = (): {
billingData: CartResponseBillingAddress;
shippingAddress: CartResponseShippingAddress;
setBillingData: ( data: CartResponseBillingAddress ) => void;
setShippingAddress: ( data: CartResponseBillingAddress ) => void;
} => {
const { updateCustomerData } = useDispatch( storeKey );
const { addErrorNotice, removeNotice } = useStoreNotices();
@ -26,16 +39,18 @@ export const useCustomerData = () => {
const {
billingAddress: initialBillingAddress,
shippingAddress: initialShippingAddress,
}: Omit< CustomerData, 'billingData' > & {
billingAddress: CartResponseBillingAddress;
} = useStoreCart();
// State of customer data is tracked here from this point, using the initial values from the useStoreCart hook.
const [ customerData, setCustomerData ] = useState( {
const [ customerData, setCustomerData ] = useState< CustomerData >( {
billingData: initialBillingAddress,
shippingAddress: initialShippingAddress,
} );
// Store values last sent to the server in a ref to avoid requests unless important fields are changed.
const previousCustomerData = useRef( customerData );
const previousCustomerData = useRef< CustomerData >( customerData );
// Debounce updates to the customerData state so it's not triggered excessively.
const [ debouncedCustomerData ] = useDebounce( customerData, 1000, {
@ -114,7 +129,6 @@ export const useCustomerData = () => {
removeNotice,
updateCustomerData,
] );
return {
billingData: customerData.billingData,
shippingAddress: customerData.shippingAddress,

View File

@ -3,6 +3,10 @@
*/
import isShallowEqual from '@wordpress/is-shallow-equal';
import { pluckAddress } from '@woocommerce/base-utils';
import type {
CartResponseBillingAddress,
CartResponseShippingAddress,
} from '@woocommerce/types';
/**
* Does a shallow compare of important address data to determine if the cart needs updating.
@ -12,7 +16,12 @@ import { pluckAddress } from '@woocommerce/base-utils';
*
* @return {boolean} True if the store needs updating due to changed data.
*/
export const shouldUpdateAddressStore = ( previousAddress, address ) => {
export const shouldUpdateAddressStore = <
T extends CartResponseBillingAddress | CartResponseShippingAddress
>(
previousAddress: T,
address: T
): boolean => {
if ( ! address.country ) {
return false;
}

View File

@ -16,21 +16,15 @@ describe( 'useShallowEqual', () => {
let renderer;
beforeEach( () => ( renderer = null ) );
it.each`
testValueA | aType | testValueB | bType | expectEqual
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'b' } } | ${ 'object' } | ${ true }
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'c' } } | ${ 'object' } | ${ false }
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'b', 'bar' ] } | ${ 'array' } | ${ true }
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'bar', 'b' ] } | ${ 'array' } | ${ false }
${ 1 } | ${ 'number' } | ${ 1 } | ${ 'number' } | ${ true }
${ 1 } | ${ 'number' } | ${ '1' } | ${ 'string' } | ${ false }
${ '1' } | ${ 'string' } | ${ '1' } | ${ 'string' } | ${ true }
${ 1 } | ${ 'number' } | ${ 2 } | ${ 'number' } | ${ false }
${ 1 } | ${ 'number' } | ${ true } | ${ 'bool' } | ${ false }
${ 0 } | ${ 'number' } | ${ false } | ${ 'bool' } | ${ false }
${ true } | ${ 'bool' } | ${ true } | ${ 'bool' } | ${ true }
testValueA | aType | testValueB | bType
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'b' } } | ${ 'object' }
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'b', 'bar' ] } | ${ 'array' }
${ 1 } | ${ 'number' } | ${ 1 } | ${ 'number' }
${ '1' } | ${ 'string' } | ${ '1' } | ${ 'string' }
${ true } | ${ 'bool' } | ${ true } | ${ 'bool' }
`(
'$testValueA ($aType) and $testValueB ($bType) are expected to be equal ($expectEqual)',
( { testValueA, testValueB, expectEqual } ) => {
'$testValueA ($aType) and $testValueB ($bType) are expected to be equal',
( { testValueA, testValueB } ) => {
let testPropValue;
act( () => {
renderer = TestRenderer.create(
@ -44,11 +38,35 @@ describe( 'useShallowEqual', () => {
renderer.update( <TestComponent testValue={ testValueB } /> );
} );
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
if ( expectEqual ) {
expect( testPropValue ).toBe( testValueA );
} else {
expect( testPropValue ).toBe( testValueB );
}
expect( testPropValue ).toBe( testValueA );
}
);
it.each`
testValueA | aType | testValueB | bType
${ { a: 'b', foo: 'bar' } } | ${ 'object' } | ${ { foo: 'bar', a: 'c' } } | ${ 'object' }
${ [ 'b', 'bar' ] } | ${ 'array' } | ${ [ 'bar', 'b' ] } | ${ 'array' }
${ 1 } | ${ 'number' } | ${ '1' } | ${ 'string' }
${ 1 } | ${ 'number' } | ${ 2 } | ${ 'number' }
${ 1 } | ${ 'number' } | ${ true } | ${ 'bool' }
${ 0 } | ${ 'number' } | ${ false } | ${ 'bool' }
`(
'$testValueA ($aType) and $testValueB ($bType) are expected to not be equal',
( { testValueA, testValueB } ) => {
let testPropValue;
act( () => {
renderer = TestRenderer.create(
<TestComponent testValue={ testValueA } />
);
} );
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
expect( testPropValue ).toBe( testValueA );
// do update
act( () => {
renderer.update( <TestComponent testValue={ testValueB } /> );
} );
testPropValue = renderer.root.findByType( 'div' ).props.newValue;
expect( testPropValue ).toBe( testValueB );
}
);
} );

View File

@ -0,0 +1,6 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {},
"include": [ ".", "../../type-defs/**.ts", "../utils/index.js" ],
"exclude": [ "**/test/**" ]
}

View File

@ -29,7 +29,7 @@ import { useResizeObserver } from 'wordpress-compose';
* };
* ```
*/
export const useContainerQueries = () => {
export const useContainerQueries = (): [ React.ReactElement, string ] => {
const [ resizeListener, { width } ] = useResizeObserver();
let className = '';

View File

@ -3,6 +3,9 @@
*/
import { useRef, useEffect } from 'react';
interface Validation< T > {
( value: T, previousValue: T | undefined ): boolean;
}
/**
* Use Previous based on https://usehooks.com/usePrevious/.
*
@ -10,8 +13,11 @@ import { useRef, useEffect } from 'react';
* @param {Function} [validation] Function that needs to validate for the value
* to be updated.
*/
export const usePrevious = ( value, validation ) => {
const ref = useRef();
export function usePrevious< T >(
value: T,
validation?: Validation< T >
): T | undefined {
const ref = useRef< T >();
useEffect( () => {
if (
@ -23,4 +29,4 @@ export const usePrevious = ( value, validation ) => {
}, [ value, validation ] );
return ref.current;
};
}

View File

@ -16,10 +16,10 @@ import isShallowEqual from '@wordpress/is-shallow-equal';
* @return {*} The previous cached instance of the value if the current has
* shallow equality with it.
*/
export const useShallowEqual = ( value ) => {
const ref = useRef();
if ( ! isShallowEqual( value, ref.current ) ) {
export function useShallowEqual< T >( value: T ): T | undefined {
const ref = useRef< T >();
if ( ref.current === undefined || ! isShallowEqual( value, ref.current ) ) {
ref.current = value;
}
return ref.current;
};
}

View File

@ -5,7 +5,7 @@ import { useState, useEffect, useRef } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { decodeEntities } from '@wordpress/html-entities';
import type { CartItem } from '@woocommerce/types';
/**
* Internal dependencies
*/
@ -16,6 +16,12 @@ import { useStoreCart } from './cart';
* @typedef {import('@woocommerce/type-defs/hooks').StoreCartItemAddToCart} StoreCartItemAddToCart
*/
interface StoreAddToCart {
cartQuantity: number;
addingToCart: boolean;
cartIsLoading: boolean;
addToCart: ( quantity: number ) => void;
}
/**
* Get the quantity of a product in the cart.
*
@ -23,7 +29,10 @@ import { useStoreCart } from './cart';
* @param {number} productId The product id to look for.
* @return {number} Quantity in the cart.
*/
const getQuantityFromCartItems = ( cartItems, productId ) => {
const getQuantityFromCartItems = (
cartItems: Array< CartItem >,
productId: number
): number => {
const productItem = cartItems.find( ( { id } ) => id === productId );
return productItem ? productItem.quantity : 0;
@ -39,7 +48,7 @@ const getQuantityFromCartItems = ( cartItems, productId ) => {
* @return {StoreCartItemAddToCart} An object exposing data and actions relating
* to add to cart functionality.
*/
export const useStoreAddToCart = ( productId ) => {
export const useStoreAddToCart = ( productId: number ): StoreAddToCart => {
const { addItemToCart } = useDispatch( storeKey );
const { cartItems, cartIsLoading } = useStoreCart();
const { addErrorNotice, removeNotice } = useStoreNotices();

View File

@ -10,16 +10,11 @@ import { useState, useCallback } from '@wordpress/element';
*
* @return {function(Object)} A function receiving the error that will be thrown.
*/
export const useThrowError = () => {
export const useThrowError = (): ( ( error: Error ) => void ) => {
const [ , setState ] = useState();
const throwError = useCallback(
( error ) =>
setState( () => {
throw error;
} ),
[]
);
return throwError;
return useCallback( ( error: Error ): void => {
setState( () => {
throw error;
} );
}, [] );
};

View File

@ -9,8 +9,8 @@ import { triggerFragmentRefresh } from '@woocommerce/base-utils';
*
* @param {number} quantityInCart Quantity of the item in the cart.
*/
export const useTriggerFragmentRefresh = ( quantityInCart ) => {
const firstMount = useRef( true );
export const useTriggerFragmentRefresh = ( quantityInCart: number ): void => {
const firstMount = useRef< boolean >( true );
useEffect( () => {
if ( firstMount.current ) {

View File

@ -1,3 +1,8 @@
/**
* External dependencies
*/
import { fromEntriesPolyfill } from '@woocommerce/base-utils';
/**
* Get an array of selected shipping rates keyed by Package ID.
*
@ -5,15 +10,11 @@
* @return {Object} Object containing the package IDs and selected rates in the format: { [packageId:string]: rateId:string }
*/
export const deriveSelectedShippingRates = ( shippingRates ) =>
shippingRates
.map( ( { package_id: packageId, shipping_rates: packageRates } ) => [
packageId,
packageRates.find( ( rate ) => rate.selected )?.rate_id,
] )
// A fromEntries polyfill, creates an object from an array of arrays.
.reduce( ( obj, [ key, val ] ) => {
if ( val ) {
obj[ key ] = val;
}
return obj;
}, {} );
fromEntriesPolyfill(
shippingRates.map(
( { package_id: packageId, shipping_rates: packageRates } ) => [
packageId,
packageRates.find( ( rate ) => rate.selected )?.rate_id,
]
)
);

View File

@ -0,0 +1,15 @@
/**
* A polyfill for Object.fromEntries function.
*
* @param {Array<[string, unknown]>} array Array to be turned back to object
* @return {Record< string, unknown >} the newly created object
*/
export const fromEntriesPolyfill = (
array: Array< [ string, unknown ] >
): Record< string, unknown > =>
array.reduce< Record< string, unknown > >( ( obj, [ key, val ] ) => {
if ( val ) {
obj[ key ] = val;
}
return obj;
}, {} );

View File

@ -7,3 +7,4 @@ export * from './get-intersection-observer';
export * from './get-valid-block-attributes';
export * from './product-data';
export * from './derive-selected-shipping-rates';
export * from './from-entries-polyfill';

View File

@ -18,6 +18,7 @@ import { ACTION_TYPES as types } from './action-types';
import { STORE_KEY as CART_STORE_KEY } from './constants';
import { apiFetchWithHeaders } from '../shared-controls';
import type { ResponseError } from '../types';
import { ReturnOrGeneratorYieldUnion } from '../../mapped-types';
/**
* Returns an action object used in updating the store with the provided items
@ -424,7 +425,7 @@ export function* updateCustomerData(
return true;
}
export type CartAction = ReturnType<
export type CartAction = ReturnOrGeneratorYieldUnion<
| typeof receiveCart
| typeof receiveError
| typeof receiveApplyingCoupon
@ -434,4 +435,8 @@ export type CartAction = ReturnType<
| typeof itemIsPendingDelete
| typeof updatingCustomerData
| typeof shippingRatesBeingSelected
| typeof updateCustomerData
| typeof removeItemFromCart
| typeof changeCartItemQuantity
| typeof addItemToCart
>;

View File

@ -3,7 +3,7 @@
*/
import { registerStore } from '@wordpress/data';
import { controls as dataControls } from '@wordpress/data-controls';
import type { DispatchFromMap, SelectFromMap } from '@automattic/data-stores';
import type { SelectFromMap } from '@automattic/data-stores';
/**
* Internal dependencies
@ -14,6 +14,7 @@ import * as actions from './actions';
import * as resolvers from './resolvers';
import reducer, { State } from './reducers';
import { controls } from '../shared-controls';
import { DispatchFromMap } from '../../mapped-types';
registerStore< State >( STORE_KEY, {
reducer,

View File

@ -1,6 +1,6 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {},
"include": [ ".", "../type-defs/**.ts" ],
"include": [ ".", "../type-defs/**.ts", "../mapped-types.ts" ],
"exclude": [ "**/test/**" ]
}

View File

@ -0,0 +1,40 @@
/**
* Usually we use ReturnType of all the action creators to deduce all the actions.
* This works until one of the action creators is a generator and doesn't actually "Return" an action.
* This type helper allows for actions to be both functions and generators
*/
export type ReturnOrGeneratorYieldUnion<
T extends ( ...args: any ) => any
> = T extends ( ...args: any ) => infer Return
? Return extends Generator< infer T, infer U, any >
? T | U
: Return
: never;
/**
* Maps a "raw" actionCreators object to the actions available when registered on the @wordpress/data store.
*
* @template A Selector map, usually from `import * as actions from './my-store/actions';`
*/
// Todo: This is a temporary implementation of what exists in @automattic/data-stores
// already: https://github.com/Automattic/wp-calypso/blob/0ba7f5a91571b3ba6a84be7db021c7eab247e434/packages/data-stores/src/mapped-types.ts
// We will need to remove this when @automattic/data-stores is updated on NPM.
export type DispatchFromMap<
A extends Record< string, ( ...args: any[] ) => any >
> = {
[ actionCreator in keyof A ]: (
...args: Parameters< A[ actionCreator ] >
) => A[ actionCreator ] extends ( ...args: any[] ) => Generator
? Promise< GeneratorReturnType< A[ actionCreator ] > >
: void;
};
/**
* Obtain the type finally returned by the generator when it's done iterating.
*/
// Todo: This is a temporary implementation of what exists in @automattic/data-stores
// already: https://github.com/Automattic/wp-calypso/blob/0ba7f5a91571b3ba6a84be7db021c7eab247e434/packages/data-stores/src/mapped-types.ts
// We will need to remove this when @automattic/data-stores is updated on NPM.
export type GeneratorReturnType<
T extends ( ...args: any[] ) => Generator
> = T extends ( ...args: any ) => Generator< any, infer R, any > ? R : never;

View File

@ -7,6 +7,7 @@ import {
WC_BLOCKS_ASSET_URL,
SHIPPING_ENABLED,
} from '@woocommerce/block-settings';
import { CartResponse } from '@woocommerce/types';
/**
* Internal dependencies
@ -16,7 +17,7 @@ import { previewShippingRates } from './shipping-rates';
// Sample data for cart block.
// This closely resembles the data returned from the Store API /cart endpoint.
// https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/trunk/src/RestApi/StoreApi#cart-api
export const previewCart = {
export const previewCart: CartResponse = {
coupons: [],
shipping_rates: SHIPPING_METHODS_EXIST ? previewShippingRates : [],
items: [
@ -162,6 +163,30 @@ export const previewCart = {
needs_payment: true,
needs_shipping: SHIPPING_ENABLED,
has_calculated_shipping: true,
shipping_address: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
},
billing_address: {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
email: '',
phone: '',
},
totals: {
currency_code: 'USD',
currency_symbol: '$',

View File

@ -2,10 +2,18 @@
* External dependencies
*/
import { __, _x } from '@wordpress/i18n';
import type { CartResponseShippingRateItem } from '@woocommerce/types';
export const previewShippingRates = [
export const previewShippingRates: CartResponseShippingRateItem[] = [
{
destination: {},
destination: {
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
},
package_id: 0,
name: __( 'Shipping', 'woo-gutenberg-products-block' ),
items: [
@ -41,7 +49,10 @@ export const previewShippingRates = [
description: '',
delivery_time: '',
price: '000',
taxes: '0',
rate_id: 'free_shipping:1',
instance_id: 0,
meta_data: [],
method_id: 'flat_rate',
selected: true,
},
@ -57,7 +68,10 @@ export const previewShippingRates = [
description: '',
delivery_time: '',
price: '200',
taxes: '0',
rate_id: 'local_pickup:1',
instance_id: 1,
meta_data: [],
method_id: 'local_pickup',
selected: false,
},

View File

@ -145,11 +145,11 @@ export interface CartResponseFeeItemTotals extends CurrencyResponseInfo {
total_tax: string;
}
export interface CartResponseFeeItem {
export type CartResponseFeeItem = {
id: string;
name: string;
totals: CartResponseFeeItemTotals;
}
};
export interface CartResponseTotals extends CurrencyResponseInfo {
total_items: string;
@ -186,7 +186,7 @@ export interface CartResponse {
needs_shipping: boolean;
has_calculated_shipping: boolean;
fees: Array< CartResponseFeeItem >;
totals: CartResponseTotalsItem;
totals: CartResponseTotals;
errors: Array< CartResponseErrorItem >;
payment_requirements: Array< unknown >;
extensions: Array< CartResponseExtensionItem >;

View File

@ -0,0 +1,53 @@
/**
* Internal dependencies
*/
import type {
CartResponseErrorItem,
CartResponseCouponItem,
CartResponseItem,
CartResponseFeeItem,
CartResponseTotals,
CartResponseShippingAddress,
CartResponseBillingAddress,
CartResponseShippingRateItem,
CartResponse,
} from './cart-response';
import type { ResponseError } from '../data/types';
export interface StoreCartItemQuantity {
isPendingDelete: boolean;
quantity: number;
changeQuantity: ( quantity: number ) => void;
removeItem: () => Promise< void > | false;
cartItemQuantityErrors: Array< CartResponseErrorItem >;
}
export interface StoreCartCoupon {
appliedCoupons: Array< CartResponseCouponItem >;
isLoading: boolean;
applyCoupon: ( coupon: string ) => void;
removeCoupon: ( coupon: string ) => void;
isApplyingCoupon: boolean;
isRemovingCoupon: boolean;
}
export interface StoreCart {
cartCoupons: Array< CartResponseCouponItem >;
cartItems: Array< CartResponseItem >;
cartFees: Array< CartResponseFeeItem >;
cartItemsCount: number;
cartItemsWeight: number;
cartNeedsPayment: boolean;
cartNeedsShipping: boolean;
cartItemErrors: Array< CartResponseErrorItem >;
cartTotals: CartResponseTotals;
cartIsLoading: boolean;
cartErrors: Array< ResponseError >;
billingAddress: CartResponseBillingAddress;
shippingAddress: CartResponseShippingAddress;
shippingRates: Array< CartResponseShippingRateItem >;
extensions: Record< string, unknown >;
shippingRatesLoading: boolean;
cartHasCalculatedShipping: boolean;
paymentRequirements: Array< string >;
receiveCart: ( cart: CartResponse ) => void;
}

View File

@ -1,2 +1,3 @@
export * from './cart-response';
export * from './cart';
export * from './hooks';