initial commit

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

View File

@ -0,0 +1,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,
};

View File

@ -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 };

View File

@ -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>
</>
);
};

View File

@ -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;
};

View File

@ -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;
} );
};