initial commit
This commit is contained in:
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES } from './constants';
|
||||
|
||||
const {
|
||||
SET_PRISTINE,
|
||||
SET_IDLE,
|
||||
SET_DISABLED,
|
||||
SET_PROCESSING,
|
||||
SET_BEFORE_PROCESSING,
|
||||
SET_AFTER_PROCESSING,
|
||||
SET_PROCESSING_RESPONSE,
|
||||
SET_HAS_ERROR,
|
||||
SET_NO_ERROR,
|
||||
SET_QUANTITY,
|
||||
SET_REQUEST_PARAMS,
|
||||
} = ACTION_TYPES;
|
||||
|
||||
/**
|
||||
* All the actions that can be dispatched for the checkout.
|
||||
*/
|
||||
export const actions = {
|
||||
setPristine: () => ( {
|
||||
type: SET_PRISTINE,
|
||||
} ),
|
||||
setIdle: () => ( {
|
||||
type: SET_IDLE,
|
||||
} ),
|
||||
setDisabled: () => ( {
|
||||
type: SET_DISABLED,
|
||||
} ),
|
||||
setProcessing: () => ( {
|
||||
type: SET_PROCESSING,
|
||||
} ),
|
||||
setBeforeProcessing: () => ( {
|
||||
type: SET_BEFORE_PROCESSING,
|
||||
} ),
|
||||
setAfterProcessing: () => ( {
|
||||
type: SET_AFTER_PROCESSING,
|
||||
} ),
|
||||
setProcessingResponse: ( data ) => ( {
|
||||
type: SET_PROCESSING_RESPONSE,
|
||||
data,
|
||||
} ),
|
||||
setHasError: ( hasError = true ) => {
|
||||
const type = hasError ? SET_HAS_ERROR : SET_NO_ERROR;
|
||||
return { type };
|
||||
},
|
||||
setQuantity: ( quantity ) => ( {
|
||||
type: SET_QUANTITY,
|
||||
quantity,
|
||||
} ),
|
||||
setRequestParams: ( data ) => ( {
|
||||
type: SET_REQUEST_PARAMS,
|
||||
data,
|
||||
} ),
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @type {import("@woocommerce/type-defs/add-to-cart-form").AddToCartFormStatusConstants}
|
||||
*/
|
||||
export const STATUS = {
|
||||
PRISTINE: 'pristine',
|
||||
IDLE: 'idle',
|
||||
DISABLED: 'disabled',
|
||||
PROCESSING: 'processing',
|
||||
BEFORE_PROCESSING: 'before_processing',
|
||||
AFTER_PROCESSING: 'after_processing',
|
||||
};
|
||||
|
||||
export const DEFAULT_STATE = {
|
||||
status: STATUS.PRISTINE,
|
||||
hasError: false,
|
||||
quantity: 1,
|
||||
processingResponse: null,
|
||||
requestParams: {},
|
||||
};
|
||||
export const ACTION_TYPES = {
|
||||
SET_PRISTINE: 'set_pristine',
|
||||
SET_IDLE: 'set_idle',
|
||||
SET_DISABLED: 'set_disabled',
|
||||
SET_PROCESSING: 'set_processing',
|
||||
SET_BEFORE_PROCESSING: 'set_before_processing',
|
||||
SET_AFTER_PROCESSING: 'set_after_processing',
|
||||
SET_PROCESSING_RESPONSE: 'set_processing_response',
|
||||
SET_HAS_ERROR: 'set_has_error',
|
||||
SET_NO_ERROR: 'set_no_error',
|
||||
SET_QUANTITY: 'set_quantity',
|
||||
SET_REQUEST_PARAMS: 'set_request_params',
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
emitterCallback,
|
||||
reducer,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
} from '../../../event-emit';
|
||||
|
||||
const EMIT_TYPES = {
|
||||
ADD_TO_CART_BEFORE_PROCESSING: 'add_to_cart_before_processing',
|
||||
ADD_TO_CART_AFTER_PROCESSING_WITH_SUCCESS:
|
||||
'add_to_cart_after_processing_with_success',
|
||||
ADD_TO_CART_AFTER_PROCESSING_WITH_ERROR:
|
||||
'add_to_cart_after_processing_with_error',
|
||||
};
|
||||
|
||||
/**
|
||||
* Receives a reducer dispatcher and returns an object with the callback registration function for
|
||||
* the add to cart 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 The emitter reducer dispatcher.
|
||||
*
|
||||
* @return {Object} An object with the add to cart form emitter registration
|
||||
*/
|
||||
const emitterObservers = ( dispatcher ) => ( {
|
||||
onAddToCartAfterProcessingWithSuccess: emitterCallback(
|
||||
EMIT_TYPES.ADD_TO_CART_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
dispatcher
|
||||
),
|
||||
onAddToCartProcessingWithError: emitterCallback(
|
||||
EMIT_TYPES.ADD_TO_CART_AFTER_PROCESSING_WITH_ERROR,
|
||||
dispatcher
|
||||
),
|
||||
onAddToCartBeforeProcessing: emitterCallback(
|
||||
EMIT_TYPES.ADD_TO_CART_BEFORE_PROCESSING,
|
||||
dispatcher
|
||||
),
|
||||
} );
|
||||
|
||||
export { EMIT_TYPES, emitterObservers, reducer, emitEvent, emitEventWithAbort };
|
@ -0,0 +1,322 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useShallowEqual } from '@woocommerce/base-hooks';
|
||||
import {
|
||||
productIsPurchasable,
|
||||
productSupportsAddToCartForm,
|
||||
} from '@woocommerce/base-utils';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { actions } from './actions';
|
||||
import { reducer } from './reducer';
|
||||
import { DEFAULT_STATE, STATUS } from './constants';
|
||||
import {
|
||||
EMIT_TYPES,
|
||||
emitterObservers,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
reducer as emitReducer,
|
||||
} from './event-emit';
|
||||
import { useValidationContext } from '../../validation';
|
||||
import { useStoreNotices } from '../../../hooks/use-store-notices';
|
||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/add-to-cart-form').AddToCartFormDispatchActions} AddToCartFormDispatchActions
|
||||
* @typedef {import('@woocommerce/type-defs/add-to-cart-form').AddToCartFormEventRegistration} AddToCartFormEventRegistration
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').AddToCartFormContext} AddToCartFormContext
|
||||
*/
|
||||
|
||||
const AddToCartFormContext = createContext( {
|
||||
product: {},
|
||||
productType: 'simple',
|
||||
productIsPurchasable: true,
|
||||
productHasOptions: false,
|
||||
supportsFormElements: true,
|
||||
showFormElements: false,
|
||||
quantity: 0,
|
||||
minQuantity: 1,
|
||||
maxQuantity: 99,
|
||||
requestParams: {},
|
||||
isIdle: false,
|
||||
isDisabled: false,
|
||||
isProcessing: false,
|
||||
isBeforeProcessing: false,
|
||||
isAfterProcessing: false,
|
||||
hasError: false,
|
||||
eventRegistration: {
|
||||
onAddToCartAfterProcessingWithSuccess: ( callback ) => void callback,
|
||||
onAddToCartAfterProcessingWithError: ( callback ) => void callback,
|
||||
onAddToCartBeforeProcessing: ( callback ) => void callback,
|
||||
},
|
||||
dispatchActions: {
|
||||
resetForm: () => void null,
|
||||
submitForm: () => void null,
|
||||
setQuantity: ( quantity ) => void quantity,
|
||||
setHasError: ( hasError ) => void hasError,
|
||||
setAfterProcessing: ( response ) => void response,
|
||||
setRequestParams: ( data ) => void data,
|
||||
},
|
||||
} );
|
||||
|
||||
/**
|
||||
* @return {AddToCartFormContext} Returns the add to cart form data context value
|
||||
*/
|
||||
export const useAddToCartFormContext = () => {
|
||||
// @ts-ignore
|
||||
return useContext( AddToCartFormContext );
|
||||
};
|
||||
|
||||
/**
|
||||
* Add to cart form state provider.
|
||||
*
|
||||
* This provides provides an api interface exposing add to cart form state.
|
||||
*
|
||||
* @param {Object} props Incoming props for the provider.
|
||||
* @param {Object} props.children The children being wrapped.
|
||||
* @param {Object} [props.product] The product for which the form belongs to.
|
||||
* @param {boolean} [props.showFormElements] Should form elements be shown.
|
||||
*/
|
||||
export const AddToCartFormStateContextProvider = ( {
|
||||
children,
|
||||
product,
|
||||
showFormElements,
|
||||
} ) => {
|
||||
const [ addToCartFormState, dispatch ] = useReducer(
|
||||
reducer,
|
||||
DEFAULT_STATE
|
||||
);
|
||||
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
|
||||
const currentObservers = useShallowEqual( observers );
|
||||
const { addErrorNotice, removeNotices } = useStoreNotices();
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const {
|
||||
isSuccessResponse,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
} = useEmitResponse();
|
||||
|
||||
/**
|
||||
* @type {AddToCartFormEventRegistration}
|
||||
*/
|
||||
const eventRegistration = useMemo(
|
||||
() => ( {
|
||||
onAddToCartAfterProcessingWithSuccess: emitterObservers(
|
||||
observerDispatch
|
||||
).onAddToCartAfterProcessingWithSuccess,
|
||||
onAddToCartAfterProcessingWithError: emitterObservers(
|
||||
observerDispatch
|
||||
).onAddToCartAfterProcessingWithError,
|
||||
onAddToCartBeforeProcessing: emitterObservers( observerDispatch )
|
||||
.onAddToCartBeforeProcessing,
|
||||
} ),
|
||||
[ observerDispatch ]
|
||||
);
|
||||
|
||||
/**
|
||||
* @type {AddToCartFormDispatchActions}
|
||||
*/
|
||||
const dispatchActions = useMemo(
|
||||
() => ( {
|
||||
resetForm: () => void dispatch( actions.setPristine() ),
|
||||
submitForm: () => void dispatch( actions.setBeforeProcessing() ),
|
||||
setQuantity: ( quantity ) =>
|
||||
void dispatch( actions.setQuantity( quantity ) ),
|
||||
setHasError: ( hasError ) =>
|
||||
void dispatch( actions.setHasError( hasError ) ),
|
||||
setRequestParams: ( data ) =>
|
||||
void dispatch( actions.setRequestParams( data ) ),
|
||||
setAfterProcessing: ( response ) => {
|
||||
dispatch( actions.setProcessingResponse( response ) );
|
||||
void dispatch( actions.setAfterProcessing() );
|
||||
},
|
||||
} ),
|
||||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* This Effect is responsible for disabling or enabling the form based on the provided product.
|
||||
*/
|
||||
useEffect( () => {
|
||||
const status = addToCartFormState.status;
|
||||
const willBeDisabled =
|
||||
! product.id || ! productIsPurchasable( product );
|
||||
|
||||
if ( status === STATUS.DISABLED && ! willBeDisabled ) {
|
||||
dispatch( actions.setIdle() );
|
||||
} else if ( status !== STATUS.DISABLED && willBeDisabled ) {
|
||||
dispatch( actions.setDisabled() );
|
||||
}
|
||||
}, [ addToCartFormState.status, product, dispatch ] );
|
||||
|
||||
/**
|
||||
* This Effect performs events before processing starts.
|
||||
*/
|
||||
useEffect( () => {
|
||||
const status = addToCartFormState.status;
|
||||
|
||||
if ( status === STATUS.BEFORE_PROCESSING ) {
|
||||
removeNotices( 'error' );
|
||||
emitEvent(
|
||||
currentObservers,
|
||||
EMIT_TYPES.ADD_TO_CART_BEFORE_PROCESSING,
|
||||
{}
|
||||
).then( ( response ) => {
|
||||
if ( response !== true ) {
|
||||
if ( Array.isArray( response ) ) {
|
||||
response.forEach(
|
||||
( { errorMessage, validationErrors } ) => {
|
||||
if ( errorMessage ) {
|
||||
addErrorNotice( errorMessage );
|
||||
}
|
||||
if ( validationErrors ) {
|
||||
setValidationErrors( validationErrors );
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
dispatch( actions.setIdle() );
|
||||
} else {
|
||||
dispatch( actions.setProcessing() );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}, [
|
||||
addToCartFormState.status,
|
||||
setValidationErrors,
|
||||
addErrorNotice,
|
||||
removeNotices,
|
||||
dispatch,
|
||||
currentObservers,
|
||||
] );
|
||||
|
||||
/**
|
||||
* This Effect performs events after processing is complete.
|
||||
*/
|
||||
useEffect( () => {
|
||||
if ( addToCartFormState.status === STATUS.AFTER_PROCESSING ) {
|
||||
// @todo: This data package differs from what is passed through in
|
||||
// the checkout state context. Should we introduce a "context"
|
||||
// property in the data package for this emitted event so that
|
||||
// observers are able to know what context the event is firing in?
|
||||
const data = {
|
||||
processingResponse: addToCartFormState.processingResponse,
|
||||
};
|
||||
|
||||
const handleErrorResponse = ( observerResponses ) => {
|
||||
let handled = false;
|
||||
observerResponses.forEach( ( response ) => {
|
||||
const { message, messageContext } = response;
|
||||
if (
|
||||
( isErrorResponse( response ) ||
|
||||
isFailResponse( response ) ) &&
|
||||
message
|
||||
) {
|
||||
const errorOptions = messageContext
|
||||
? { context: messageContext }
|
||||
: undefined;
|
||||
handled = true;
|
||||
addErrorNotice( message, errorOptions );
|
||||
}
|
||||
} );
|
||||
return handled;
|
||||
};
|
||||
|
||||
if ( addToCartFormState.hasError ) {
|
||||
// allow things to customize the error with a fallback if nothing customizes it.
|
||||
emitEventWithAbort(
|
||||
currentObservers,
|
||||
EMIT_TYPES.ADD_TO_CART_AFTER_PROCESSING_WITH_ERROR,
|
||||
data
|
||||
).then( ( observerResponses ) => {
|
||||
if ( ! handleErrorResponse( observerResponses ) ) {
|
||||
// no error handling in place by anything so let's fall back to default
|
||||
const message =
|
||||
data.processingResponse?.message ||
|
||||
__(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woocommerce'
|
||||
);
|
||||
addErrorNotice( message, {
|
||||
id: 'add-to-cart',
|
||||
} );
|
||||
}
|
||||
dispatch( actions.setIdle() );
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
emitEventWithAbort(
|
||||
currentObservers,
|
||||
EMIT_TYPES.ADD_TO_CART_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
data
|
||||
).then( ( observerResponses ) => {
|
||||
if ( handleErrorResponse( observerResponses ) ) {
|
||||
// this will set an error which will end up
|
||||
// triggering the onAddToCartAfterProcessingWithError emitter.
|
||||
// and then setting to IDLE state.
|
||||
dispatch( actions.setHasError( true ) );
|
||||
} else {
|
||||
dispatch( actions.setIdle() );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}, [
|
||||
addToCartFormState.status,
|
||||
addToCartFormState.hasError,
|
||||
addToCartFormState.processingResponse,
|
||||
dispatchActions,
|
||||
addErrorNotice,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
isSuccessResponse,
|
||||
currentObservers,
|
||||
] );
|
||||
|
||||
const supportsFormElements = productSupportsAddToCartForm( product );
|
||||
|
||||
/**
|
||||
* @type {AddToCartFormContext}
|
||||
*/
|
||||
const contextData = {
|
||||
product,
|
||||
productType: product.type || 'simple',
|
||||
productIsPurchasable: productIsPurchasable( product ),
|
||||
productHasOptions: product.has_options || false,
|
||||
supportsFormElements,
|
||||
showFormElements: showFormElements && supportsFormElements,
|
||||
quantity: addToCartFormState.quantity,
|
||||
minQuantity: 1,
|
||||
maxQuantity: product.quantity_limit || 99,
|
||||
requestParams: addToCartFormState.requestParams,
|
||||
isIdle: addToCartFormState.status === STATUS.IDLE,
|
||||
isDisabled: addToCartFormState.status === STATUS.DISABLED,
|
||||
isProcessing: addToCartFormState.status === STATUS.PROCESSING,
|
||||
isBeforeProcessing:
|
||||
addToCartFormState.status === STATUS.BEFORE_PROCESSING,
|
||||
isAfterProcessing:
|
||||
addToCartFormState.status === STATUS.AFTER_PROCESSING,
|
||||
hasError: addToCartFormState.hasError,
|
||||
eventRegistration,
|
||||
dispatchActions,
|
||||
};
|
||||
return (
|
||||
<AddToCartFormContext.Provider
|
||||
// @ts-ignore
|
||||
value={ contextData }
|
||||
>
|
||||
{ children }
|
||||
</AddToCartFormContext.Provider>
|
||||
);
|
||||
};
|
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION_TYPES, DEFAULT_STATE, STATUS } from './constants';
|
||||
|
||||
const {
|
||||
SET_PRISTINE,
|
||||
SET_IDLE,
|
||||
SET_DISABLED,
|
||||
SET_PROCESSING,
|
||||
SET_BEFORE_PROCESSING,
|
||||
SET_AFTER_PROCESSING,
|
||||
SET_PROCESSING_RESPONSE,
|
||||
SET_HAS_ERROR,
|
||||
SET_NO_ERROR,
|
||||
SET_QUANTITY,
|
||||
SET_REQUEST_PARAMS,
|
||||
} = ACTION_TYPES;
|
||||
|
||||
const {
|
||||
PRISTINE,
|
||||
IDLE,
|
||||
DISABLED,
|
||||
PROCESSING,
|
||||
BEFORE_PROCESSING,
|
||||
AFTER_PROCESSING,
|
||||
} = STATUS;
|
||||
|
||||
/**
|
||||
* Reducer for the checkout state
|
||||
*
|
||||
* @param {Object} state Current state.
|
||||
* @param {Object} action Incoming action object.
|
||||
* @param {number} action.quantity Incoming quantity.
|
||||
* @param {string} action.type Type of action.
|
||||
* @param {Object} action.data Incoming payload for action.
|
||||
*/
|
||||
export const reducer = ( state = DEFAULT_STATE, { quantity, type, data } ) => {
|
||||
let newState;
|
||||
switch ( type ) {
|
||||
case SET_PRISTINE:
|
||||
newState = DEFAULT_STATE;
|
||||
break;
|
||||
case SET_IDLE:
|
||||
newState =
|
||||
state.status !== IDLE
|
||||
? {
|
||||
...state,
|
||||
status: IDLE,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_DISABLED:
|
||||
newState =
|
||||
state.status !== DISABLED
|
||||
? {
|
||||
...state,
|
||||
status: DISABLED,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_QUANTITY:
|
||||
newState =
|
||||
quantity !== state.quantity
|
||||
? {
|
||||
...state,
|
||||
quantity,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_REQUEST_PARAMS:
|
||||
newState = {
|
||||
...state,
|
||||
requestParams: {
|
||||
...state.requestParams,
|
||||
...data,
|
||||
},
|
||||
};
|
||||
break;
|
||||
case SET_PROCESSING_RESPONSE:
|
||||
newState = {
|
||||
...state,
|
||||
processingResponse: data,
|
||||
};
|
||||
break;
|
||||
case SET_PROCESSING:
|
||||
newState =
|
||||
state.status !== PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: PROCESSING,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
// clear any error state.
|
||||
newState =
|
||||
newState.hasError === false
|
||||
? newState
|
||||
: { ...newState, hasError: false };
|
||||
break;
|
||||
case SET_BEFORE_PROCESSING:
|
||||
newState =
|
||||
state.status !== BEFORE_PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: BEFORE_PROCESSING,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_AFTER_PROCESSING:
|
||||
newState =
|
||||
state.status !== AFTER_PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: AFTER_PROCESSING,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_HAS_ERROR:
|
||||
newState = state.hasError
|
||||
? state
|
||||
: {
|
||||
...state,
|
||||
hasError: true,
|
||||
};
|
||||
newState =
|
||||
state.status === PROCESSING ||
|
||||
state.status === BEFORE_PROCESSING
|
||||
? {
|
||||
...newState,
|
||||
status: IDLE,
|
||||
}
|
||||
: newState;
|
||||
break;
|
||||
case SET_NO_ERROR:
|
||||
newState = state.hasError
|
||||
? {
|
||||
...state,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
}
|
||||
// automatically update state to idle from pristine as soon as it initially changes.
|
||||
if (
|
||||
newState !== state &&
|
||||
type !== SET_PRISTINE &&
|
||||
newState.status === PRISTINE
|
||||
) {
|
||||
newState.status = IDLE;
|
||||
}
|
||||
return newState;
|
||||
};
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AddToCartFormStateContextProvider } from '../form-state';
|
||||
import { ValidationContextProvider } from '../../validation';
|
||||
import FormSubmit from './submit';
|
||||
|
||||
/**
|
||||
* Add to cart form provider.
|
||||
*
|
||||
* This wraps the add to cart form and provides an api interface for children via various hooks.
|
||||
*
|
||||
* @param {Object} props Incoming props for the provider.
|
||||
* @param {Object} props.children The children being wrapped.
|
||||
* @param {Object} [props.product] The product for which the form belongs to.
|
||||
* @param {boolean} [props.showFormElements] Should form elements be shown.
|
||||
*/
|
||||
export const AddToCartFormContextProvider = ( {
|
||||
children,
|
||||
product,
|
||||
showFormElements,
|
||||
} ) => {
|
||||
return (
|
||||
<ValidationContextProvider>
|
||||
<AddToCartFormStateContextProvider
|
||||
product={ product }
|
||||
showFormElements={ showFormElements }
|
||||
>
|
||||
{ children }
|
||||
<FormSubmit />
|
||||
</AddToCartFormStateContextProvider>
|
||||
</ValidationContextProvider>
|
||||
);
|
||||
};
|
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import triggerFetch from '@wordpress/api-fetch';
|
||||
import { useEffect, useCallback, useState } from '@wordpress/element';
|
||||
import { decodeEntities } from '@wordpress/html-entities';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useAddToCartFormContext } from '../../form-state';
|
||||
import { useValidationContext } from '../../../validation';
|
||||
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
|
||||
import { useStoreNotices } from '../../../../hooks/use-store-notices';
|
||||
|
||||
/**
|
||||
* FormSubmit.
|
||||
*
|
||||
* Subscribes to add to cart form context and triggers processing via the API.
|
||||
*/
|
||||
const FormSubmit = () => {
|
||||
const {
|
||||
dispatchActions,
|
||||
product,
|
||||
quantity,
|
||||
eventRegistration,
|
||||
hasError,
|
||||
isProcessing,
|
||||
requestParams,
|
||||
} = useAddToCartFormContext();
|
||||
const {
|
||||
hasValidationErrors,
|
||||
showAllValidationErrors,
|
||||
} = useValidationContext();
|
||||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
||||
const { receiveCart } = useStoreCart();
|
||||
const [ isSubmitting, setIsSubmitting ] = useState( false );
|
||||
const doSubmit = ! hasError && isProcessing;
|
||||
|
||||
const checkValidationContext = useCallback( () => {
|
||||
if ( hasValidationErrors ) {
|
||||
showAllValidationErrors();
|
||||
return {
|
||||
type: 'error',
|
||||
};
|
||||
}
|
||||
return true;
|
||||
}, [ hasValidationErrors, showAllValidationErrors ] );
|
||||
|
||||
// Subscribe to emitter before processing.
|
||||
useEffect( () => {
|
||||
const unsubscribeProcessing = eventRegistration.onAddToCartBeforeProcessing(
|
||||
checkValidationContext,
|
||||
0
|
||||
);
|
||||
return () => {
|
||||
unsubscribeProcessing();
|
||||
};
|
||||
}, [ eventRegistration, checkValidationContext ] );
|
||||
|
||||
// Triggers form submission to the API.
|
||||
const submitFormCallback = useCallback( () => {
|
||||
setIsSubmitting( true );
|
||||
removeNotice( 'add-to-cart' );
|
||||
|
||||
const fetchData = {
|
||||
id: product.id || 0,
|
||||
quantity,
|
||||
...requestParams,
|
||||
};
|
||||
|
||||
triggerFetch( {
|
||||
path: '/wc/store/cart/add-item',
|
||||
method: 'POST',
|
||||
data: fetchData,
|
||||
cache: 'no-store',
|
||||
parse: false,
|
||||
} )
|
||||
.then( ( fetchResponse ) => {
|
||||
// Update nonce.
|
||||
triggerFetch.setNonce( fetchResponse.headers );
|
||||
|
||||
// Handle response.
|
||||
fetchResponse.json().then( function ( response ) {
|
||||
if ( ! fetchResponse.ok ) {
|
||||
// We received an error response.
|
||||
if ( response.body && response.body.message ) {
|
||||
addErrorNotice(
|
||||
decodeEntities( response.body.message ),
|
||||
{
|
||||
id: 'add-to-cart',
|
||||
}
|
||||
);
|
||||
} else {
|
||||
addErrorNotice(
|
||||
__(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woocommerce'
|
||||
),
|
||||
{
|
||||
id: 'add-to-cart',
|
||||
}
|
||||
);
|
||||
}
|
||||
dispatchActions.setHasError();
|
||||
} else {
|
||||
receiveCart( response );
|
||||
}
|
||||
dispatchActions.setAfterProcessing( response );
|
||||
setIsSubmitting( false );
|
||||
} );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
error.json().then( function ( response ) {
|
||||
// If updated cart state was returned, also update that.
|
||||
if ( response.data?.cart ) {
|
||||
receiveCart( response.data.cart );
|
||||
}
|
||||
dispatchActions.setHasError();
|
||||
dispatchActions.setAfterProcessing( response );
|
||||
setIsSubmitting( false );
|
||||
} );
|
||||
} );
|
||||
}, [
|
||||
product,
|
||||
addErrorNotice,
|
||||
removeNotice,
|
||||
receiveCart,
|
||||
dispatchActions,
|
||||
quantity,
|
||||
requestParams,
|
||||
] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( doSubmit && ! isSubmitting ) {
|
||||
submitFormCallback();
|
||||
}
|
||||
}, [ doSubmit, submitFormCallback, isSubmitting ] );
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default FormSubmit;
|
@ -0,0 +1,2 @@
|
||||
export * from './form';
|
||||
export * from './form-state';
|
Reference in New Issue
Block a user