Try move shipping related dat to a `@wordpress/data` store (https://github.com/woocommerce/woocommerce-blocks/pull/5896)

* Add address-related items to wc/store/cart data store

* Add useUpdateCustomerData hook

This allows us to have a single hook responsible for updating the customer information on the server.

* Add useUpdateCustomerData hook in Checkout block

* Remove shippingAsBilling from previousCustomerData ref type

* Add useShippingAsBillingCheckbox hook

* Remove checkbox handling from useCheckoutAddress

* Merge with woocommerce/woocommerce-blocks#5810 changes

* Move shipping as billing to checkout state context provider

* Subscribe to changes

* Cache customerDataToUpdate

* Combine customerDataType and customerDataContextType

* Fix notice context

* Clean up inline docs for push changes

* Add useShippingData hook

* Add shipping related selectors to cart store

* Update useShippingDataContext to useCustomerData hook

* Update uses of useShippingDataContext to get data from hook instead

* Remove rogue linebreak

* Re-add linebreak

* Re-add linebreak, remove shippingAsBilling

* Re-add linebreak

* Use useShippingData and useCustomerData instead of context

* Fix fromEntriesPolyfill to use number or undefined as an index option

* Convert derive-selected-shipping-rates to TS

* Add SelectShippingRateType

* Get needsShipping from new hook and not context

* Get address data from useCustomerData instead of useShippingDataContext

* Move selectedRates, selectShippingRate and isSelectingRate

* Remove items from ShippingDatacontext that are available in data stores

* Get shipping data from stores, not context in payment method interface

* Consider shipping rates to be loading if customer data is updating

* Get rates from useShippingData hook instead of context

* Fix incorrect TypeScript types and incorrectly named destructure

* Move useShippingData into shipping folder

* Update tests to mock useShippingData instead of context

* Remove empty string fallback from shipping phone

* Get types from Cart declaration instead of Picking them

Co-authored-by: Mike Jolley <mike.jolley@me.com>
This commit is contained in:
Thomas Roberts 2022-03-04 17:43:45 +00:00 committed by GitHub
parent 719c7f7c23
commit 6b8ef2773a
17 changed files with 180 additions and 107 deletions

View File

@ -1,8 +1,8 @@
/**
* External dependencies
*/
import { useShippingDataContext } from '@woocommerce/base-context';
import type { EnteredAddress } from '@woocommerce/settings';
import { useCustomerData } from '@woocommerce/base-context/hooks';
/**
* Internal dependencies
@ -21,7 +21,7 @@ const ShippingCalculator = ( {
},
addressFields = [ 'country', 'state', 'city', 'postcode' ],
}: ShippingCalculatorProps ): JSX.Element => {
const { shippingAddress, setShippingAddress } = useShippingDataContext();
const { shippingAddress, setShippingAddress } = useCustomerData();
return (
<div className="wc-block-components-shipping-calculator">
<ShippingCalculatorAddress

View File

@ -23,6 +23,7 @@ import { usePaymentMethodDataContext } from '../../providers/cart-checkout/payme
import { useShippingDataContext } from '../../providers/cart-checkout/shipping';
import { useCustomerDataContext } from '../../providers/cart-checkout/customer';
import { prepareTotalItems } from './utils';
import { useShippingData } from '../shipping/use-shipping-data';
/**
* Returns am interface to use as payment method props.
@ -50,17 +51,19 @@ export const usePaymentMethodInterface = (): PaymentMethodInterface => {
const {
shippingErrorStatus,
shippingErrorTypes,
shippingRates,
shippingRatesLoading,
selectedRates,
setSelectedRates,
isSelectingRate,
onShippingRateSuccess,
onShippingRateFail,
onShippingRateSelectSuccess,
onShippingRateSelectFail,
needsShipping,
} = useShippingDataContext();
const {
shippingRates,
shippingRatesLoading,
selectedRates,
isSelectingRate,
selectShippingRate,
needsShipping,
} = useShippingData();
const {
billingData,
shippingAddress,
@ -157,7 +160,7 @@ export const usePaymentMethodInterface = (): PaymentMethodInterface => {
isSelectingRate,
needsShipping,
selectedRates,
setSelectedRates,
selectShippingRate,
setShippingAddress,
shippingAddress,
shippingRates,

View File

@ -1 +1,2 @@
export * from './use-select-shipping-rate';
export * from './use-shipping-data';

View File

@ -5,6 +5,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useThrowError } from '@woocommerce/base-hooks';
import { SelectShippingRateType } from '@woocommerce/type-defs/shipping';
/**
* Internal dependencies
@ -18,15 +19,7 @@ import { useStoreEvents } from '../use-store-events';
* - selectShippingRate: A function that immediately returns the selected rate and dispatches an action generator.
* - isSelectingRate: True when rates are being resolved to the API.
*/
export const useSelectShippingRate = (): {
// Returns a function that accepts a shipping rate ID and a package ID.
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
) => unknown;
// True when a rate is currently being selected and persisted to the server.
isSelectingRate: boolean;
} => {
export const useSelectShippingRate = (): SelectShippingRateType => {
const throwError = useThrowError();
const { dispatchCheckoutEvent } = useStoreEvents();

View File

@ -0,0 +1,66 @@
/**
* External dependencies
*/
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { useSelect } from '@wordpress/data';
import { Cart } from '@woocommerce/type-defs/cart';
import { SelectShippingRateType } from '@woocommerce/type-defs/shipping';
import { useEffect, useRef } from '@wordpress/element';
import { deriveSelectedShippingRates } from '@woocommerce/base-utils';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { isObject } from '@woocommerce/types';
/**
* Internal dependencies
*/
import { useSelectShippingRate } from './use-select-shipping-rate';
interface ShippingData extends SelectShippingRateType {
needsShipping: Cart[ 'needsShipping' ];
hasCalculatedShipping: Cart[ 'hasCalculatedShipping' ];
shippingRates: Cart[ 'shippingRates' ];
shippingRatesLoading: boolean;
selectedRates: Record< string, string | unknown >;
}
export const useShippingData = (): ShippingData => {
const {
shippingRates,
needsShipping,
hasCalculatedShipping,
shippingRatesLoading,
} = useSelect( ( select ) => {
const store = select( storeKey );
return {
shippingRates: store.getShippingRates(),
needsShipping: store.getNeedsShipping(),
hasCalculatedShipping: store.getHasCalculatedShipping(),
shippingRatesLoading: store.isCustomerDataUpdating(),
};
} );
const { isSelectingRate, selectShippingRate } = useSelectShippingRate();
// set selected rates on ref so it's always current.
const selectedRates = useRef< Record< string, unknown > >( {} );
useEffect( () => {
const derivedSelectedRates = deriveSelectedShippingRates(
shippingRates
);
if (
isObject( derivedSelectedRates ) &&
! isShallowEqual( selectedRates.current, derivedSelectedRates )
) {
selectedRates.current = derivedSelectedRates;
}
}, [ shippingRates ] );
return {
isSelectingRate,
selectedRates: selectedRates.current,
selectShippingRate,
shippingRates,
needsShipping,
hasCalculatedShipping,
shippingRatesLoading,
};
};

View File

@ -13,11 +13,9 @@ import { useCallback } from '@wordpress/element';
/**
* Internal dependencies
*/
import {
useShippingDataContext,
useCheckoutContext,
} from '../providers/cart-checkout';
import { useCheckoutContext } from '../providers/cart-checkout';
import { useCustomerData } from './use-customer-data';
import { useShippingData } from './shipping/use-shipping-data';
interface CheckoutAddress {
shippingAddress: ShippingAddress;
@ -38,7 +36,7 @@ interface CheckoutAddress {
* Custom hook for exposing address related functionality for the checkout address form.
*/
export const useCheckoutAddress = (): CheckoutAddress => {
const { needsShipping } = useShippingDataContext();
const { needsShipping } = useShippingData();
const {
useShippingAsBilling,
setUseShippingAsBilling,

View File

@ -8,12 +8,11 @@ import { useCallback, useMemo } from '@wordpress/element';
*/
import { actions, ActionType } from './actions';
import { STATUS } from './constants';
import { useCustomerDataContext } from '../customer';
import { useShippingDataContext } from '../shipping';
import type {
PaymentStatusDispatchers,
PaymentMethodDispatchers,
} from './types';
import { useCustomerData } from '../../../hooks/use-customer-data';
export const usePaymentMethodDataDispatchers = (
dispatch: React.Dispatch< ActionType >
@ -21,8 +20,7 @@ export const usePaymentMethodDataDispatchers = (
dispatchActions: PaymentMethodDispatchers;
setPaymentStatus: () => PaymentStatusDispatchers;
} => {
const { setBillingData } = useCustomerDataContext();
const { setShippingAddress } = useShippingDataContext();
const { setBillingData, setShippingAddress } = useCustomerData();
const dispatchActions = useMemo(
(): PaymentMethodDispatchers => ( {

View File

@ -21,12 +21,12 @@ import { useDebouncedCallback } from 'use-debounce';
* Internal dependencies
*/
import { useEditorContext } from '../../editor-context';
import { useShippingDataContext } from '../shipping';
import { useCustomerDataContext } from '../customer';
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
import { useStoreNotices } from '../../../hooks/use-store-notices';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import type { PaymentMethodsDispatcherType } from './types';
import { useShippingData } from '../../../hooks/shipping/use-shipping-data';
/**
* This hook handles initializing registered payment methods and exposing all
@ -48,7 +48,7 @@ const usePaymentMethodRegistration = (
) => {
const [ isInitialized, setIsInitialized ] = useState( false );
const { isEditor } = useEditorContext();
const { selectedRates } = useShippingDataContext();
const { selectedRates } = useShippingData();
const { billingData, shippingAddress } = useCustomerDataContext();
const selectedShippingMethods = useShallowEqual( selectedRates );
const paymentMethodsOrder = useShallowEqual( paymentMethodsSortOrder );

View File

@ -9,8 +9,6 @@ import {
useMemo,
useRef,
} from '@wordpress/element';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { deriveSelectedShippingRates } from '@woocommerce/base-utils';
/**
* Internal dependencies
@ -25,9 +23,9 @@ import {
emitEvent,
} from './event-emit';
import { useCheckoutContext } from '../checkout-state';
import { useCustomerDataContext } from '../customer';
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
import { useSelectShippingRate } from '../../../hooks/shipping/use-select-shipping-rate';
import { useShippingData } from '../../../hooks/shipping/use-shipping-data';
/**
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataContext} ShippingDataContext
@ -52,15 +50,9 @@ export const useShippingDataContext = () => {
*/
export const ShippingDataProvider = ( { children } ) => {
const { dispatchActions } = useCheckoutContext();
const { shippingAddress, setShippingAddress } = useCustomerDataContext();
const {
cartNeedsShipping: needsShipping,
cartHasCalculatedShipping: hasCalculatedShipping,
shippingRates,
shippingRatesLoading,
cartErrors,
} = useStoreCart();
const { selectShippingRate, isSelectingRate } = useSelectShippingRate();
const { shippingRates, shippingRatesLoading, cartErrors } = useStoreCart();
const { isSelectingRate } = useSelectShippingRate();
const { selectedRates } = useShippingData();
const [ shippingErrorStatus, dispatchErrorStatus ] = useReducer(
errorStatusReducer,
NONE
@ -85,19 +77,6 @@ export const ShippingDataProvider = ( { children } ) => {
currentObservers.current = observers;
}, [ observers ] );
// set selected rates on ref so it's always current.
const selectedRates = useRef( () =>
deriveSelectedShippingRates( shippingRates )
);
useEffect( () => {
const derivedSelectedRates = deriveSelectedShippingRates(
shippingRates
);
if ( ! isShallowEqual( selectedRates.current, derivedSelectedRates ) ) {
selectedRates.current = derivedSelectedRates;
}
}, [ shippingRates ] );
// increment/decrement checkout calculating counts when shipping is loading.
useEffect( () => {
if ( shippingRatesLoading ) {
@ -198,6 +177,7 @@ export const ShippingDataProvider = ( { children } ) => {
);
}
}, [
selectedRates,
isSelectingRate,
currentErrorStatus.hasError,
currentErrorStatus.hasInvalidAddress,
@ -210,15 +190,6 @@ export const ShippingDataProvider = ( { children } ) => {
shippingErrorStatus: currentErrorStatus,
dispatchErrorStatus,
shippingErrorTypes: ERROR_TYPES,
shippingRates,
shippingRatesLoading,
selectedRates: selectedRates.current,
setSelectedRates: selectShippingRate,
isSelectingRate,
shippingAddress,
setShippingAddress,
needsShipping,
hasCalculatedShipping,
...eventObservers,
};

View File

@ -1,10 +1,17 @@
/**
* External dependencies
*/
import { CartShippingRate } from '@woocommerce/type-defs/cart';
/**
* Get an array of selected shipping rates keyed by Package ID.
*
* @param {Array} shippingRates Array of shipping rates.
* @return {Object} Object containing the package IDs and selected rates in the format: { [packageId:string]: rateId:string }
*/
export const deriveSelectedShippingRates = ( shippingRates ) =>
export const deriveSelectedShippingRates = (
shippingRates: CartShippingRate[]
): Record< string, string | unknown > =>
Object.fromEntries(
shippingRates.map(
( { package_id: packageId, shipping_rates: packageRates } ) => [

View File

@ -4,10 +4,8 @@
import classnames from 'classnames';
import { __ } from '@wordpress/i18n';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutContext,
useShippingDataContext,
} from '@woocommerce/base-context';
import { useCheckoutContext } from '@woocommerce/base-context';
import { useShippingData } from '@woocommerce/base-context/hooks';
/**
* Internal dependencies
@ -15,7 +13,7 @@ import {
import CheckoutOrderNotes from '../../order-notes';
const Block = ( { className }: { className?: string } ): JSX.Element => {
const { needsShipping } = useShippingDataContext();
const { needsShipping } = useShippingData();
const {
isProcessing: checkoutIsProcessing,
orderNotes,

View File

@ -18,17 +18,13 @@ import {
} from '@woocommerce/blocks-checkout';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import { useShippingDataContext } from '@woocommerce/base-context';
import {
useStoreCartCoupons,
useStoreCart,
useShippingData,
} from '@woocommerce/base-context/hooks';
import { getSetting } from '@woocommerce/settings';
/**
* Internal dependencies
*/
const Block = ( {
showRateAfterTaxName = false,
className,
@ -44,7 +40,7 @@ const Block = ( {
isRemovingCoupon,
} = useStoreCartCoupons();
const { needsShipping } = useShippingDataContext();
const { needsShipping } = useShippingData();
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
// Prepare props to pass to the ExperimentalOrderMeta slot fill.

View File

@ -18,7 +18,6 @@ import {
textContentMatcherAcrossSiblings,
} from '../../../../../../../tests/utils/find-by-text';
const baseContextHooks = jest.requireMock( '@woocommerce/base-context/hooks' );
const baseContext = jest.requireMock( '@woocommerce/base-context' );
const woocommerceSettings = jest.requireMock( '@woocommerce/settings' );
const defaultUseStoreCartValue = {
@ -51,15 +50,7 @@ jest.mock( '@woocommerce/base-context/hooks', () => ( {
billingAddress: mockPreviewCart.billing_address,
cartHasCalculatedShipping: mockPreviewCart.has_calculated_shipping,
} ),
} ) );
jest.mock( '@woocommerce/base-context', () => ( {
...jest.requireActual( '@woocommerce/base-context' ),
useContainerWidthContext: jest.fn().mockReturnValue( {
hasContainerWidth: true,
isLarge: true,
} ),
useShippingDataContext: jest.fn().mockReturnValue( {
useShippingData: jest.fn().mockReturnValue( {
needsShipping: true,
shippingRates: [
{
@ -167,6 +158,14 @@ jest.mock( '@woocommerce/base-context', () => ( {
} ),
} ) );
jest.mock( '@woocommerce/base-context', () => ( {
...jest.requireActual( '@woocommerce/base-context' ),
useContainerWidthContext: jest.fn().mockReturnValue( {
hasContainerWidth: true,
isLarge: true,
} ),
} ) );
jest.mock( '@woocommerce/settings', () => {
const originalModule = jest.requireActual( '@woocommerce/settings' );
@ -189,12 +188,14 @@ const setGetSettingImplementation = ( implementation ) => {
woocommerceSettings.getSetting.mockImplementation( implementation );
};
const setUseShippingDataContextReturnValue = ( value ) => {
baseContext.useShippingDataContext.mockReturnValue( value );
const setUseShippingDataReturnValue = ( value ) => {
baseContextHooks.useShippingData.mockReturnValue( value );
};
describe( 'Checkout Order Summary', () => {
beforeEach( () => setUseStoreCartReturnValue() );
beforeEach( () => {
setUseStoreCartReturnValue();
} );
it( 'Renders the standard preview items in the sidebar', async () => {
const { container } = render( <Block showRateAfterTaxName={ true } /> );
@ -335,7 +336,7 @@ describe( 'Checkout Order Summary', () => {
...defaultUseStoreCartValue,
needsShipping: false,
} );
setUseShippingDataContextReturnValue( { needsShipping: false } );
setUseShippingDataReturnValue( { needsShipping: false } );
const { container } = render( <Block showRateAfterTaxName={ true } /> );
expect( queryByText( container, 'Shipping' ) ).not.toBeInTheDocument();
} );
@ -349,7 +350,6 @@ describe( 'Checkout Order Summary', () => {
tax_lines: [ { name: 'Tax', price: '1000', rate: '5%' } ],
},
} );
setUseShippingDataContextReturnValue( { needsShipping: false } );
setGetSettingImplementation( ( setting, ...rest ) => {
if ( setting === 'displayCartPricesIncludingTax' ) {
return true;
@ -378,7 +378,7 @@ describe( 'Checkout Order Summary', () => {
tax_lines: [ { name: 'Tax', price: '1000', rate: '5%' } ],
},
} );
setUseShippingDataContextReturnValue( { needsShipping: false } );
setUseShippingDataReturnValue( { needsShipping: false } );
setGetSettingImplementation( ( setting, ...rest ) => {
if ( setting === 'displayCartPricesIncludingTax' ) {
return false;
@ -407,7 +407,7 @@ describe( 'Checkout Order Summary', () => {
...mockPreviewCart.totals,
},
} );
setUseShippingDataContextReturnValue( { needsShipping: false } );
setUseShippingDataReturnValue( { needsShipping: false } );
const { container } = render( <Block showRateAfterTaxName={ true } /> );
expect(
await findByText(
@ -425,7 +425,7 @@ describe( 'Checkout Order Summary', () => {
total_shipping: '4000',
},
} );
setUseShippingDataContextReturnValue( {
setUseShippingDataReturnValue( {
needsShipping: true,
shippingRates: [
{

View File

@ -2,14 +2,12 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { useShippingData } from '@woocommerce/base-context/hooks';
import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout';
import { getShippingRatesPackageCount } from '@woocommerce/base-utils';
import { getCurrencyFromPriceResponse } from '@woocommerce/price-format';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import {
useEditorContext,
useShippingDataContext,
} from '@woocommerce/base-context';
import { useEditorContext } from '@woocommerce/base-context';
import { decodeEntities } from '@wordpress/html-entities';
import { Notice } from 'wordpress-components';
import classnames from 'classnames';
@ -50,12 +48,13 @@ const renderShippingRatesControlOption = (
const Block = (): JSX.Element | null => {
const { isEditor } = useEditorContext();
const {
shippingRates,
shippingRatesLoading,
needsShipping,
shippingRatesLoading,
hasCalculatedShipping,
} = useShippingDataContext();
} = useShippingData();
if ( ! needsShipping ) {
return null;

View File

@ -1,7 +1,13 @@
/**
* External dependencies
*/
import type { Cart, CartTotals, CartMeta, CartItem } from '@woocommerce/types';
import type {
Cart,
CartTotals,
CartMeta,
CartItem,
CartShippingRate,
} from '@woocommerce/types';
import { BillingAddress, ShippingAddress } from '@woocommerce/settings';
/**
@ -32,6 +38,36 @@ export const getCustomerData = (
};
};
/**
* Retrieves shipping rates from state.
*
* @param { CartState } state The current state.
* @return { CartShippingRate[] } The shipping rates on the cart.
*/
export const getShippingRates = ( state: CartState ): CartShippingRate[] => {
return state.cartData.shippingRates;
};
/**
* Retrieves whether the cart needs shipping.
*
* @param { CartState } state The current state.
* @return { boolean } True if the cart needs shipping.
*/
export const getNeedsShipping = ( state: CartState ): boolean => {
return state.cartData.needsShipping;
};
/**
* Retrieves whether the cart shipping has been calculated.
*
* @param { CartState } state The current state.
* @return { boolean } True if the shipping has been calculated.
*/
export const getHasCalculatedShipping = ( state: CartState ): boolean => {
return state.cartData.hasCalculatedShipping;
};
/**
* Retrieves cart totals from state.
*

View File

@ -10,10 +10,7 @@ import type LoadingMask from '@woocommerce/base-components/loading-mask';
* Internal dependencies
*/
import type { Currency } from './currency';
import type {
CartBillingAddress,
CartShippingPackageShippingRate,
} from './cart';
import type { CartBillingAddress, CartShippingRate } from './cart';
import type {
responseTypes,
noticeContexts,
@ -114,8 +111,8 @@ export interface ShippingDataProps {
isSelectingRate: boolean;
// True if cart requires shipping.
needsShipping: boolean;
// An array of selected rates (rate ids).
selectedRates: string[];
// An object containing package IDs as the key and selected rate as the value (rate ids).
selectedRates: Record< string, unknown >;
// A function for setting selected rates (receives id).
setSelectedRates: (
newShippingRateId: string,
@ -126,7 +123,7 @@ export interface ShippingDataProps {
// The current set shipping address.
shippingAddress: CartResponseShippingAddress;
// All the available shipping rates.
shippingRates: CartShippingPackageShippingRate[];
shippingRates: CartShippingRate[];
// Whether the rates are loading or not.
shippingRatesLoading: boolean;
}

View File

@ -11,3 +11,13 @@ export interface PackageRateOption {
secondaryDescription?: string;
id?: string;
}
export interface SelectShippingRateType {
// Returns a function that accepts a shipping rate ID and a package ID.
selectShippingRate: (
newShippingRateId: string,
packageId: string | number
) => unknown;
// True when a rate is currently being selected and persisted to the server.
isSelectingRate: boolean;
}