/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import triggerFetch from '@wordpress/api-fetch';
import {
useEffect,
useRef,
useCallback,
useState,
useMemo,
} from '@wordpress/element';
import {
emptyHiddenAddressFields,
formatStoreApiErrorMessage,
} from '@woocommerce/base-utils';
/**
* Internal dependencies
*/
import { preparePaymentData } from './utils';
import { useCheckoutContext } from '../../checkout-state';
import { useShippingDataContext } from '../../shipping';
import { useCustomerDataContext } from '../../customer';
import { usePaymentMethodDataContext } from '../../payment-methods';
import { useValidationContext } from '../../../validation';
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
import { useStoreNotices } from '../../../../hooks/use-store-notices';
/**
* CheckoutProcessor component.
*
* @todo Needs to consume all contexts.
*
* Subscribes to checkout context and triggers processing via the API.
*/
const CheckoutProcessor = () => {
const {
hasError: checkoutHasError,
onCheckoutValidationBeforeProcessing,
dispatchActions,
redirectUrl,
isProcessing: checkoutIsProcessing,
isBeforeProcessing: checkoutIsBeforeProcessing,
isComplete: checkoutIsComplete,
orderNotes,
shouldCreateAccount,
} = useCheckoutContext();
const { hasValidationErrors } = useValidationContext();
const { shippingErrorStatus } = useShippingDataContext();
const { billingData, shippingAddress } = useCustomerDataContext();
const { cartNeedsPayment, receiveCart } = useStoreCart();
const {
activePaymentMethod,
currentStatus: currentPaymentStatus,
paymentMethodData,
expressPaymentMethods,
paymentMethods,
shouldSavePayment,
} = usePaymentMethodDataContext();
const { addErrorNotice, removeNotice, setIsSuppressed } = useStoreNotices();
const currentBillingData = useRef( billingData );
const currentShippingAddress = useRef( shippingAddress );
const currentRedirectUrl = useRef( redirectUrl );
const [ isProcessingOrder, setIsProcessingOrder ] = useState( false );
const expressPaymentMethodActive = Object.keys(
expressPaymentMethods
).includes( activePaymentMethod );
const paymentMethodId = useMemo( () => {
const merged = { ...expressPaymentMethods, ...paymentMethods };
return merged?.[ activePaymentMethod ]?.paymentMethodId;
}, [ activePaymentMethod, expressPaymentMethods, paymentMethods ] );
const checkoutWillHaveError =
( hasValidationErrors && ! expressPaymentMethodActive ) ||
currentPaymentStatus.hasError ||
shippingErrorStatus.hasError;
// If express payment method is active, let's suppress notices
useEffect( () => {
setIsSuppressed( expressPaymentMethodActive );
}, [ expressPaymentMethodActive, setIsSuppressed ] );
useEffect( () => {
if (
checkoutWillHaveError !== checkoutHasError &&
( checkoutIsProcessing || checkoutIsBeforeProcessing ) &&
! expressPaymentMethodActive
) {
dispatchActions.setHasError( checkoutWillHaveError );
}
}, [
checkoutWillHaveError,
checkoutHasError,
checkoutIsProcessing,
checkoutIsBeforeProcessing,
expressPaymentMethodActive,
dispatchActions,
] );
const paidAndWithoutErrors =
! checkoutHasError &&
! checkoutWillHaveError &&
( currentPaymentStatus.isSuccessful || ! cartNeedsPayment ) &&
checkoutIsProcessing;
useEffect( () => {
currentBillingData.current = billingData;
currentShippingAddress.current = shippingAddress;
currentRedirectUrl.current = redirectUrl;
}, [ billingData, shippingAddress, redirectUrl ] );
const checkValidation = useCallback( () => {
if ( hasValidationErrors ) {
return {
errorMessage: __(
'Some input fields are invalid.',
'woocommerce'
),
};
}
if ( currentPaymentStatus.hasError ) {
return {
errorMessage: __(
'There was a problem with your payment option.',
'woocommerce'
),
};
}
if ( shippingErrorStatus.hasError ) {
return {
errorMessage: __(
'There was a problem with your shipping option.',
'woocommerce'
),
};
}
return true;
}, [
hasValidationErrors,
currentPaymentStatus.hasError,
shippingErrorStatus.hasError,
] );
useEffect( () => {
let unsubscribeProcessing;
if ( ! expressPaymentMethodActive ) {
unsubscribeProcessing = onCheckoutValidationBeforeProcessing(
checkValidation,
0
);
}
return () => {
if ( ! expressPaymentMethodActive ) {
unsubscribeProcessing();
}
};
}, [
onCheckoutValidationBeforeProcessing,
checkValidation,
expressPaymentMethodActive,
] );
const processOrder = useCallback( () => {
setIsProcessingOrder( true );
removeNotice( 'checkout' );
let data = {
billing_address: emptyHiddenAddressFields(
currentBillingData.current
),
shipping_address: emptyHiddenAddressFields(
currentShippingAddress.current
),
customer_note: orderNotes,
should_create_account: shouldCreateAccount,
};
if ( cartNeedsPayment ) {
data = {
...data,
payment_method: paymentMethodId,
payment_data: preparePaymentData(
paymentMethodData,
shouldSavePayment,
activePaymentMethod
),
};
}
triggerFetch( {
path: '/wc/store/checkout',
method: 'POST',
data,
cache: 'no-store',
parse: false,
} )
.then( ( fetchResponse ) => {
// Update nonce.
triggerFetch.setNonce( fetchResponse.headers );
// Update user using headers.
dispatchActions.setCustomerId(
fetchResponse.headers.get( 'X-WC-Store-API-User' )
);
// Handle response.
fetchResponse.json().then( function ( response ) {
if ( ! fetchResponse.ok ) {
// We received an error response.
addErrorNotice(
formatStoreApiErrorMessage( response ),
{
id: 'checkout',
}
);
dispatchActions.setHasError();
}
dispatchActions.setAfterProcessing( response );
setIsProcessingOrder( false );
} );
} )
.catch( ( errorResponse ) => {
// Update nonce.
triggerFetch.setNonce( errorResponse.headers );
// If new customer ID returned, update the store.
if ( errorResponse.headers?.get( 'X-WC-Store-API-User' ) ) {
dispatchActions.setCustomerId(
errorResponse.headers.get( 'X-WC-Store-API-User' )
);
}
errorResponse.json().then( function ( response ) {
// If updated cart state was returned, update the store.
if ( response.data?.cart ) {
receiveCart( response.data.cart );
}
addErrorNotice( formatStoreApiErrorMessage( response ), {
id: 'checkout',
} );
response.additional_errors?.forEach?.(
( additionalError ) => {
addErrorNotice( additionalError.message, {
id: additionalError.error_code,
} );
}
);
dispatchActions.setHasError();
dispatchActions.setAfterProcessing( response );
setIsProcessingOrder( false );
} );
} );
}, [
addErrorNotice,
removeNotice,
paymentMethodId,
activePaymentMethod,
paymentMethodData,
shouldSavePayment,
cartNeedsPayment,
receiveCart,
dispatchActions,
orderNotes,
shouldCreateAccount,
] );
// redirect when checkout is complete and there is a redirect url.
useEffect( () => {
if ( currentRedirectUrl.current ) {
window.location.href = currentRedirectUrl.current;
}
}, [ checkoutIsComplete ] );
// process order if conditions are good.
useEffect( () => {
if ( paidAndWithoutErrors && ! isProcessingOrder ) {
processOrder();
}
}, [ processOrder, paidAndWithoutErrors, isProcessingOrder ] );
return null;
};
export default CheckoutProcessor;