initial commit
This commit is contained in:
@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingErrorTypes} ShippingErrorTypes
|
||||
* @typedef {import('@woocommerce/type-defs/shipping').ShippingAddress} ShippingAddress
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataContext} ShippingDataContext
|
||||
*/
|
||||
|
||||
/**
|
||||
* @type {ShippingErrorTypes}
|
||||
*/
|
||||
export const ERROR_TYPES = {
|
||||
NONE: 'none',
|
||||
INVALID_ADDRESS: 'invalid_address',
|
||||
UNKNOWN: 'unknown_error',
|
||||
};
|
||||
|
||||
export const shippingErrorCodes = {
|
||||
INVALID_COUNTRY: 'woocommerce_rest_cart_shipping_rates_invalid_country',
|
||||
MISSING_COUNTRY: 'woocommerce_rest_cart_shipping_rates_missing_country',
|
||||
INVALID_STATE: 'woocommerce_rest_cart_shipping_rates_invalid_state',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {ShippingAddress}
|
||||
*/
|
||||
export const DEFAULT_SHIPPING_ADDRESS = {
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
company: '',
|
||||
address_1: '',
|
||||
address_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
postcode: '',
|
||||
country: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {ShippingDataContext}
|
||||
*/
|
||||
export const DEFAULT_SHIPPING_CONTEXT_DATA = {
|
||||
shippingErrorStatus: {
|
||||
isPristine: true,
|
||||
isValid: false,
|
||||
hasInvalidAddress: false,
|
||||
hasError: false,
|
||||
},
|
||||
dispatchErrorStatus: () => null,
|
||||
shippingErrorTypes: ERROR_TYPES,
|
||||
shippingRates: [],
|
||||
shippingRatesLoading: false,
|
||||
selectedRates: [],
|
||||
setSelectedRates: () => null,
|
||||
shippingAddress: DEFAULT_SHIPPING_ADDRESS,
|
||||
setShippingAddress: () => null,
|
||||
onShippingRateSuccess: () => null,
|
||||
onShippingRateFail: () => null,
|
||||
onShippingRateSelectSuccess: () => null,
|
||||
onShippingRateSelectFail: () => null,
|
||||
needsShipping: false,
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { emitterCallback, reducer, emitEvent } from '../../../event-emit';
|
||||
|
||||
const EMIT_TYPES = {
|
||||
SHIPPING_RATES_SUCCESS: 'shipping_rates_success',
|
||||
SHIPPING_RATES_FAIL: 'shipping_rates_fail',
|
||||
SHIPPING_RATE_SELECT_SUCCESS: 'shipping_rate_select_success',
|
||||
SHIPPING_RATE_SELECT_FAIL: 'shipping_rate_select_fail',
|
||||
};
|
||||
|
||||
/**
|
||||
* Receives a reducer dispatcher and returns an object with the onSuccess and
|
||||
* onFail callback registration points for the shipping option emit events.
|
||||
*
|
||||
* Calling the event registration function with the callback will register it
|
||||
* for the event emitter and will return a dispatcher for removing the
|
||||
* registered callback (useful for implementation in `useEffect`).
|
||||
*
|
||||
* @param {Function} dispatcher A reducer dispatcher
|
||||
* @return {Object} An object with `onSuccess` and `onFail` emitter registration.
|
||||
*/
|
||||
const emitterObservers = ( dispatcher ) => ( {
|
||||
onSuccess: emitterCallback( EMIT_TYPES.SHIPPING_RATES_SUCCESS, dispatcher ),
|
||||
onFail: emitterCallback( EMIT_TYPES.SHIPPING_RATES_FAIL, dispatcher ),
|
||||
onSelectSuccess: emitterCallback(
|
||||
EMIT_TYPES.SHIPPING_RATE_SELECT_SUCCESS,
|
||||
dispatcher
|
||||
),
|
||||
onSelectFail: emitterCallback(
|
||||
EMIT_TYPES.SHIPPING_RATE_SELECT_FAIL,
|
||||
dispatcher
|
||||
),
|
||||
} );
|
||||
|
||||
export { EMIT_TYPES, emitterObservers, reducer, emitEvent };
|
@ -0,0 +1,232 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from '@wordpress/element';
|
||||
import isShallowEqual from '@wordpress/is-shallow-equal';
|
||||
import { deriveSelectedShippingRates } from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ERROR_TYPES, DEFAULT_SHIPPING_CONTEXT_DATA } from './constants';
|
||||
import { hasInvalidShippingAddress } from './utils';
|
||||
import { errorStatusReducer } from './reducers';
|
||||
import {
|
||||
EMIT_TYPES,
|
||||
emitterObservers,
|
||||
reducer as emitReducer,
|
||||
emitEvent,
|
||||
} from './event-emit';
|
||||
import { useCheckoutContext } from '../checkout-state';
|
||||
import { useCustomerDataContext } from '../customer';
|
||||
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
|
||||
import { useSelectShippingRates } from '../../../hooks/shipping/use-select-shipping-rates';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataContext} ShippingDataContext
|
||||
* @typedef {import('react')} React
|
||||
*/
|
||||
|
||||
const { NONE, INVALID_ADDRESS, UNKNOWN } = ERROR_TYPES;
|
||||
const ShippingDataContext = createContext( DEFAULT_SHIPPING_CONTEXT_DATA );
|
||||
|
||||
/**
|
||||
* @return {ShippingDataContext} Returns data and functions related to shipping methods.
|
||||
*/
|
||||
export const useShippingDataContext = () => {
|
||||
return useContext( ShippingDataContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* The shipping data provider exposes the interface for shipping in the checkout/cart.
|
||||
*
|
||||
* @param {Object} props Incoming props for provider
|
||||
* @param {React.ReactElement} props.children
|
||||
*/
|
||||
export const ShippingDataProvider = ( { children } ) => {
|
||||
const { dispatchActions } = useCheckoutContext();
|
||||
const { shippingAddress, setShippingAddress } = useCustomerDataContext();
|
||||
const {
|
||||
cartNeedsShipping: needsShipping,
|
||||
cartHasCalculatedShipping: hasCalculatedShipping,
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
cartErrors,
|
||||
} = useStoreCart();
|
||||
const { selectShippingRate, isSelectingRate } = useSelectShippingRates();
|
||||
const [ shippingErrorStatus, dispatchErrorStatus ] = useReducer(
|
||||
errorStatusReducer,
|
||||
NONE
|
||||
);
|
||||
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
|
||||
const currentObservers = useRef( observers );
|
||||
const eventObservers = useMemo(
|
||||
() => ( {
|
||||
onShippingRateSuccess: emitterObservers( observerDispatch )
|
||||
.onSuccess,
|
||||
onShippingRateFail: emitterObservers( observerDispatch ).onFail,
|
||||
onShippingRateSelectSuccess: emitterObservers( observerDispatch )
|
||||
.onSelectSuccess,
|
||||
onShippingRateSelectFail: emitterObservers( observerDispatch )
|
||||
.onSelectFail,
|
||||
} ),
|
||||
[ observerDispatch ]
|
||||
);
|
||||
|
||||
// set observers on ref so it's always current.
|
||||
useEffect( () => {
|
||||
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 ) {
|
||||
dispatchActions.incrementCalculating();
|
||||
} else {
|
||||
dispatchActions.decrementCalculating();
|
||||
}
|
||||
}, [ shippingRatesLoading, dispatchActions ] );
|
||||
|
||||
// increment/decrement checkout calculating counts when shipping rates are being selected.
|
||||
useEffect( () => {
|
||||
if ( isSelectingRate ) {
|
||||
dispatchActions.incrementCalculating();
|
||||
} else {
|
||||
dispatchActions.decrementCalculating();
|
||||
}
|
||||
}, [ isSelectingRate, dispatchActions ] );
|
||||
|
||||
// set shipping error status if there are shipping error codes
|
||||
useEffect( () => {
|
||||
if (
|
||||
cartErrors.length > 0 &&
|
||||
hasInvalidShippingAddress( cartErrors )
|
||||
) {
|
||||
dispatchErrorStatus( { type: INVALID_ADDRESS } );
|
||||
} else {
|
||||
dispatchErrorStatus( { type: NONE } );
|
||||
}
|
||||
}, [ cartErrors ] );
|
||||
|
||||
const currentErrorStatus = useMemo(
|
||||
() => ( {
|
||||
isPristine: shippingErrorStatus === NONE,
|
||||
isValid: shippingErrorStatus === NONE,
|
||||
hasInvalidAddress: shippingErrorStatus === INVALID_ADDRESS,
|
||||
hasError:
|
||||
shippingErrorStatus === UNKNOWN ||
|
||||
shippingErrorStatus === INVALID_ADDRESS,
|
||||
} ),
|
||||
[ shippingErrorStatus ]
|
||||
);
|
||||
|
||||
// emit events.
|
||||
useEffect( () => {
|
||||
if (
|
||||
! shippingRatesLoading &&
|
||||
( shippingRates.length === 0 || currentErrorStatus.hasError )
|
||||
) {
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.SHIPPING_RATES_FAIL,
|
||||
{
|
||||
hasInvalidAddress: currentErrorStatus.hasInvalidAddress,
|
||||
hasError: currentErrorStatus.hasError,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
currentErrorStatus.hasError,
|
||||
currentErrorStatus.hasInvalidAddress,
|
||||
] );
|
||||
|
||||
useEffect( () => {
|
||||
if (
|
||||
! shippingRatesLoading &&
|
||||
shippingRates.length > 0 &&
|
||||
! currentErrorStatus.hasError
|
||||
) {
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.SHIPPING_RATES_SUCCESS,
|
||||
shippingRates
|
||||
);
|
||||
}
|
||||
}, [ shippingRates, shippingRatesLoading, currentErrorStatus.hasError ] );
|
||||
|
||||
// emit shipping rate selection events.
|
||||
useEffect( () => {
|
||||
if ( isSelectingRate ) {
|
||||
return;
|
||||
}
|
||||
if ( currentErrorStatus.hasError ) {
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.SHIPPING_RATE_SELECT_FAIL,
|
||||
{
|
||||
hasError: currentErrorStatus.hasError,
|
||||
hasInvalidAddress: currentErrorStatus.hasInvalidAddress,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.SHIPPING_RATE_SELECT_SUCCESS,
|
||||
selectedRates.current
|
||||
);
|
||||
}
|
||||
}, [
|
||||
isSelectingRate,
|
||||
currentErrorStatus.hasError,
|
||||
currentErrorStatus.hasInvalidAddress,
|
||||
] );
|
||||
|
||||
/**
|
||||
* @type {ShippingDataContext}
|
||||
*/
|
||||
const ShippingData = {
|
||||
shippingErrorStatus: currentErrorStatus,
|
||||
dispatchErrorStatus,
|
||||
shippingErrorTypes: ERROR_TYPES,
|
||||
shippingRates,
|
||||
shippingRatesLoading,
|
||||
selectedRates: selectedRates.current,
|
||||
setSelectedRates: selectShippingRate,
|
||||
isSelectingRate,
|
||||
shippingAddress,
|
||||
setShippingAddress,
|
||||
needsShipping,
|
||||
hasCalculatedShipping,
|
||||
...eventObservers,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShippingDataContext.Provider value={ ShippingData }>
|
||||
{ children }
|
||||
</ShippingDataContext.Provider>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ERROR_TYPES } from './constants';
|
||||
|
||||
/**
|
||||
* Reducer for shipping status state
|
||||
*
|
||||
* @param {string} state The current status.
|
||||
* @param {Object} action The incoming action.
|
||||
* @param {string} action.type The type of action.
|
||||
*/
|
||||
export const errorStatusReducer = ( state, { type } ) => {
|
||||
if ( Object.values( ERROR_TYPES ).includes( type ) ) {
|
||||
return type;
|
||||
}
|
||||
return state;
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { shippingErrorCodes } from './constants';
|
||||
|
||||
export const hasInvalidShippingAddress = ( errors ) => {
|
||||
return errors.some( ( error ) => {
|
||||
if (
|
||||
error.code &&
|
||||
Object.values( shippingErrorCodes ).includes( error.code )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} );
|
||||
};
|
Reference in New Issue
Block a user