210 lines
5.5 KiB
TypeScript
210 lines
5.5 KiB
TypeScript
/**
|
|
* External dependencies
|
|
*/
|
|
import { __ } from '@wordpress/i18n';
|
|
import clsx from 'clsx';
|
|
import { createInterpolateElement, useEffect } from '@wordpress/element';
|
|
import {
|
|
useStoreCart,
|
|
useShowShippingTotalWarning,
|
|
} from '@woocommerce/base-context/hooks';
|
|
import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context';
|
|
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
|
|
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
|
|
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
|
|
import { StoreNoticesContainer } from '@woocommerce/blocks-components';
|
|
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
|
|
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
|
|
import { useDispatch, useSelect } from '@wordpress/data';
|
|
import {
|
|
CHECKOUT_STORE_KEY,
|
|
VALIDATION_STORE_KEY,
|
|
} from '@woocommerce/block-data';
|
|
|
|
/**
|
|
* Internal dependencies
|
|
*/
|
|
import './styles/style.scss';
|
|
import EmptyCart from './empty-cart';
|
|
import CheckoutOrderError from './checkout-order-error';
|
|
import { LOGIN_TO_CHECKOUT_URL, isLoginRequired, reloadPage } from './utils';
|
|
import type { Attributes } from './types';
|
|
import { CheckoutBlockContext } from './context';
|
|
|
|
const MustLoginPrompt = () => {
|
|
return (
|
|
<div className="wc-block-must-login-prompt">
|
|
{ __( 'You must be logged in to checkout.', 'woocommerce' ) }{ ' ' }
|
|
<a href={ LOGIN_TO_CHECKOUT_URL }>
|
|
{ __( 'Click here to log in.', 'woocommerce' ) }
|
|
</a>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Checkout = ( {
|
|
attributes,
|
|
children,
|
|
}: {
|
|
attributes: Attributes;
|
|
children: React.ReactChildren;
|
|
} ): JSX.Element => {
|
|
const { hasOrder, customerId } = useSelect( ( select ) => {
|
|
const store = select( CHECKOUT_STORE_KEY );
|
|
return {
|
|
hasOrder: store.hasOrder(),
|
|
customerId: store.getCustomerId(),
|
|
};
|
|
} );
|
|
const { cartItems, cartIsLoading } = useStoreCart();
|
|
|
|
const {
|
|
showCompanyField,
|
|
requireCompanyField,
|
|
showApartmentField,
|
|
requireApartmentField,
|
|
showPhoneField,
|
|
requirePhoneField,
|
|
showFormStepNumbers,
|
|
} = attributes;
|
|
|
|
if ( ! cartIsLoading && cartItems.length === 0 ) {
|
|
return <EmptyCart />;
|
|
}
|
|
|
|
if ( ! hasOrder ) {
|
|
return <CheckoutOrderError />;
|
|
}
|
|
|
|
/**
|
|
* If checkout requires an account (guest checkout is turned off), render
|
|
* a notice and prevent access to the checkout, unless we explicitly allow
|
|
* account creation during the checkout flow.
|
|
*/
|
|
if (
|
|
isLoginRequired( customerId ) &&
|
|
! getSetting( 'checkoutAllowsSignup', false )
|
|
) {
|
|
return <MustLoginPrompt />;
|
|
}
|
|
|
|
return (
|
|
<CheckoutBlockContext.Provider
|
|
value={
|
|
{
|
|
showCompanyField,
|
|
requireCompanyField,
|
|
showApartmentField,
|
|
requireApartmentField,
|
|
showPhoneField,
|
|
requirePhoneField,
|
|
showFormStepNumbers,
|
|
} as Attributes
|
|
}
|
|
>
|
|
{ children }
|
|
</CheckoutBlockContext.Provider>
|
|
);
|
|
};
|
|
|
|
const ScrollOnError = ( {
|
|
scrollToTop,
|
|
}: {
|
|
scrollToTop: ( props: Record< string, unknown > ) => void;
|
|
} ): null => {
|
|
const { hasError: checkoutHasError, isIdle: checkoutIsIdle } = useSelect(
|
|
( select ) => {
|
|
const store = select( CHECKOUT_STORE_KEY );
|
|
return {
|
|
isIdle: store.isIdle(),
|
|
hasError: store.hasError(),
|
|
};
|
|
}
|
|
);
|
|
const { hasValidationErrors } = useSelect( ( select ) => {
|
|
const store = select( VALIDATION_STORE_KEY );
|
|
return {
|
|
hasValidationErrors: store.hasValidationErrors(),
|
|
};
|
|
} );
|
|
const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY );
|
|
|
|
const hasErrorsToDisplay =
|
|
checkoutIsIdle && checkoutHasError && hasValidationErrors;
|
|
|
|
useEffect( () => {
|
|
let scrollToTopTimeout: number;
|
|
if ( hasErrorsToDisplay ) {
|
|
showAllValidationErrors();
|
|
// Scroll after a short timeout to allow a re-render. This will allow focusableSelector to match updated components.
|
|
scrollToTopTimeout = window.setTimeout( () => {
|
|
scrollToTop( {
|
|
focusableSelector:
|
|
'input:invalid, .has-error input, .has-error select',
|
|
} );
|
|
}, 50 );
|
|
}
|
|
return () => {
|
|
clearTimeout( scrollToTopTimeout );
|
|
};
|
|
}, [ hasErrorsToDisplay, scrollToTop, showAllValidationErrors ] );
|
|
|
|
return null;
|
|
};
|
|
|
|
const Block = ( {
|
|
attributes,
|
|
children,
|
|
scrollToTop,
|
|
}: {
|
|
attributes: Attributes;
|
|
children: React.ReactChildren;
|
|
scrollToTop: ( props: Record< string, unknown > ) => void;
|
|
} ): JSX.Element => {
|
|
useShowShippingTotalWarning();
|
|
return (
|
|
<BlockErrorBoundary
|
|
header={ __(
|
|
'Something went wrong. Please contact us for assistance.',
|
|
'woocommerce'
|
|
) }
|
|
text={ createInterpolateElement(
|
|
__(
|
|
'The checkout has encountered an unexpected error. <button>Try reloading the page</button>. If the error persists, please get in touch with us so we can assist.',
|
|
'woocommerce'
|
|
),
|
|
{
|
|
button: (
|
|
<button
|
|
className="wc-block-link-button"
|
|
onClick={ reloadPage }
|
|
/>
|
|
),
|
|
}
|
|
) }
|
|
showErrorMessage={ CURRENT_USER_IS_ADMIN }
|
|
>
|
|
<StoreNoticesContainer
|
|
context={ [ noticeContexts.CHECKOUT, noticeContexts.CART ] }
|
|
/>
|
|
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
|
|
<SlotFillProvider>
|
|
<CheckoutProvider>
|
|
<SidebarLayout
|
|
className={ clsx( 'wc-block-checkout', {
|
|
'has-dark-controls': attributes.hasDarkControls,
|
|
} ) }
|
|
>
|
|
<Checkout attributes={ attributes }>
|
|
{ children }
|
|
</Checkout>
|
|
<ScrollOnError scrollToTop={ scrollToTop } />
|
|
</SidebarLayout>
|
|
</CheckoutProvider>
|
|
</SlotFillProvider>
|
|
</BlockErrorBoundary>
|
|
);
|
|
};
|
|
|
|
export default withScrollToTop( Block );
|