diff --git a/assets/js/atomic/utils/index.js b/assets/js/atomic/utils/index.js index 111fae8fd7d..8b3c23b7821 100644 --- a/assets/js/atomic/utils/index.js +++ b/assets/js/atomic/utils/index.js @@ -1,5 +1,5 @@ export * from './get-block-map'; export * from './create-blocks-from-template'; export * from './render-parent-block'; -export * from './block-styling.js'; +export * from './block-styling'; export * from './render-standalone-blocks'; diff --git a/assets/js/base/components/block-error-boundary/style.scss b/assets/js/base/components/block-error-boundary/style.scss index 06b7b344483..0676d961a49 100644 --- a/assets/js/base/components/block-error-boundary/style.scss +++ b/assets/js/base/components/block-error-boundary/style.scss @@ -24,7 +24,7 @@ max-width: 60ch; } .wc-block-components-error__message { - margin: 1em 0 0; + margin: 1em auto 0; font-style: italic; color: $studio-gray-30; max-width: 60ch; diff --git a/assets/js/base/components/cart-checkout/form-step/style.scss b/assets/js/base/components/cart-checkout/form-step/style.scss index ddfea2b1540..cdfbf0206da 100644 --- a/assets/js/base/components/cart-checkout/form-step/style.scss +++ b/assets/js/base/components/cart-checkout/form-step/style.scss @@ -32,7 +32,11 @@ position: relative; } -.wc-block-components-checkout-step__content { +.wc-block-components-checkout-step__content > * { + margin-bottom: em($gap); +} +.wc-block-components-checkout-step--with-step-number .wc-block-components-checkout-step__content > :last-child { + margin-bottom: 0; padding-bottom: em($gap-large); } diff --git a/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx b/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx index e1913177849..9d3c27ab724 100644 --- a/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx +++ b/assets/js/base/components/cart-checkout/shipping-rates-control/index.tsx @@ -10,7 +10,7 @@ import { getShippingRatesPackageCount, getShippingRatesRateCount, } from '@woocommerce/base-utils'; -import { useStoreCart } from '@woocommerce/base-context/hooks'; +import { useStoreCart, useEditorContext } from '@woocommerce/base-context'; import { CartResponseShippingRate } from '@woocommerce/type-defs/cart-response'; import { ReactElement } from 'react'; @@ -162,6 +162,7 @@ const ShippingRatesControl = ( { ShippingRatesControlPackage, }, }; + const { isEditor } = useEditorContext(); return ( - - + { isEditor ? ( - + ) : ( + <> + + + + + + ) } ); }; diff --git a/assets/js/base/components/checkbox-control/index.js b/assets/js/base/components/checkbox-control/index.tsx similarity index 60% rename from assets/js/base/components/checkbox-control/index.js rename to assets/js/base/components/checkbox-control/index.tsx index 91fb25b9267..a28601685c3 100644 --- a/assets/js/base/components/checkbox-control/index.js +++ b/assets/js/base/components/checkbox-control/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import { withInstanceId } from '@woocommerce/base-hocs/with-instance-id'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; /** @@ -10,16 +9,17 @@ import classNames from 'classnames'; */ import './style.scss'; +type CheckboxControlProps = { + className?: string; + label?: string; + id?: string; + instanceId: string; + onChange: ( value: boolean ) => void; + children: React.ReactChildren; +}; + /** * Component used to show a checkbox control with styles. - * - * @param {Object} props Incoming props for the component. - * @param {string} props.className CSS class used. - * @param {string} props.label Label for component. - * @param {string} props.id Id for component. - * @param {string} props.instanceId Unique id for instance of component. - * @param {function():any} props.onChange Function called when input changes. - * @param {Object} props.rest Rest of properties spread. */ const CheckboxControl = ( { className, @@ -27,8 +27,9 @@ const CheckboxControl = ( { id, instanceId, onChange, + children, ...rest -} ) => { +}: CheckboxControlProps ): JSX.Element => { const checkboxId = id || `checkbox-control-${ instanceId }`; return ( @@ -54,19 +55,14 @@ const CheckboxControl = ( { > - - - { label } - + { label && ( + + { label } + + ) } + { children } ); }; -CheckboxControl.propTypes = { - className: PropTypes.string, - label: PropTypes.string, - id: PropTypes.string, - onChange: PropTypes.func, -}; - export default withInstanceId( CheckboxControl ); diff --git a/assets/js/base/components/checkbox-control/style.scss b/assets/js/base/components/checkbox-control/style.scss index 6cb72b0bcab..d0c8a9a3752 100644 --- a/assets/js/base/components/checkbox-control/style.scss +++ b/assets/js/base/components/checkbox-control/style.scss @@ -1,8 +1,7 @@ .wc-block-components-checkbox { @include reset-typography(); - align-items: center; + align-items: flex-start; display: flex; - height: 1em; position: relative; .wc-block-components-checkbox__input[type="checkbox"] { @@ -10,6 +9,7 @@ appearance: none; border: 2px solid $input-border-gray; border-radius: 2px; + box-sizing: border-box; height: em(24px); width: em(24px); margin: 0; @@ -48,8 +48,8 @@ .wc-block-components-checkbox__mark { fill: #000; position: absolute; - left: em(3px); - top: em(-2px); + margin-left: em(3px); + margin-top: em(1px); width: em(18px); height: em(18px); @@ -58,9 +58,11 @@ } } + > span, .wc-block-components-checkbox__label { padding-left: $gap; vertical-align: middle; + line-height: em(24px); } } diff --git a/assets/js/base/context/hooks/cart/test/use-store-cart.js b/assets/js/base/context/hooks/cart/test/use-store-cart.js index 1a9ffcfed26..1adeb1861d1 100644 --- a/assets/js/base/context/hooks/cart/test/use-store-cart.js +++ b/assets/js/base/context/hooks/cart/test/use-store-cart.js @@ -61,6 +61,7 @@ describe( 'useStoreCart', () => { state: '', postcode: '', country: '', + phone: '', }, shippingRates: previewCart.shipping_rates, extensions: {}, diff --git a/assets/js/base/context/hooks/cart/use-store-cart.ts b/assets/js/base/context/hooks/cart/use-store-cart.ts index 2197748156d..406163033f5 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart.ts @@ -41,12 +41,12 @@ const defaultShippingAddress: CartResponseShippingAddress = { state: '', postcode: '', country: '', + phone: '', }; const defaultBillingAddress: CartResponseBillingAddress = { ...defaultShippingAddress, email: '', - phone: '', }; const defaultCartTotals: CartResponseTotals = { diff --git a/assets/js/base/context/hooks/use-checkout-address.js b/assets/js/base/context/hooks/use-checkout-address.js index 0ac8b09e573..a4e7cb3a61e 100644 --- a/assets/js/base/context/hooks/use-checkout-address.js +++ b/assets/js/base/context/hooks/use-checkout-address.js @@ -2,7 +2,7 @@ * External dependencies */ import { defaultAddressFields } from '@woocommerce/settings'; -import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; +import { useEffect, useCallback, useRef } from '@wordpress/element'; /** * Internal dependencies @@ -10,43 +10,22 @@ import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { useShippingDataContext, useCustomerDataContext, - useCheckoutContext, } from '../providers/cart-checkout'; -/** - * Compare two addresses and see if they are the same. - * - * @param {Object} address1 First address. - * @param {Object} address2 Second address. - */ -const isSameAddress = ( address1, address2 ) => { - return Object.keys( defaultAddressFields ).every( - ( field ) => address1[ field ] === address2[ field ] - ); -}; - /** * Custom hook for exposing address related functionality for the checkout address form. */ export const useCheckoutAddress = () => { - const { customerId } = useCheckoutContext(); const { needsShipping } = useShippingDataContext(); const { billingData, setBillingData, shippingAddress, setShippingAddress, + shippingAsBilling, + setShippingAsBilling, } = useCustomerDataContext(); - // This tracks the state of the "shipping as billing" address checkbox. It's - // initial value is true (if shipping is needed), however, if the user is - // logged in and they have a different billing address, we can toggle this off. - const [ shippingAsBilling, setShippingAsBilling ] = useState( - () => - needsShipping && - ( ! customerId || isSameAddress( shippingAddress, billingData ) ) - ); - const currentShippingAsBilling = useRef( shippingAsBilling ); const previousBillingData = useRef( billingData ); @@ -78,9 +57,8 @@ export const useCheckoutAddress = () => { [ needsShipping, setShippingAddress, setBillingData ] ); - // When the "Use same address" checkbox is toggled we need to update the current billing address to reflect this; - // that is either setting the billing address to the shipping address, or restoring the billing address to it's - // previous state. + // When the "Use same address" checkbox is toggled we need to update the current billing address to reflect this. + // This either sets the billing address to the shipping address, or restores the billing address to it's previous state. useEffect( () => { if ( currentShippingAsBilling.current !== shippingAsBilling ) { if ( shippingAsBilling ) { @@ -88,13 +66,13 @@ export const useCheckoutAddress = () => { setBillingData( shippingAddress ); } else { const { - // We need to pluck out email and phone from previous billing data because they can be empty, causing the current email and phone to get emptied. See issue #4155 + // We need to pluck out email from previous billing data because they can be empty, causing the current email to get emptied. See issue #4155 /* eslint-disable no-unused-vars */ email, - phone, /* eslint-enable no-unused-vars */ ...billingAddress } = previousBillingData.current; + setBillingData( { ...billingAddress, } ); @@ -103,14 +81,29 @@ export const useCheckoutAddress = () => { } }, [ shippingAsBilling, setBillingData, shippingAddress, billingData ] ); - const setEmail = ( value ) => - void setBillingData( { - email: value, - } ); - const setPhone = ( value ) => - void setBillingData( { - phone: value, - } ); + const setEmail = useCallback( + ( value ) => + void setBillingData( { + email: value, + } ), + [ setBillingData ] + ); + + const setPhone = useCallback( + ( value ) => + void setBillingData( { + phone: value, + } ), + [ setBillingData ] + ); + + const setShippingPhone = useCallback( + ( value ) => + void setShippingFields( { + phone: value, + } ), + [ setShippingFields ] + ); // Note that currentShippingAsBilling is returned rather than the current state of shippingAsBilling--this is so that // the billing fields are not rendered before sync (billing field values are debounced and would be outdated) @@ -122,8 +115,10 @@ export const useCheckoutAddress = () => { setBillingFields, setEmail, setPhone, + setShippingPhone, shippingAsBilling, setShippingAsBilling, + showShippingFields: needsShipping, showBillingFields: ! needsShipping || ! currentShippingAsBilling.current, }; diff --git a/assets/js/base/context/hooks/use-customer-data.ts b/assets/js/base/context/hooks/use-customer-data.ts index 165fda1cc52..98dcce60c2d 100644 --- a/assets/js/base/context/hooks/use-customer-data.ts +++ b/assets/js/base/context/hooks/use-customer-data.ts @@ -112,7 +112,7 @@ export const useCustomerData = (): { /** * Set billing data. * - * Contains special handling for email and phone so those fields are not overwritten if simply updating address. + * Contains special handling for email so those fields are not overwritten if simply updating address. */ const setBillingData = useCallback( ( newData ) => { setCustomerData( ( prevState ) => { @@ -130,10 +130,15 @@ export const useCustomerData = (): { * Set shipping data. */ const setShippingAddress = useCallback( ( newData ) => { - setCustomerData( ( prevState ) => ( { - ...prevState, - shippingAddress: newData, - } ) ); + setCustomerData( ( prevState ) => { + return { + ...prevState, + shippingAddress: { + ...prevState.shippingAddress, + ...newData, + }, + }; + } ); }, [] ); /** diff --git a/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx index 83daeaea332..fde2834212b 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx +++ b/assets/js/base/context/providers/cart-checkout/checkout-state/index.tsx @@ -55,8 +55,7 @@ export const useCheckoutContext = (): CheckoutStateContextType => { /** * Checkout state provider - * This provides provides an api interface exposing checkout state for use with - * cart or checkout blocks. + * This provides an API interface exposing checkout state for use with cart or checkout blocks. * * @param {Object} props Incoming props for the provider. * @param {Object} props.children The children being wrapped. diff --git a/assets/js/base/context/providers/cart-checkout/customer/index.js b/assets/js/base/context/providers/cart-checkout/customer/index.js index 15c52c74ffc..abb6ff19682 100644 --- a/assets/js/base/context/providers/cart-checkout/customer/index.js +++ b/assets/js/base/context/providers/cart-checkout/customer/index.js @@ -1,12 +1,15 @@ /** * External dependencies */ -import { createContext, useContext } from '@wordpress/element'; +import { createContext, useContext, useState } from '@wordpress/element'; +import { defaultAddressFields } from '@woocommerce/settings'; /** * Internal dependencies */ import { useCustomerData } from '../../../hooks/use-customer-data'; +import { useCheckoutContext } from '../checkout-state'; +import { useStoreCart } from '../../../hooks/cart/use-store-cart'; /** * @typedef {import('@woocommerce/type-defs/contexts').CustomerDataContext} CustomerDataContext @@ -44,6 +47,7 @@ export const defaultShippingAddress = { state: '', postcode: '', country: '', + phone: '', }; /** @@ -54,6 +58,8 @@ const CustomerDataContext = createContext( { shippingAddress: defaultShippingAddress, setBillingData: () => null, setShippingAddress: () => null, + shippingAsBilling: true, + setShippingAsBilling: () => null, } ); /** @@ -63,6 +69,18 @@ export const useCustomerDataContext = () => { return useContext( CustomerDataContext ); }; +/** + * Compare two addresses and see if they are the same. + * + * @param {Object} address1 First address. + * @param {Object} address2 Second address. + */ +const isSameAddress = ( address1, address2 ) => { + return Object.keys( defaultAddressFields ).every( + ( field ) => address1[ field ] === address2[ field ] + ); +}; + /** * Customer Data context provider. * @@ -76,6 +94,13 @@ export const CustomerDataProvider = ( { children } ) => { setBillingData, setShippingAddress, } = useCustomerData(); + const { cartNeedsShipping: needsShipping } = useStoreCart(); + const { customerId } = useCheckoutContext(); + const [ shippingAsBilling, setShippingAsBilling ] = useState( + () => + needsShipping && + ( ! customerId || isSameAddress( shippingAddress, billingData ) ) + ); /** * @type {CustomerDataContext} @@ -85,6 +110,8 @@ export const CustomerDataProvider = ( { children } ) => { shippingAddress, setBillingData, setShippingAddress, + shippingAsBilling, + setShippingAsBilling, }; return ( diff --git a/assets/js/base/context/providers/store-notices/context.js b/assets/js/base/context/providers/store-notices/context.js index c488636ccfb..824d252242c 100644 --- a/assets/js/base/context/providers/store-notices/context.js +++ b/assets/js/base/context/providers/store-notices/context.js @@ -50,10 +50,10 @@ export const useStoreNoticesContext = () => { * - Success * * @param {Object} props Incoming props for the component. - * @param {React.ReactChildren} props.children The Elements wrapped by this component. - * @param {string} props.className CSS class used. - * @param {boolean} props.createNoticeContainer Whether to create a notice container or not. - * @param {string} props.context The notice context for notices being rendered. + * @param {JSX.Element} props.children The Elements wrapped by this component. + * @param {string} [props.className] CSS class used. + * @param {boolean} [props.createNoticeContainer] Whether to create a notice container or not. + * @param {string} [props.context] The notice context for notices being rendered. */ export const StoreNoticesProvider = ( { children, diff --git a/assets/js/base/context/providers/validation/context.js b/assets/js/base/context/providers/validation/context.js index 37b0dd7ac06..9ccfe77c308 100644 --- a/assets/js/base/context/providers/validation/context.js +++ b/assets/js/base/context/providers/validation/context.js @@ -41,7 +41,7 @@ export const useValidationContext = () => { * for tracking validation. * * @param {Object} props Incoming props for the component. - * @param {React.ReactChildren} props.children What react elements are wrapped by this component. + * @param {JSX.Element} props.children What react elements are wrapped by this component. */ export const ValidationContextProvider = ( { children } ) => { const [ validationErrors, updateValidationErrors ] = useState( {} ); diff --git a/assets/js/base/hocs/with-scroll-to-top/index.js b/assets/js/base/hocs/with-scroll-to-top/index.js deleted file mode 100644 index e57bc84b696..00000000000 --- a/assets/js/base/hocs/with-scroll-to-top/index.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * External dependencies - */ -import { Component, createRef } from 'react'; - -/** - * Internal dependencies - */ -import './style.scss'; - -/** - * HOC that provides a function to scroll to the top of the component. - * - * @param {Function} OriginalComponent Component being wrapped. - */ -const withScrollToTop = ( OriginalComponent ) => { - class WrappedComponent extends Component { - constructor() { - super(); - - this.scrollPointRef = createRef(); - } - - scrollToTopIfNeeded = () => { - const scrollPointRefYPosition = this.scrollPointRef.current.getBoundingClientRect() - .bottom; - const isScrollPointRefVisible = - scrollPointRefYPosition >= 0 && - scrollPointRefYPosition <= window.innerHeight; - if ( ! isScrollPointRefVisible ) { - this.scrollPointRef.current.scrollIntoView(); - } - }; - - moveFocusToTop = ( focusableSelector ) => { - const focusableElements = this.scrollPointRef.current.parentElement.querySelectorAll( - focusableSelector - ); - if ( focusableElements.length ) { - focusableElements[ 0 ].focus(); - } - }; - - scrollToTop = ( args ) => { - if ( ! window || ! Number.isFinite( window.innerHeight ) ) { - return; - } - - this.scrollToTopIfNeeded(); - if ( args && args.focusableSelector ) { - this.moveFocusToTop( args.focusableSelector ); - } - }; - - render() { - return ( - <> -
- - - ); - } - } - - WrappedComponent.displayName = 'withScrollToTop'; - - return WrappedComponent; -}; - -export default withScrollToTop; diff --git a/assets/js/base/hocs/with-scroll-to-top/index.tsx b/assets/js/base/hocs/with-scroll-to-top/index.tsx new file mode 100644 index 00000000000..c9b9f8c513e --- /dev/null +++ b/assets/js/base/hocs/with-scroll-to-top/index.tsx @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import './style.scss'; + +interface ScrollToTopProps { + focusableSelector?: string; +} + +const maybeScrollToTop = ( scrollPoint: HTMLElement ): void => { + const yPos = scrollPoint.getBoundingClientRect().bottom; + const isScrollPointVisible = yPos >= 0 && yPos <= window.innerHeight; + + if ( ! isScrollPointVisible ) { + scrollPoint.scrollIntoView(); + } +}; + +const moveFocusToTop = ( + scrollPoint: HTMLElement, + focusableSelector: string +): void => { + const focusableElements = + scrollPoint.parentElement?.querySelectorAll( focusableSelector ) || []; + + if ( focusableElements.length ) { + ( focusableElements[ 0 ] as HTMLElement )?.focus(); + } +}; + +const scrollToHTMLElement = ( + scrollPoint: HTMLElement, + { focusableSelector }: ScrollToTopProps +): void => { + if ( ! window || ! Number.isFinite( window.innerHeight ) ) { + return; + } + + maybeScrollToTop( scrollPoint ); + + if ( focusableSelector ) { + moveFocusToTop( scrollPoint, focusableSelector ); + } +}; + +/** + * HOC that provides a function to scroll to the top of the component. + */ +const withScrollToTop = ( + OriginalComponent: React.FunctionComponent< Record< string, unknown > > +) => { + return ( props: Record< string, unknown > ): JSX.Element => { + const scrollPointRef = useRef< HTMLDivElement >( null ); + const scrollToTop = ( args: ScrollToTopProps ) => { + if ( scrollPointRef.current !== null ) { + scrollToHTMLElement( scrollPointRef.current, args ); + } + }; + return ( + <> +
+ + + ); + }; +}; + +export default withScrollToTop; diff --git a/assets/js/blocks/cart-checkout/checkout-i2/attributes.ts b/assets/js/blocks/cart-checkout/checkout-i2/attributes.ts new file mode 100644 index 00000000000..d6caf35dc0b --- /dev/null +++ b/assets/js/blocks/cart-checkout/checkout-i2/attributes.ts @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { getSetting } from '@woocommerce/settings'; + +export const blockName = 'woocommerce/checkout-i2'; +export const blockAttributes = { + isPreview: { + type: 'boolean', + default: false, + save: false, + }, + hasDarkControls: { + type: 'boolean', + default: getSetting( 'hasDarkEditorStyleSupport', false ), + }, + showCompanyField: { + type: 'boolean', + default: false, + }, + requireCompanyField: { + type: 'boolean', + default: false, + }, + allowCreateAccount: { + type: 'boolean', + default: false, + }, + showApartmentField: { + type: 'boolean', + default: true, + }, + showPhoneField: { + type: 'boolean', + default: true, + }, + requirePhoneField: { + type: 'boolean', + default: false, + }, + // Deprecated - here for v1 migration support + showOrderNotes: { + type: 'boolean', + default: true, + }, + showPolicyLinks: { + type: 'boolean', + default: true, + }, + showReturnToCart: { + type: 'boolean', + default: true, + }, + cartPageId: { + type: 'number', + default: 0, + }, + showRateAfterTaxName: { + type: 'boolean', + default: getSetting( 'displayCartPricesIncludingTax', false ), + }, +}; diff --git a/assets/js/blocks/cart-checkout/checkout-i2/block.tsx b/assets/js/blocks/cart-checkout/checkout-i2/block.tsx new file mode 100644 index 00000000000..f1a9e992031 --- /dev/null +++ b/assets/js/blocks/cart-checkout/checkout-i2/block.tsx @@ -0,0 +1,181 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { createInterpolateElement, useEffect } from '@wordpress/element'; +import { useStoreCart, useStoreNotices } from '@woocommerce/base-context/hooks'; +import { + useCheckoutContext, + useValidationContext, + ValidationContextProvider, + StoreNoticesProvider, + CheckoutProvider, +} from '@woocommerce/base-context'; +import { StoreSnackbarNoticesProvider } from '@woocommerce/base-context/providers'; +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 { SlotFillProvider } from '@woocommerce/blocks-checkout'; +import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; + +/** + * 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 LoginPrompt = () => { + return ( + <> + { __( + 'You must be logged in to checkout. ', + 'woo-gutenberg-products-block' + ) } + + { __( + 'Click here to log in.', + 'woo-gutenberg-products-block' + ) } + + + ); +}; + +const Checkout = ( { + attributes, + children, +}: { + attributes: Attributes; + children: React.ReactChildren; +} ): JSX.Element => { + const { hasOrder, customerId } = useCheckoutContext(); + const { cartItems, cartIsLoading } = useStoreCart(); + + const { + allowCreateAccount, + showCompanyField, + requireCompanyField, + showApartmentField, + showPhoneField, + requirePhoneField, + } = attributes; + + if ( ! cartIsLoading && cartItems.length === 0 ) { + return ; + } + + if ( ! hasOrder ) { + return ; + } + + if ( + isLoginRequired( customerId ) && + allowCreateAccount && + getSetting( 'checkoutAllowsSignup', false ) + ) { + ; + } + + return ( + + { children } + + ); +}; + +const ScrollOnError = ( { + scrollToTop, +}: { + scrollToTop: ( props: Record< string, unknown > ) => void; +} ): null => { + const { hasNoticesOfType } = useStoreNotices(); + const { + hasError: checkoutHasError, + isIdle: checkoutIsIdle, + } = useCheckoutContext(); + const { + hasValidationErrors, + showAllValidationErrors, + } = useValidationContext(); + + const hasErrorsToDisplay = + checkoutIsIdle && + checkoutHasError && + ( hasValidationErrors || hasNoticesOfType( 'default' ) ); + + useEffect( () => { + if ( hasErrorsToDisplay ) { + showAllValidationErrors(); + scrollToTop( { focusableSelector: 'input:invalid' } ); + } + }, [ hasErrorsToDisplay, scrollToTop, showAllValidationErrors ] ); + + return null; +}; + +const Block = ( { + attributes, + children, + scrollToTop, +}: { + attributes: Attributes; + children: React.ReactChildren; + scrollToTop: ( props: Record< string, unknown > ) => void; +} ): JSX.Element => ( + Try reloading the page. If the error persists, please get in touch with us so we can assist.', + 'woo-gutenberg-products-block' + ), + { + button: ( +