From 01b73d3304d35829809a968db6570c2d4ab015b2 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 16 Nov 2022 15:13:01 +0000 Subject: [PATCH 01/34] Refactor Store Notices Move snackbar hiding filter before notice creation Implements showApplyCouponNotice Refactor context providers Use STORE_NOTICE_CONTEXTS use refs to track notice containers Refactor ref usage Use existing noticeContexts --- assets/js/base/context/event-emit/utils.ts | 4 + .../hooks/cart/use-store-cart-coupons.ts | 10 +- assets/js/base/context/providers/index.js | 1 + .../context/providers/store-notices/index.ts | 3 + .../snackbar-notices-container/index.tsx | 66 ++++++++ .../snackbar-notices-container}/style.scss | 8 + .../snackbar-notices-container/types.ts | 15 ++ .../store-notices/store-notice-context.tsx | 144 ++++++++++++++++++ .../store-notices-container/index.tsx | 94 ++++++++++++ .../store-notices-container/style.scss | 43 ++++++ .../store-notices-container/types.ts | 23 +++ .../store-notices-container/utils.ts | 12 ++ .../context/providers/store-notices/types.ts | 21 +++ .../components/snackbar-notices-container.js | 85 ----------- .../providers/store-snackbar-notices/index.ts | 1 - .../payment-method-error-boundary.js | 4 + assets/js/blocks/cart/block.js | 27 +++- assets/js/blocks/checkout/block.tsx | 40 ++--- .../checkout-payment-block/block.tsx | 11 +- .../checkout-payment-block/frontend.tsx | 1 - .../checkout-shipping-address-block/block.tsx | 5 + .../checkout-shipping-methods-block/block.tsx | 10 +- 22 files changed, 512 insertions(+), 116 deletions(-) create mode 100644 assets/js/base/context/providers/store-notices/index.ts create mode 100644 assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx rename assets/js/base/context/providers/{store-snackbar-notices/components => store-notices/snackbar-notices-container}/style.scss (70%) create mode 100644 assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts create mode 100644 assets/js/base/context/providers/store-notices/store-notice-context.tsx create mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/index.tsx create mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/style.scss create mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/types.ts create mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/utils.ts create mode 100644 assets/js/base/context/providers/store-notices/types.ts delete mode 100644 assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js delete mode 100644 assets/js/base/context/providers/store-snackbar-notices/index.ts diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index 5f5a58c0b64..ae3f37596e1 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -28,6 +28,10 @@ export enum responseTypes { export enum noticeContexts { PAYMENTS = 'wc/payment-area', EXPRESS_PAYMENTS = 'wc/express-payment-area', + CART = 'wc/cart', + CHECKOUT = 'wc/checkout', + SHIPPING_ADDRESS = 'wc/checkout/shippingAddress', + SHIPPING_METHODS = 'wc/checkout/shippingMethods', } export interface ResponseType extends Record< string, unknown > { diff --git a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts index b3ff5960823..f72ffa10753 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts @@ -11,6 +11,7 @@ import { } from '@woocommerce/block-data'; import { decodeEntities } from '@wordpress/html-entities'; import type { StoreCartCoupon } from '@woocommerce/types'; +import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -62,7 +63,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { const applyCouponWithNotices = ( couponCode: string ) => { applyCoupon( couponCode ) .then( ( result ) => { - if ( result === true ) { + if ( + result === true && + __experimentalApplyCheckoutFilter( + 'showApplyCouponNotice', + true, + { couponCode, context } + ) + ) { createNotice( 'info', sprintf( diff --git a/assets/js/base/context/providers/index.js b/assets/js/base/context/providers/index.js index 73e64c7c2cc..865db635957 100644 --- a/assets/js/base/context/providers/index.js +++ b/assets/js/base/context/providers/index.js @@ -2,6 +2,7 @@ export * from './editor-context'; export * from './add-to-cart-form'; export * from './cart-checkout'; export * from './store-snackbar-notices'; +export * from './store-notices'; export * from './container-width-context'; export * from './editor-context'; export * from './query-state-context'; diff --git a/assets/js/base/context/providers/store-notices/index.ts b/assets/js/base/context/providers/store-notices/index.ts new file mode 100644 index 00000000000..b1f06241641 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/index.ts @@ -0,0 +1,3 @@ +export * from './store-notice-context'; +export { default as StoreNoticesContainer } from './store-notices-container'; +export { default as SnackbarNoticesContainer } from './snackbar-notices-container'; diff --git a/assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx b/assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx new file mode 100644 index 00000000000..11ba410c816 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx @@ -0,0 +1,66 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { SnackbarList } from 'wordpress-components'; +import { useSelect, useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import './style.scss'; +import type { + NoticeType, + NoticeOptions, + SnackbarNoticesContainerProps, +} from './types'; +import { useStoreNoticeContext } from '../store-notice-context'; + +const SnackbarNoticesContainer = ( { + className, + forceType = false, + context = 'default', +}: SnackbarNoticesContainerProps ): JSX.Element | null => { + const { suppressNotices } = useStoreNoticeContext(); + const { removeNotice } = useDispatch( 'core/notices' ); + + const notices = useSelect< Array< NoticeType & NoticeOptions > >( + ( select ) => + select( 'core/notices' ).getNotices( context ) as Array< + NoticeType & NoticeOptions + > + ); + + if ( suppressNotices ) { + return null; + } + + return ( + notice.type === 'snackbar' || forceType ) + .map( ( notice ) => { + return { + ...notice, + className: + 'components-snackbar--status-' + notice.status, + }; + } ) } + onRemove={ ( noticeId: string ) => { + notices.forEach( ( notice ) => { + if ( notice.explicitDismiss && notice.id === noticeId ) { + removeNotice( notice.id, context ); + } else { + removeNotice( notice.id, context ); + } + } ); + } } + /> + ); +}; + +export default SnackbarNoticesContainer; diff --git a/assets/js/base/context/providers/store-snackbar-notices/components/style.scss b/assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss similarity index 70% rename from assets/js/base/context/providers/store-snackbar-notices/components/style.scss rename to assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss index e5a419a1810..e1d80e61243 100644 --- a/assets/js/base/context/providers/store-snackbar-notices/components/style.scss +++ b/assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss @@ -17,4 +17,12 @@ margin-right: 10px; } } + + .components-snackbar--status-error { + background-color: #e2401c; + } + + .components-snackbar--status-info { + background-color: #0073aa; + } } diff --git a/assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts b/assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts new file mode 100644 index 00000000000..6ddceca9178 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import type { + Notice as NoticeType, + Options as NoticeOptions, +} from '@wordpress/notices'; + +export interface SnackbarNoticesContainerProps { + className?: string; + context?: string; + forceType?: boolean; +} + +export { NoticeType, NoticeOptions }; diff --git a/assets/js/base/context/providers/store-notices/store-notice-context.tsx b/assets/js/base/context/providers/store-notices/store-notice-context.tsx new file mode 100644 index 00000000000..43ab62c51b2 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/store-notice-context.tsx @@ -0,0 +1,144 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + createContext, + useContext, + useCallback, + useState, +} from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { Button } from '@wordpress/components'; +import type { Options as NoticeOptions } from '@wordpress/notices'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; + +/** + * Internal dependencies + */ +import { StoreNoticeContextType } from './types'; +import { useEditorContext } from '../editor-context'; +import { noticeContexts } from '../../event-emit/utils'; + +const StoreNoticeContext = createContext< StoreNoticeContextType >( { + createNotice: () => void 0, + registerContainer: () => void 0, + suppressNotices: false, +} ); + +export const useStoreNoticeContext = () => { + return useContext( StoreNoticeContext ); +}; + +export const StoreNoticeProvider = ( { + children, + defaultContext, +}: { + children: JSX.Element | JSX.Element[]; + defaultContext: string; +} ): JSX.Element => { + const { createNotice: dispatchCreateNotice } = + useDispatch( 'core/notices' ); + const { isEditor } = useEditorContext(); + const isExpressPaymentMethodActive = useSelect( ( select ) => + select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() + ); + + // Maintains a list of containers that have been registered in the hierarchy. + const [ containerRefs, setContainerRefs ] = useState( {} ); + const registerContainer = useCallback( + ( + containerContext: string, + ref: React.MutableRefObject< HTMLDivElement | null > + ) => { + setContainerRefs( ( current ) => ( { + ...current, + [ containerContext ]: ref, + } ) ); + }, + [ setContainerRefs ] + ); + + /** + * Wrapper for createNotice used to create the correct type of notice based on the provided context. + */ + const createNotice = useCallback< + StoreNoticeContextType[ 'createNotice' ] + >( + ( + status: 'error' | 'warning' | 'info', + message: string, + options: Partial< NoticeOptions > + ) => { + let noticeContext = options?.context || defaultContext; + + // If the container ref was not registered, show the notice in the default context instead so it is visible. + if ( ! Object.keys( containerRefs ).includes( noticeContext ) ) { + noticeContext = defaultContext; + } + + const type = + noticeContext === defaultContext ? 'snackbar' : 'default'; + + dispatchCreateNotice( status, message, { + isDismissible: true, + ...options, + type, + context: noticeContext, + } ); + }, + [ containerRefs, defaultContext, dispatchCreateNotice ] + ); + + const contextData = { + createNotice, + registerContainer, + suppressNotices: isEditor || isExpressPaymentMethodActive, + } as StoreNoticeContextType; + + return ( + + + { Object.values( noticeContexts ).map( ( contextValue ) => { + return ( + + ); + } ) } + { children } + + ); +}; diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/index.tsx b/assets/js/base/context/providers/store-notices/store-notices-container/index.tsx new file mode 100644 index 00000000000..1b11f231ae1 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/store-notices-container/index.tsx @@ -0,0 +1,94 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { useRef, useEffect } from '@wordpress/element'; +import { Notice } from 'wordpress-components'; +import { sanitizeHTML } from '@woocommerce/utils'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { usePrevious } from '@woocommerce/base-hooks'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import './style.scss'; +import { getClassNameFromStatus } from './utils'; +import type { + StoreNoticesContainerProps, + NoticeType, + NoticeOptions, +} from './types'; +import { useStoreNoticeContext } from '../store-notice-context'; + +const StoreNoticesContainer = ( { + className, + context = 'default', + additionalNotices = [], +}: StoreNoticesContainerProps ): JSX.Element | null => { + const ref = useRef< HTMLDivElement >( null ); + const { registerContainer, suppressNotices } = useStoreNoticeContext(); + const { removeNotice } = useDispatch( 'core/notices' ); + + const notices = useSelect< Array< NoticeType & NoticeOptions > >( + ( select ) => + select( 'core/notices' ).getNotices( context ) as Array< + NoticeType & NoticeOptions + > + ) + .filter( ( notice ) => notice.type !== 'snackbar' ) + .concat( additionalNotices ); + + // Register the container with the parent. + useEffect( () => { + registerContainer( context, ref ); + }, [ context, ref, registerContainer ] ); + + // Scroll to container when an error is added here. + const noticeIds = notices.map( ( notice ) => notice.id ); + const previousNoticeIds = usePrevious( noticeIds ); + + useEffect( () => { + const newNoticeIds = noticeIds.filter( + ( value ) => + ! previousNoticeIds || ! previousNoticeIds.includes( value ) + ); + + if ( newNoticeIds.length ) { + ref.current?.scrollIntoView( { + behavior: 'smooth', + } ); + } + }, [ noticeIds, previousNoticeIds, ref ] ); + + if ( suppressNotices ) { + return null; + } + + return ( +
+ { notices.map( ( props ) => ( + { + if ( props.isDismissible ) { + removeNotice( props.id, context ); + } + } } + > + { sanitizeHTML( decodeEntities( props.content ) ) } + + ) ) } +
+ ); +}; + +export default StoreNoticesContainer; diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/style.scss b/assets/js/base/context/providers/store-notices/store-notices-container/style.scss new file mode 100644 index 00000000000..d4b58526489 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/store-notices-container/style.scss @@ -0,0 +1,43 @@ +.wc-block-components-notices { + display: block; + margin-bottom: 2em; + .wc-block-components-notices__notice { + margin: 0; + display: flex; + flex-wrap: nowrap; + a { + text-decoration: underline; + } + .components-notice__dismiss { + background: transparent none; + padding: 0; + margin: 0 0 0 auto; + border: 0; + outline: 0; + color: currentColor; + svg { + fill: currentColor; + vertical-align: text-top; + } + } + .components-notice__content > div:not(.components-notice__actions) { + *:first-child { + margin-top: 0; + } + *:last-child { + margin-bottom: 0; + } + } + } + .wc-block-components-notices__notice + .wc-block-components-notices__notice { + margin-top: 1em; + } +} + +// @todo Either move notice style fixes to Woo core, or take full control over notice component styling in blocks. +.theme-twentytwentyone, +.theme-twentytwenty { + .wc-block-components-notices__notice { + padding: 1.5rem 3rem; + } +} diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/types.ts b/assets/js/base/context/providers/store-notices/store-notices-container/types.ts new file mode 100644 index 00000000000..5eafe6de196 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/store-notices-container/types.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import type { + Notice as NoticeType, + Options as NoticeOptions, +} from '@wordpress/notices'; + +export interface StoreNotice { + content: string; + id: string; + status: string; + isDismissible?: boolean; + type: 'default' | 'snackbar'; +} + +export interface StoreNoticesContainerProps { + className?: string; + context?: string; + additionalNotices?: ( NoticeType & NoticeOptions )[]; +} + +export { NoticeType, NoticeOptions }; diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/utils.ts b/assets/js/base/context/providers/store-notices/store-notices-container/utils.ts new file mode 100644 index 00000000000..e7706a9fe14 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/store-notices-container/utils.ts @@ -0,0 +1,12 @@ +export const getClassNameFromStatus = ( { status = 'default' } ): string => { + switch ( status ) { + case 'error': + return 'woocommerce-error'; + case 'success': + return 'woocommerce-message'; + case 'info': + case 'warning': + return 'woocommerce-info'; + } + return ''; +}; diff --git a/assets/js/base/context/providers/store-notices/types.ts b/assets/js/base/context/providers/store-notices/types.ts new file mode 100644 index 00000000000..4570e9470c8 --- /dev/null +++ b/assets/js/base/context/providers/store-notices/types.ts @@ -0,0 +1,21 @@ +/** + * External dependencies + */ +import type { Options as NoticeOptions } from '@wordpress/notices'; + +export type StoreNoticeContextType = { + createNotice: ( + status: 'error' | 'warning' | 'info', + message: string, + options: Partial< + Omit< NoticeOptions, 'context' > & { + context: string; + } + > + ) => void; + registerContainer: ( + context: string, + ref: React.MutableRefObject< HTMLDivElement | null > + ) => void; + suppressNotices: boolean; +}; diff --git a/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js b/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js deleted file mode 100644 index 87ee473c504..00000000000 --- a/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * External dependencies - */ -import PropTypes from 'prop-types'; -import { SnackbarList } from 'wordpress-components'; -import classnames from 'classnames'; -import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout'; -import { useDispatch, useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { useEditorContext } from '../../editor-context'; - -const EMPTY_SNACKBAR_NOTICES = {}; - -export const SnackbarNoticesContainer = ( { - className, - context = 'default', -} ) => { - const { isEditor } = useEditorContext(); - - const { notices } = useSelect( ( select ) => { - const store = select( 'core/notices' ); - return { - notices: store.getNotices( context ), - }; - } ); - const { removeNotice } = useDispatch( 'core/notices' ); - - if ( isEditor ) { - return null; - } - - const snackbarNotices = notices.filter( - ( notice ) => notice.type === 'snackbar' - ); - - const noticeVisibility = - snackbarNotices.length > 0 - ? snackbarNotices.reduce( ( acc, { content } ) => { - acc[ content ] = true; - return acc; - }, {} ) - : EMPTY_SNACKBAR_NOTICES; - - const filteredNotices = __experimentalApplyCheckoutFilter( { - filterName: 'snackbarNoticeVisibility', - defaultValue: noticeVisibility, - } ); - - const visibleNotices = snackbarNotices.filter( - ( notice ) => filteredNotices[ notice.content ] === true - ); - - const wrapperClass = classnames( - className, - 'wc-block-components-notices__snackbar' - ); - - return ( - { - visibleNotices.forEach( ( notice ) => - removeNotice( notice.id, context ) - ); - } } - /> - ); -}; - -SnackbarNoticesContainer.propTypes = { - className: PropTypes.string, - notices: PropTypes.arrayOf( - PropTypes.shape( { - content: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - isDismissible: PropTypes.bool, - type: PropTypes.oneOf( [ 'default', 'snackbar' ] ), - } ) - ), -}; diff --git a/assets/js/base/context/providers/store-snackbar-notices/index.ts b/assets/js/base/context/providers/store-snackbar-notices/index.ts deleted file mode 100644 index 05f01b11123..00000000000 --- a/assets/js/base/context/providers/store-snackbar-notices/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './components/snackbar-notices-container'; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js index ea2904e7f49..1eb191a9e37 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js @@ -7,6 +7,10 @@ import PropTypes from 'prop-types'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; import { noticeContexts } from '@woocommerce/base-context/hooks'; +import { + noticeContexts, + StoreNoticesContainer, +} from '@woocommerce/base-context'; class PaymentMethodErrorBoundary extends Component { state = { errorMessage: '', hasError: false }; diff --git a/assets/js/blocks/cart/block.js b/assets/js/blocks/cart/block.js index 42459b213a4..bf121db0d77 100644 --- a/assets/js/blocks/cart/block.js +++ b/assets/js/blocks/cart/block.js @@ -6,6 +6,12 @@ import { useStoreCart } from '@woocommerce/base-context/hooks'; import { useEffect } from '@wordpress/element'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { SnackbarNoticesContainer } from '@woocommerce/base-context'; +import { + noticeContexts, + StoreNoticeProvider, + SnackbarNoticesContainer, + CartProvider, +} from '@woocommerce/base-context'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import { translateJQueryEventToNative } from '@woocommerce/base-utils'; @@ -15,6 +21,7 @@ import { SlotFillProvider, StoreNoticesContainer, } from '@woocommerce/blocks-checkout'; +import { SlotFillProvider } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -83,14 +90,18 @@ const Block = ( { attributes, children, scrollToTop } ) => ( } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - - - - - { children } - - - + + + + + { children } + + + + ); export default withScrollToTop( Block ); diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 1e0bca0df2b..a3795b02d37 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -7,6 +7,8 @@ import { createInterpolateElement, useEffect } from '@wordpress/element'; import { useStoreCart } from '@woocommerce/base-context/hooks'; import { CheckoutProvider, + noticeContexts, + StoreNoticeProvider, SnackbarNoticesContainer, } from '@woocommerce/base-context'; @@ -186,23 +188,27 @@ const Block = ( { ) } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - - - { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } - - - - - { children } - - - - - + + + { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } + + + + + { children } + + + + + + ); }; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx index de7f2979e4f..ef0616c7b18 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx @@ -1,6 +1,10 @@ /** * External dependencies */ +import { + StoreNoticesContainer, + noticeContexts, +} from '@woocommerce/base-context'; /** * Internal dependencies @@ -8,7 +12,12 @@ import { PaymentMethods } from '../../../cart-checkout-shared/payment-methods'; const Block = (): JSX.Element | null => { - return ; + return ( + <> + + + + ); }; export default Block; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx index 1da7d62da9e..8c60ecdc89e 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx @@ -49,7 +49,6 @@ const FrontendBlock = ( { description={ description } showStepNumber={ showStepNumber } > - { children } diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx index 5e8bd4c5496..8f3e3b6a158 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx @@ -8,6 +8,8 @@ import { useCheckoutAddress, useStoreEvents, useEditorContext, + StoreNoticesContainer, + noticeContexts, } from '@woocommerce/base-context'; import { CheckboxControl } from '@woocommerce/blocks-checkout'; import Noninteractive from '@woocommerce/base-components/noninteractive'; @@ -96,6 +98,9 @@ const Block = ( { return ( <> + { const { isEditor } = useEditorContext(); - const { shippingRates, needsShipping, @@ -80,6 +83,9 @@ const Block = (): JSX.Element | null => { return ( <> + { isEditor && ! shippingRatesPackageCount ? ( ) : ( From 07bac08dc2429335e0f886abd2b154c6f456f0d7 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Thu, 17 Nov 2022 16:13:58 +0000 Subject: [PATCH 02/34] Move new notice code to checkout package --- assets/js/base/context/providers/index.js | 2 - .../payment-method-error-boundary.js | 6 +- assets/js/blocks/cart/block.js | 34 ++---- assets/js/blocks/checkout/block.tsx | 63 +++++----- .../checkout-payment-block/block.tsx | 6 +- .../checkout-payment-block/frontend.tsx | 2 - .../checkout-shipping-address-block/block.tsx | 6 +- .../checkout-shipping-methods-block/block.tsx | 7 +- assets/js/data/index.ts | 1 + .../js/data/store-notice-containers/index.ts | 38 ++++++ packages/checkout/components/index.js | 1 + .../snackbar-notices-container/index.tsx | 68 +++++++++++ .../snackbar-notices-container/style.scss | 28 +++++ .../snackbar-notices-container/types.ts | 15 +++ .../store-notices-container/index.tsx | 108 +++++++++--------- .../store-notices-container/types.ts | 23 ++++ .../store-notices-container/utils.ts | 12 ++ 17 files changed, 299 insertions(+), 121 deletions(-) create mode 100644 assets/js/data/store-notice-containers/index.ts create mode 100644 packages/checkout/components/snackbar-notices-container/index.tsx create mode 100644 packages/checkout/components/snackbar-notices-container/style.scss create mode 100644 packages/checkout/components/snackbar-notices-container/types.ts create mode 100644 packages/checkout/components/store-notices-container/types.ts create mode 100644 packages/checkout/components/store-notices-container/utils.ts diff --git a/assets/js/base/context/providers/index.js b/assets/js/base/context/providers/index.js index 865db635957..d02577e0b64 100644 --- a/assets/js/base/context/providers/index.js +++ b/assets/js/base/context/providers/index.js @@ -1,8 +1,6 @@ export * from './editor-context'; export * from './add-to-cart-form'; export * from './cart-checkout'; -export * from './store-snackbar-notices'; -export * from './store-notices'; export * from './container-width-context'; export * from './editor-context'; export * from './query-state-context'; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js index 1eb191a9e37..42f90ed26d3 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js @@ -6,11 +6,7 @@ import { Component } from 'react'; import PropTypes from 'prop-types'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; -import { noticeContexts } from '@woocommerce/base-context/hooks'; -import { - noticeContexts, - StoreNoticesContainer, -} from '@woocommerce/base-context'; +import { noticeContexts } from '@woocommerce/base-context'; class PaymentMethodErrorBoundary extends Component { state = { errorMessage: '', hasError: false }; diff --git a/assets/js/blocks/cart/block.js b/assets/js/blocks/cart/block.js index bf121db0d77..b567e501d44 100644 --- a/assets/js/blocks/cart/block.js +++ b/assets/js/blocks/cart/block.js @@ -5,23 +5,15 @@ import { __ } from '@wordpress/i18n'; import { useStoreCart } from '@woocommerce/base-context/hooks'; import { useEffect } from '@wordpress/element'; import LoadingMask from '@woocommerce/base-components/loading-mask'; -import { SnackbarNoticesContainer } from '@woocommerce/base-context'; -import { - noticeContexts, - StoreNoticeProvider, - SnackbarNoticesContainer, - CartProvider, -} from '@woocommerce/base-context'; +import { noticeContexts, CartProvider } from '@woocommerce/base-context'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import { translateJQueryEventToNative } from '@woocommerce/base-utils'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; -import { CartProvider } from '@woocommerce/base-context/providers'; import { SlotFillProvider, - StoreNoticesContainer, + SnackbarNoticesContainer, } from '@woocommerce/blocks-checkout'; -import { SlotFillProvider } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -90,18 +82,16 @@ const Block = ( { attributes, children, scrollToTop } ) => ( } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - - - - - { children } - - - - + + + + { children } + + + ); export default withScrollToTop( Block ); diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index a3795b02d37..82e60247e8b 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -5,19 +5,14 @@ import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; import { createInterpolateElement, useEffect } from '@wordpress/element'; import { useStoreCart } from '@woocommerce/base-context/hooks'; -import { - CheckoutProvider, - noticeContexts, - StoreNoticeProvider, - SnackbarNoticesContainer, -} from '@woocommerce/base-context'; +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 { SlotFillProvider, - StoreNoticesContainer, + SnackbarNoticesContainer, } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -25,6 +20,7 @@ import { CHECKOUT_STORE_KEY, VALIDATION_STORE_KEY, } from '@woocommerce/block-data'; +import { Button } from '@wordpress/components'; /** * Internal dependencies @@ -188,27 +184,38 @@ const Block = ( { ) } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - - - { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } - - - - - { children } - - - - - - + + + { Object.values( noticeContexts ).map( ( contextValue ) => { + return ( + + ); + } ) } + { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } + + + + + { children } + + + + + ); }; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx index ef0616c7b18..1794550ad28 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx @@ -1,10 +1,8 @@ /** * External dependencies */ -import { - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/base-context'; +import { noticeContexts } from '@woocommerce/base-context'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; /** * Internal dependencies diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx index 8c60ecdc89e..b750bcb4718 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/frontend.tsx @@ -7,14 +7,12 @@ import { withFilteredAttributes } from '@woocommerce/shared-hocs'; import { FormStep } from '@woocommerce/base-components/cart-checkout'; import { useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY } from '@woocommerce/block-data'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import Block from './block'; import attributes from './attributes'; -import { noticeContexts } from '../../../../base/context/event-emit'; const FrontendBlock = ( { title, diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx index 8f3e3b6a158..528bfdb10f5 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx @@ -8,10 +8,12 @@ import { useCheckoutAddress, useStoreEvents, useEditorContext, - StoreNoticesContainer, noticeContexts, } from '@woocommerce/base-context'; -import { CheckboxControl } from '@woocommerce/blocks-checkout'; +import { + CheckboxControl, + StoreNoticesContainer, +} from '@woocommerce/blocks-checkout'; import Noninteractive from '@woocommerce/base-components/noninteractive'; import type { BillingAddress, diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx index 0e5853f36ee..a26e4606514 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx @@ -7,11 +7,8 @@ import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout import { getShippingRatesPackageCount } from '@woocommerce/base-utils'; import { getCurrencyFromPriceResponse } from '@woocommerce/price-format'; import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; -import { - useEditorContext, - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/base-context'; +import { useEditorContext, noticeContexts } from '@woocommerce/base-context'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; import { decodeEntities } from '@wordpress/html-entities'; import { Notice } from 'wordpress-components'; import classnames from 'classnames'; diff --git a/assets/js/data/index.ts b/assets/js/data/index.ts index ce459998a6e..2f0e0b07488 100644 --- a/assets/js/data/index.ts +++ b/assets/js/data/index.ts @@ -13,5 +13,6 @@ export { CHECKOUT_STORE_KEY } from './checkout'; export { PAYMENT_STORE_KEY } from './payment'; export { VALIDATION_STORE_KEY } from './validation'; export { QUERY_STATE_STORE_KEY } from './query-state'; +export { STORE_NOTICE_CONTAINERS_STORE_KEY } from './store-notice-containers'; export * from './constants'; export * from './types'; diff --git a/assets/js/data/store-notice-containers/index.ts b/assets/js/data/store-notice-containers/index.ts new file mode 100644 index 00000000000..dfadf978c46 --- /dev/null +++ b/assets/js/data/store-notice-containers/index.ts @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; + +export const STORE_KEY = 'wc/store/notice-containers'; + +const store = createReduxStore( STORE_KEY, { + reducer( state = {}, action ) { + switch ( action.type ) { + case 'REGISTER_CONTAINER': + return { + ...state, + [ action.containerContext ]: action.ref, + }; + } + return state; + }, + actions: { + registerContainer( + containerContext: string, + ref: React.MutableRefObject< HTMLDivElement | null > + ) { + return { + type: 'REGISTER_CONTAINER', + containerContext, + ref, + }; + }, + }, + selectors: { + getContainers: ( state ) => state, + }, +} ); + +register( store ); + +export const STORE_NOTICE_CONTAINERS_STORE_KEY = STORE_KEY; diff --git a/packages/checkout/components/index.js b/packages/checkout/components/index.js index 42aa464f0d2..9887b6d5215 100644 --- a/packages/checkout/components/index.js +++ b/packages/checkout/components/index.js @@ -7,6 +7,7 @@ export { default as Panel } from './panel'; export { default as Button } from './button'; export { default as Label } from './label'; export { default as StoreNoticesContainer } from './store-notices-container'; +export { default as SnackbarNoticesContainer } from './snackbar-notices-container'; export { default as CheckboxControl } from './checkbox-control'; export { default as ValidatedTextInput } from './text-input/validated-text-input'; export { default as TextInput } from './text-input/text-input'; diff --git a/packages/checkout/components/snackbar-notices-container/index.tsx b/packages/checkout/components/snackbar-notices-container/index.tsx new file mode 100644 index 00000000000..c0ad49a74a8 --- /dev/null +++ b/packages/checkout/components/snackbar-notices-container/index.tsx @@ -0,0 +1,68 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { SnackbarList } from 'wordpress-components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; + +/** + * Internal dependencies + */ +import './style.scss'; +import type { + NoticeType, + NoticeOptions, + SnackbarNoticesContainerProps, +} from './types'; + +const SnackbarNoticesContainer = ( { + className, + forceType = false, + context = 'default', +}: SnackbarNoticesContainerProps ): JSX.Element | null => { + const suppressNotices = useSelect( ( select ) => + select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() + ); + const { removeNotice } = useDispatch( 'core/notices' ); + + const notices = useSelect< Array< NoticeType & NoticeOptions > >( + ( select ) => + select( 'core/notices' ).getNotices( context ) as Array< + NoticeType & NoticeOptions + > + ); + + if ( suppressNotices ) { + return null; + } + + return ( + notice.type === 'snackbar' || forceType ) + .map( ( notice ) => { + return { + ...notice, + className: + 'components-snackbar--status-' + notice.status, + }; + } ) } + onRemove={ ( noticeId: string ) => { + notices.forEach( ( notice ) => { + if ( notice.explicitDismiss && notice.id === noticeId ) { + removeNotice( notice.id, context ); + } else { + removeNotice( notice.id, context ); + } + } ); + } } + /> + ); +}; + +export default SnackbarNoticesContainer; diff --git a/packages/checkout/components/snackbar-notices-container/style.scss b/packages/checkout/components/snackbar-notices-container/style.scss new file mode 100644 index 00000000000..e1d80e61243 --- /dev/null +++ b/packages/checkout/components/snackbar-notices-container/style.scss @@ -0,0 +1,28 @@ +.wc-block-components-notices__snackbar { + position: fixed; + bottom: 20px; + left: 16px; + width: auto; + + @include breakpoint("<782px") { + position: fixed; + top: 10px; + left: 0; + bottom: auto; + } + + .components-snackbar-list__notice-container { + @include breakpoint("<782px") { + margin-left: 10px; + margin-right: 10px; + } + } + + .components-snackbar--status-error { + background-color: #e2401c; + } + + .components-snackbar--status-info { + background-color: #0073aa; + } +} diff --git a/packages/checkout/components/snackbar-notices-container/types.ts b/packages/checkout/components/snackbar-notices-container/types.ts new file mode 100644 index 00000000000..6ddceca9178 --- /dev/null +++ b/packages/checkout/components/snackbar-notices-container/types.ts @@ -0,0 +1,15 @@ +/** + * External dependencies + */ +import type { + Notice as NoticeType, + Options as NoticeOptions, +} from '@wordpress/notices'; + +export interface SnackbarNoticesContainerProps { + className?: string; + context?: string; + forceType?: boolean; +} + +export { NoticeType, NoticeOptions }; diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index 12faf4790cd..8f46379b27c 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -2,83 +2,89 @@ * External dependencies */ import classnames from 'classnames'; +import { useRef, useEffect } from '@wordpress/element'; import { Notice } from 'wordpress-components'; import { sanitizeHTML } from '@woocommerce/utils'; import { useDispatch, useSelect } from '@wordpress/data'; -import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; -import type { Notice as NoticeType } from '@wordpress/notices'; +import { usePrevious } from '@woocommerce/base-hooks'; +import { decodeEntities } from '@wordpress/html-entities'; +import { + STORE_NOTICE_CONTAINERS_STORE_KEY, + PAYMENT_STORE_KEY, +} from '@woocommerce/block-data'; /** * Internal dependencies */ import './style.scss'; +import { getClassNameFromStatus } from './utils'; +import type { + StoreNoticesContainerProps, + NoticeType, + NoticeOptions, +} from './types'; -const getWooClassName = ( { status = 'default' } ) => { - switch ( status ) { - case 'error': - return 'woocommerce-error'; - case 'success': - return 'woocommerce-message'; - case 'info': - case 'warning': - return 'woocommerce-info'; - } - return ''; -}; - -interface StoreNoticesContainerProps { - className?: string; - context?: string; - additionalNotices?: NoticeType[]; -} - -/** - * Component that displays notices from the core/notices data store. See - * https://developer.wordpress.org/block-editor/reference-guides/data/data-core-notices/ for more information on this - * data store. - * - * @param props - * @param props.className Class name to add to the container. - * @param props.context Context to show notices from. - * @param props.additionalNotices Additional notices to display. - * @function Object() { [native code] } - */ -export const StoreNoticesContainer = ( { +const StoreNoticesContainer = ( { className, context = 'default', additionalNotices = [], }: StoreNoticesContainerProps ): JSX.Element | null => { - const isExpressPaymentMethodActive = useSelect( ( select ) => + const ref = useRef< HTMLDivElement >( null ); + const { registerContainer } = useDispatch( + STORE_NOTICE_CONTAINERS_STORE_KEY + ); + const { removeNotice } = useDispatch( 'core/notices' ); + const suppressNotices = useSelect( ( select ) => select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() ); - const { notices } = useSelect( ( select ) => { - const store = select( 'core/notices' ); - return { - notices: store.getNotices( context ), - }; - } ); - const { removeNotice } = useDispatch( 'core/notices' ); - const regularNotices = notices + const notices = useSelect< Array< NoticeType & NoticeOptions > >( + ( select ) => + select( 'core/notices' ).getNotices( context ) as Array< + NoticeType & NoticeOptions + > + ) .filter( ( notice ) => notice.type !== 'snackbar' ) .concat( additionalNotices ); - if ( ! regularNotices.length ) { + // Register the container with the parent. + useEffect( () => { + registerContainer( context, ref ); + }, [ context, ref, registerContainer ] ); + + // Scroll to container when an error is added here. + const noticeIds = notices.map( ( notice ) => notice.id ); + const previousNoticeIds = usePrevious( noticeIds ); + + useEffect( () => { + const newNoticeIds = noticeIds.filter( + ( value ) => + ! previousNoticeIds || ! previousNoticeIds.includes( value ) + ); + + if ( newNoticeIds.length ) { + ref.current?.scrollIntoView( { + behavior: 'smooth', + } ); + } + }, [ noticeIds, previousNoticeIds, ref ] ); + + if ( suppressNotices ) { return null; } - const wrapperClass = classnames( className, 'wc-block-components-notices' ); - - // We suppress the notices when the express payment method is active - return isExpressPaymentMethodActive ? null : ( -
- { regularNotices.map( ( props ) => ( + return ( +
+ { notices.map( ( props ) => ( { if ( props.isDismissible ) { @@ -86,7 +92,7 @@ export const StoreNoticesContainer = ( { } } } > - { sanitizeHTML( props.content ) } + { sanitizeHTML( decodeEntities( props.content ) ) } ) ) }
diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts new file mode 100644 index 00000000000..5eafe6de196 --- /dev/null +++ b/packages/checkout/components/store-notices-container/types.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import type { + Notice as NoticeType, + Options as NoticeOptions, +} from '@wordpress/notices'; + +export interface StoreNotice { + content: string; + id: string; + status: string; + isDismissible?: boolean; + type: 'default' | 'snackbar'; +} + +export interface StoreNoticesContainerProps { + className?: string; + context?: string; + additionalNotices?: ( NoticeType & NoticeOptions )[]; +} + +export { NoticeType, NoticeOptions }; diff --git a/packages/checkout/components/store-notices-container/utils.ts b/packages/checkout/components/store-notices-container/utils.ts new file mode 100644 index 00000000000..e7706a9fe14 --- /dev/null +++ b/packages/checkout/components/store-notices-container/utils.ts @@ -0,0 +1,12 @@ +export const getClassNameFromStatus = ( { status = 'default' } ): string => { + switch ( status ) { + case 'error': + return 'woocommerce-error'; + case 'success': + return 'woocommerce-message'; + case 'info': + case 'warning': + return 'woocommerce-info'; + } + return ''; +}; From 926976e3b845f614c6a576a8ae255dc74f75347f Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 08:51:32 +0000 Subject: [PATCH 03/34] Combine store and snackbars --- assets/js/blocks/checkout/block.tsx | 41 +++++++-- .../js/data/store-notice-containers/index.ts | 5 +- packages/checkout/components/index.js | 1 - .../snackbar-notices-container/index.tsx | 68 --------------- .../snackbar-notices-container/style.scss | 28 ------ .../snackbar-notices-container/types.ts | 15 ---- .../store-notices-container/index.tsx | 87 +++++-------------- .../snackbar-notices.tsx | 49 +++++++++++ .../store-notices-container/store-notices.tsx | 82 +++++++++++++++++ .../store-notices-container/style.scss | 29 +++++++ packages/checkout/utils/create-notice.ts | 56 ++++++++++++ packages/checkout/utils/index.js | 1 + 12 files changed, 277 insertions(+), 185 deletions(-) delete mode 100644 packages/checkout/components/snackbar-notices-container/index.tsx delete mode 100644 packages/checkout/components/snackbar-notices-container/style.scss delete mode 100644 packages/checkout/components/snackbar-notices-container/types.ts create mode 100644 packages/checkout/components/store-notices-container/snackbar-notices.tsx create mode 100644 packages/checkout/components/store-notices-container/store-notices.tsx create mode 100644 packages/checkout/utils/create-notice.ts diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 82e60247e8b..3193bb8d9c7 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -12,7 +12,8 @@ import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout'; import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings'; import { SlotFillProvider, - SnackbarNoticesContainer, + StoreNoticesContainer, + createNotice, } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -184,19 +185,45 @@ const Block = ( { ) } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - - + { Object.values( noticeContexts ).map( ( contextValue ) => { return ( - ); diff --git a/assets/js/data/store-notice-containers/index.ts b/assets/js/data/store-notice-containers/index.ts index dfadf978c46..fa89367df1e 100644 --- a/assets/js/data/store-notice-containers/index.ts +++ b/assets/js/data/store-notice-containers/index.ts @@ -29,7 +29,10 @@ const store = createReduxStore( STORE_KEY, { }, }, selectors: { - getContainers: ( state ) => state, + getContainers: ( + state + ): Record< string, React.MutableRefObject< HTMLDivElement | null > > => + state, }, } ); diff --git a/packages/checkout/components/index.js b/packages/checkout/components/index.js index 9887b6d5215..42aa464f0d2 100644 --- a/packages/checkout/components/index.js +++ b/packages/checkout/components/index.js @@ -7,7 +7,6 @@ export { default as Panel } from './panel'; export { default as Button } from './button'; export { default as Label } from './label'; export { default as StoreNoticesContainer } from './store-notices-container'; -export { default as SnackbarNoticesContainer } from './snackbar-notices-container'; export { default as CheckboxControl } from './checkbox-control'; export { default as ValidatedTextInput } from './text-input/validated-text-input'; export { default as TextInput } from './text-input/text-input'; diff --git a/packages/checkout/components/snackbar-notices-container/index.tsx b/packages/checkout/components/snackbar-notices-container/index.tsx deleted file mode 100644 index c0ad49a74a8..00000000000 --- a/packages/checkout/components/snackbar-notices-container/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { SnackbarList } from 'wordpress-components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; - -/** - * Internal dependencies - */ -import './style.scss'; -import type { - NoticeType, - NoticeOptions, - SnackbarNoticesContainerProps, -} from './types'; - -const SnackbarNoticesContainer = ( { - className, - forceType = false, - context = 'default', -}: SnackbarNoticesContainerProps ): JSX.Element | null => { - const suppressNotices = useSelect( ( select ) => - select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() - ); - const { removeNotice } = useDispatch( 'core/notices' ); - - const notices = useSelect< Array< NoticeType & NoticeOptions > >( - ( select ) => - select( 'core/notices' ).getNotices( context ) as Array< - NoticeType & NoticeOptions - > - ); - - if ( suppressNotices ) { - return null; - } - - return ( - notice.type === 'snackbar' || forceType ) - .map( ( notice ) => { - return { - ...notice, - className: - 'components-snackbar--status-' + notice.status, - }; - } ) } - onRemove={ ( noticeId: string ) => { - notices.forEach( ( notice ) => { - if ( notice.explicitDismiss && notice.id === noticeId ) { - removeNotice( notice.id, context ); - } else { - removeNotice( notice.id, context ); - } - } ); - } } - /> - ); -}; - -export default SnackbarNoticesContainer; diff --git a/packages/checkout/components/snackbar-notices-container/style.scss b/packages/checkout/components/snackbar-notices-container/style.scss deleted file mode 100644 index e1d80e61243..00000000000 --- a/packages/checkout/components/snackbar-notices-container/style.scss +++ /dev/null @@ -1,28 +0,0 @@ -.wc-block-components-notices__snackbar { - position: fixed; - bottom: 20px; - left: 16px; - width: auto; - - @include breakpoint("<782px") { - position: fixed; - top: 10px; - left: 0; - bottom: auto; - } - - .components-snackbar-list__notice-container { - @include breakpoint("<782px") { - margin-left: 10px; - margin-right: 10px; - } - } - - .components-snackbar--status-error { - background-color: #e2401c; - } - - .components-snackbar--status-info { - background-color: #0073aa; - } -} diff --git a/packages/checkout/components/snackbar-notices-container/types.ts b/packages/checkout/components/snackbar-notices-container/types.ts deleted file mode 100644 index 6ddceca9178..00000000000 --- a/packages/checkout/components/snackbar-notices-container/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import type { - Notice as NoticeType, - Options as NoticeOptions, -} from '@wordpress/notices'; - -export interface SnackbarNoticesContainerProps { - className?: string; - context?: string; - forceType?: boolean; -} - -export { NoticeType, NoticeOptions }; diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index 8f46379b27c..fdd468117fe 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -1,23 +1,15 @@ /** * External dependencies */ -import classnames from 'classnames'; -import { useRef, useEffect } from '@wordpress/element'; -import { Notice } from 'wordpress-components'; -import { sanitizeHTML } from '@woocommerce/utils'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { usePrevious } from '@woocommerce/base-hooks'; -import { decodeEntities } from '@wordpress/html-entities'; -import { - STORE_NOTICE_CONTAINERS_STORE_KEY, - PAYMENT_STORE_KEY, -} from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; +import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies */ import './style.scss'; -import { getClassNameFromStatus } from './utils'; +import StoreNotices from './store-notices'; +import SnackbarNotices from './snackbar-notices'; import type { StoreNoticesContainerProps, NoticeType, @@ -25,15 +17,10 @@ import type { } from './types'; const StoreNoticesContainer = ( { - className, + className = '', context = 'default', additionalNotices = [], }: StoreNoticesContainerProps ): JSX.Element | null => { - const ref = useRef< HTMLDivElement >( null ); - const { registerContainer } = useDispatch( - STORE_NOTICE_CONTAINERS_STORE_KEY - ); - const { removeNotice } = useDispatch( 'core/notices' ); const suppressNotices = useSelect( ( select ) => select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() ); @@ -43,59 +30,29 @@ const StoreNoticesContainer = ( { select( 'core/notices' ).getNotices( context ) as Array< NoticeType & NoticeOptions > - ) - .filter( ( notice ) => notice.type !== 'snackbar' ) - .concat( additionalNotices ); - - // Register the container with the parent. - useEffect( () => { - registerContainer( context, ref ); - }, [ context, ref, registerContainer ] ); - - // Scroll to container when an error is added here. - const noticeIds = notices.map( ( notice ) => notice.id ); - const previousNoticeIds = usePrevious( noticeIds ); - - useEffect( () => { - const newNoticeIds = noticeIds.filter( - ( value ) => - ! previousNoticeIds || ! previousNoticeIds.includes( value ) - ); - - if ( newNoticeIds.length ) { - ref.current?.scrollIntoView( { - behavior: 'smooth', - } ); - } - }, [ noticeIds, previousNoticeIds, ref ] ); + ); if ( suppressNotices ) { return null; } return ( -
- { notices.map( ( props ) => ( - { - if ( props.isDismissible ) { - removeNotice( props.id, context ); - } - } } - > - { sanitizeHTML( decodeEntities( props.content ) ) } - - ) ) } -
+ <> + notice.type !== 'snackbar' ) + .concat( additionalNotices ) } + /> + notice.type === 'snackbar' + ) } + /> + ); }; diff --git a/packages/checkout/components/store-notices-container/snackbar-notices.tsx b/packages/checkout/components/store-notices-container/snackbar-notices.tsx new file mode 100644 index 00000000000..7f2c7c6f1c3 --- /dev/null +++ b/packages/checkout/components/store-notices-container/snackbar-notices.tsx @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { SnackbarList } from 'wordpress-components'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import type { NoticeType, NoticeOptions } from './types'; + +const SnackbarNotices = ( { + className, + context, + notices, +}: { + context: string; + className: string; + notices: Array< NoticeType & NoticeOptions >; +} ): JSX.Element => { + const { removeNotice } = useDispatch( 'core/notices' ); + + return ( + { + return { + ...notice, + className: 'components-snackbar--status-' + notice.status, + }; + } ) } + onRemove={ ( noticeId: string ) => { + notices.forEach( ( notice ) => { + if ( notice.explicitDismiss && notice.id === noticeId ) { + removeNotice( notice.id, context ); + } else { + removeNotice( notice.id, context ); + } + } ); + } } + /> + ); +}; + +export default SnackbarNotices; diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx new file mode 100644 index 00000000000..1fbeffadb58 --- /dev/null +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -0,0 +1,82 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { useRef, useEffect } from '@wordpress/element'; +import { Notice } from 'wordpress-components'; +import { sanitizeHTML } from '@woocommerce/utils'; +import { useDispatch } from '@wordpress/data'; +import { usePrevious } from '@woocommerce/base-hooks'; +import { decodeEntities } from '@wordpress/html-entities'; +import { STORE_NOTICE_CONTAINERS_STORE_KEY } from '@woocommerce/block-data'; + +/** + * Internal dependencies + */ +import { getClassNameFromStatus } from './utils'; +import type { NoticeType, NoticeOptions } from './types'; + +const StoreNotices = ( { + context, + className, + notices, +}: { + context: string; + className: string; + notices: Array< NoticeType & NoticeOptions >; +} ): JSX.Element => { + const { removeNotice } = useDispatch( 'core/notices' ); + const ref = useRef< HTMLDivElement >( null ); + const { registerContainer } = useDispatch( + STORE_NOTICE_CONTAINERS_STORE_KEY + ); + + // Register the container with the parent. + useEffect( () => { + registerContainer( context, ref ); + }, [ context, ref, registerContainer ] ); + + // Scroll to container when an error is added here. + const noticeIds = notices.map( ( notice ) => notice.id ); + const previousNoticeIds = usePrevious( noticeIds ); + + useEffect( () => { + const newNoticeIds = noticeIds.filter( + ( value ) => + ! previousNoticeIds || ! previousNoticeIds.includes( value ) + ); + + if ( newNoticeIds.length ) { + ref.current?.scrollIntoView( { + behavior: 'smooth', + } ); + } + }, [ noticeIds, previousNoticeIds, ref ] ); + + return ( +
+ { notices.map( ( props ) => ( + { + if ( props.isDismissible ) { + removeNotice( props.id, context ); + } + } } + > + { sanitizeHTML( decodeEntities( props.content ) ) } + + ) ) } +
+ ); +}; + +export default StoreNotices; diff --git a/packages/checkout/components/store-notices-container/style.scss b/packages/checkout/components/store-notices-container/style.scss index d4b58526489..cb347c56bbb 100644 --- a/packages/checkout/components/store-notices-container/style.scss +++ b/packages/checkout/components/store-notices-container/style.scss @@ -41,3 +41,32 @@ padding: 1.5rem 3rem; } } + +.wc-block-components-notices__snackbar { + position: fixed; + bottom: 20px; + left: 16px; + width: auto; + + @include breakpoint("<782px") { + position: fixed; + top: 10px; + left: 0; + bottom: auto; + } + + .components-snackbar-list__notice-container { + @include breakpoint("<782px") { + margin-left: 10px; + margin-right: 10px; + } + } + + .components-snackbar--status-error { + background-color: #e2401c; + } + + .components-snackbar--status-info { + background-color: #0073aa; + } +} diff --git a/packages/checkout/utils/create-notice.ts b/packages/checkout/utils/create-notice.ts new file mode 100644 index 00000000000..7f358f4a2b2 --- /dev/null +++ b/packages/checkout/utils/create-notice.ts @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import type { Options as NoticeOptions } from '@wordpress/notices'; +import { + STORE_NOTICE_CONTAINERS_STORE_KEY, + PAYMENT_STORE_KEY, +} from '@woocommerce/block-data'; +import { select, dispatch } from '@wordpress/data'; + +/** + * Wrapper for @wordpress/notices createNotice. + * + * This is used to create the correct type of notice based on the provided context, and to ensure the notice container + * exists first, otherwise it uses the default context instead. + */ +export const createNotice = ( + status: 'error' | 'warning' | 'info' | 'success', + message: string, + options: Partial< NoticeOptions > +) => { + let noticeContext = options?.context || 'wc'; + + const containerRefs = select( + STORE_NOTICE_CONTAINERS_STORE_KEY + ).getContainers() as Record< + string, + React.MutableRefObject< HTMLDivElement | null > + >; + + const suppressNotices = + select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive(); + + if ( suppressNotices ) { + return; + } + + // If the container ref was not registered, use the parent context instead. + if ( ! Object.keys( containerRefs ).includes( noticeContext ) ) { + if ( noticeContext.includes( 'wc/checkout/' ) ) { + noticeContext = 'wc/checkout'; + } else if ( noticeContext.includes( 'wc/cart/' ) ) { + noticeContext = 'wc/cart'; + } else { + noticeContext = 'wc/global'; + } + } + + const { createNotice: dispatchCreateNotice } = dispatch( 'core/notices' ); + + dispatchCreateNotice( status, message, { + isDismissible: true, + ...options, + context: noticeContext, + } ); +}; diff --git a/packages/checkout/utils/index.js b/packages/checkout/utils/index.js index c48b5ad34b3..24c2979de55 100644 --- a/packages/checkout/utils/index.js +++ b/packages/checkout/utils/index.js @@ -1,2 +1,3 @@ export * from './validation'; +export * from './create-notice'; export { extensionCartUpdate } from './extension-cart-update'; From 5b0eb8d429674e9861676b93bb2e9d1bac3c4086 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 09:07:50 +0000 Subject: [PATCH 04/34] Update noticeContexts imports --- assets/js/base/context/event-emit/utils.ts | 10 ++-------- .../payment-methods/use-payment-method-interface.ts | 7 +++++-- assets/js/base/context/hooks/use-checkout-notices.js | 6 +----- .../providers/store-notices/store-notice-context.tsx | 2 +- .../express-payment/cart-express-payment.js | 6 ++++-- .../express-payment/checkout-express-payment.js | 7 +++++-- .../payment-methods/payment-method-error-boundary.js | 6 ++++-- .../payment-methods/payment-method-options.js | 2 +- .../payment-methods/saved-payment-method-options.js | 2 +- assets/js/blocks/cart/block.js | 10 ++++------ assets/js/blocks/checkout/block.tsx | 4 ++-- .../inner-blocks/checkout-payment-block/block.tsx | 6 ++++-- .../checkout-shipping-address-block/block.tsx | 2 +- .../checkout-shipping-methods-block/block.tsx | 7 +++++-- assets/js/data/payment/thunks.ts | 2 +- .../js/data/payment/utils/check-payment-methods.ts | 2 +- .../js/types/type-defs/payment-method-interface.ts | 6 ++++-- packages/checkout/utils/create-notice.ts | 12 ++++++++++++ 18 files changed, 58 insertions(+), 41 deletions(-) diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index ae3f37596e1..84759c2e1ed 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -2,6 +2,7 @@ * External dependencies */ import { isObject } from '@woocommerce/types'; +import { noticeContexts as storeNoticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -25,14 +26,7 @@ export enum responseTypes { ERROR = 'error', } -export enum noticeContexts { - PAYMENTS = 'wc/payment-area', - EXPRESS_PAYMENTS = 'wc/express-payment-area', - CART = 'wc/cart', - CHECKOUT = 'wc/checkout', - SHIPPING_ADDRESS = 'wc/checkout/shippingAddress', - SHIPPING_METHODS = 'wc/checkout/shippingMethods', -} +export const noticeContexts = storeNoticeContexts; export interface ResponseType extends Record< string, unknown > { type: responseTypes; diff --git a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts index 21a2ebf7536..83cf3a40560 100644 --- a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts +++ b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts @@ -12,14 +12,17 @@ import LoadingMask from '@woocommerce/base-components/loading-mask'; import type { PaymentMethodInterface } from '@woocommerce/types'; import { useSelect, useDispatch } from '@wordpress/data'; import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data'; -import { ValidationInputError } from '@woocommerce/blocks-checkout'; +import { + ValidationInputError, + noticeContexts, +} from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import { useStoreCart } from '../cart/use-store-cart'; import { useStoreCartCoupons } from '../cart/use-store-cart-coupons'; -import { noticeContexts, responseTypes } from '../../event-emit'; +import { responseTypes } from '../../event-emit'; import { useCheckoutEventsContext } from '../../providers/cart-checkout/checkout-events'; import { usePaymentEventsContext } from '../../providers/cart-checkout/payment-events'; import { useShippingDataContext } from '../../providers/cart-checkout/shipping'; diff --git a/assets/js/base/context/hooks/use-checkout-notices.js b/assets/js/base/context/hooks/use-checkout-notices.js index c211bcabc14..d226449fbb1 100644 --- a/assets/js/base/context/hooks/use-checkout-notices.js +++ b/assets/js/base/context/hooks/use-checkout-notices.js @@ -2,11 +2,7 @@ * External dependencies */ import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { noticeContexts } from '../event-emit'; +import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * @typedef {import('@woocommerce/type-defs/contexts').StoreNoticeObject} StoreNoticeObject diff --git a/assets/js/base/context/providers/store-notices/store-notice-context.tsx b/assets/js/base/context/providers/store-notices/store-notice-context.tsx index 43ab62c51b2..044aaa79b99 100644 --- a/assets/js/base/context/providers/store-notices/store-notice-context.tsx +++ b/assets/js/base/context/providers/store-notices/store-notice-context.tsx @@ -12,13 +12,13 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { Button } from '@wordpress/components'; import type { Options as NoticeOptions } from '@wordpress/notices'; import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; +import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import { StoreNoticeContextType } from './types'; import { useEditorContext } from '../editor-context'; -import { noticeContexts } from '../../event-emit/utils'; const StoreNoticeContext = createContext< StoreNoticeContextType >( { createNotice: () => void 0, diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js index 19bd9b914ed..609261adb85 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js @@ -3,8 +3,10 @@ */ import { __ } from '@wordpress/i18n'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; -import { noticeContexts } from '@woocommerce/base-context'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { + StoreNoticesContainer, + noticeContexts, +} from '@woocommerce/blocks-checkout'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data'; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js index 957e90fdc98..7309d5c1502 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js @@ -2,8 +2,11 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { useEditorContext, noticeContexts } from '@woocommerce/base-context'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { useEditorContext } from '@woocommerce/base-context'; +import { + StoreNoticesContainer, + noticeContexts, +} from '@woocommerce/blocks-checkout'; import Title from '@woocommerce/base-components/title'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js index 42f90ed26d3..280d3ceff61 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js @@ -5,8 +5,10 @@ import { __ } from '@wordpress/i18n'; import { Component } from 'react'; import PropTypes from 'prop-types'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; -import { noticeContexts } from '@woocommerce/base-context'; +import { + StoreNoticesContainer, + noticeContexts, +} from '@woocommerce/blocks-checkout'; class PaymentMethodErrorBoundary extends Component { state = { errorMessage: '', hasError: false }; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js index ec80578572b..aeba8651d89 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js @@ -11,12 +11,12 @@ import classNames from 'classnames'; import RadioControlAccordion from '@woocommerce/base-components/radio-control-accordion'; import { useDispatch, useSelect } from '@wordpress/data'; import { getPaymentMethods } from '@woocommerce/blocks-registry'; +import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import PaymentMethodCard from './payment-method-card'; -import { noticeContexts } from '../../../base/context/event-emit'; import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants'; /** diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js b/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js index 82dfeb8ad66..53f728c5adf 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js @@ -3,7 +3,6 @@ */ import { useMemo, cloneElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { noticeContexts } from '@woocommerce/base-context'; import RadioControl from '@woocommerce/base-components/radio-control'; import { usePaymentMethodInterface, @@ -12,6 +11,7 @@ import { import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; import { useDispatch, useSelect } from '@wordpress/data'; import { getPaymentMethods } from '@woocommerce/blocks-registry'; +import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod diff --git a/assets/js/blocks/cart/block.js b/assets/js/blocks/cart/block.js index b567e501d44..32954d0e3b8 100644 --- a/assets/js/blocks/cart/block.js +++ b/assets/js/blocks/cart/block.js @@ -5,14 +5,15 @@ import { __ } from '@wordpress/i18n'; import { useStoreCart } from '@woocommerce/base-context/hooks'; import { useEffect } from '@wordpress/element'; import LoadingMask from '@woocommerce/base-components/loading-mask'; -import { noticeContexts, CartProvider } from '@woocommerce/base-context'; +import { CartProvider } from '@woocommerce/base-context'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import { translateJQueryEventToNative } from '@woocommerce/base-utils'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { SlotFillProvider, - SnackbarNoticesContainer, + StoreNoticesContainer, + noticeContexts, } from '@woocommerce/blocks-checkout'; /** @@ -82,10 +83,7 @@ const Block = ( { attributes, children, scrollToTop } ) => ( } showErrorMessage={ CURRENT_USER_IS_ADMIN } > - + { children } diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 3193bb8d9c7..2a7b0ff6ba0 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -5,8 +5,7 @@ import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; import { createInterpolateElement, useEffect } from '@wordpress/element'; import { useStoreCart } from '@woocommerce/base-context/hooks'; -import { CheckoutProvider, noticeContexts } from '@woocommerce/base-context'; - +import { CheckoutProvider } 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'; @@ -14,6 +13,7 @@ import { SlotFillProvider, StoreNoticesContainer, createNotice, + noticeContexts, } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { useDispatch, useSelect } from '@wordpress/data'; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx index 1794550ad28..234b2f49623 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx @@ -1,8 +1,10 @@ /** * External dependencies */ -import { noticeContexts } from '@woocommerce/base-context'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { + StoreNoticesContainer, + noticeContexts, +} from '@woocommerce/blocks-checkout'; /** * Internal dependencies diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx index 528bfdb10f5..53b930f6c7b 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx @@ -8,11 +8,11 @@ import { useCheckoutAddress, useStoreEvents, useEditorContext, - noticeContexts, } from '@woocommerce/base-context'; import { CheckboxControl, StoreNoticesContainer, + noticeContexts, } from '@woocommerce/blocks-checkout'; import Noninteractive from '@woocommerce/base-components/noninteractive'; import type { diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx index a26e4606514..efa3131b5bf 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx @@ -7,8 +7,11 @@ import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout import { getShippingRatesPackageCount } from '@woocommerce/base-utils'; import { getCurrencyFromPriceResponse } from '@woocommerce/price-format'; import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; -import { useEditorContext, noticeContexts } from '@woocommerce/base-context'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { useEditorContext } from '@woocommerce/base-context'; +import { + StoreNoticesContainer, + noticeContexts, +} from '@woocommerce/blocks-checkout'; import { decodeEntities } from '@wordpress/html-entities'; import { Notice } from 'wordpress-components'; import classnames from 'classnames'; diff --git a/assets/js/data/payment/thunks.ts b/assets/js/data/payment/thunks.ts index 65516525b4d..c41dc344ba5 100644 --- a/assets/js/data/payment/thunks.ts +++ b/assets/js/data/payment/thunks.ts @@ -2,6 +2,7 @@ * External dependencies */ import { store as noticesStore } from '@wordpress/notices'; +import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -12,7 +13,6 @@ import { isErrorResponse, isFailResponse, isSuccessResponse, - noticeContexts, } from '../../base/context/event-emit'; import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit'; import type { emitProcessingEventType } from './types'; diff --git a/assets/js/data/payment/utils/check-payment-methods.ts b/assets/js/data/payment/utils/check-payment-methods.ts index b85734e04e0..3de666ed119 100644 --- a/assets/js/data/payment/utils/check-payment-methods.ts +++ b/assets/js/data/payment/utils/check-payment-methods.ts @@ -19,13 +19,13 @@ import { getPaymentMethods, } from '@woocommerce/blocks-registry'; import { previewCart } from '@woocommerce/resource-previews'; +import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import { STORE_KEY as CART_STORE_KEY } from '../../cart/constants'; import { STORE_KEY as PAYMENT_STORE_KEY } from '../constants'; -import { noticeContexts } from '../../../base/context/event-emit'; import { EMPTY_CART_ERRORS, EMPTY_CART_ITEM_ERRORS, diff --git a/assets/js/types/type-defs/payment-method-interface.ts b/assets/js/types/type-defs/payment-method-interface.ts index 84dd7ef4c49..92ca16995da 100644 --- a/assets/js/types/type-defs/payment-method-interface.ts +++ b/assets/js/types/type-defs/payment-method-interface.ts @@ -5,7 +5,10 @@ import type PaymentMethodLabel from '@woocommerce/base-components/cart-checkout/payment-method-label'; import type PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons'; import type LoadingMask from '@woocommerce/base-components/loading-mask'; -import { ValidationInputError } from '@woocommerce/blocks-checkout'; +import { + ValidationInputError, + noticeContexts, +} from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -15,7 +18,6 @@ import type { CartBillingAddress, CartShippingRate } from './cart'; import type { emitterCallback, responseTypes, - noticeContexts, } from '../../base/context/event-emit'; import type { CartResponseShippingAddress, diff --git a/packages/checkout/utils/create-notice.ts b/packages/checkout/utils/create-notice.ts index 7f358f4a2b2..48ebbdd1acb 100644 --- a/packages/checkout/utils/create-notice.ts +++ b/packages/checkout/utils/create-notice.ts @@ -8,6 +8,18 @@ import { } from '@woocommerce/block-data'; import { select, dispatch } from '@wordpress/data'; +/** + * List of valid notice contexts included in blocks. + */ +export const noticeContexts = { + CART: 'wc/cart', + CHECKOUT: 'wc/checkout', + PAYMENTS: 'wc/checkout/payments', + EXPRESS_PAYMENTS: 'wc/checkout/express-payments', + SHIPPING_ADDRESS: 'wc/checkout/shipping-address', + SHIPPING_METHODS: 'wc/checkout/shipping-methods', +}; + /** * Wrapper for @wordpress/notices createNotice. * From 1b9a2056fa2eb162c20b2ec87c6a7680d85e2786 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 09:27:46 +0000 Subject: [PATCH 05/34] Remove context provider --- .../context/providers/store-notices/index.ts | 3 - .../snackbar-notices-container/index.tsx | 66 -------- .../snackbar-notices-container/style.scss | 28 ---- .../snackbar-notices-container/types.ts | 15 -- .../store-notices/store-notice-context.tsx | 144 ------------------ .../store-notices-container/index.tsx | 94 ------------ .../store-notices-container/style.scss | 43 ------ .../store-notices-container/types.ts | 23 --- .../store-notices-container/utils.ts | 12 -- .../context/providers/store-notices/types.ts | 21 --- 10 files changed, 449 deletions(-) delete mode 100644 assets/js/base/context/providers/store-notices/index.ts delete mode 100644 assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx delete mode 100644 assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss delete mode 100644 assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts delete mode 100644 assets/js/base/context/providers/store-notices/store-notice-context.tsx delete mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/index.tsx delete mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/style.scss delete mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/types.ts delete mode 100644 assets/js/base/context/providers/store-notices/store-notices-container/utils.ts delete mode 100644 assets/js/base/context/providers/store-notices/types.ts diff --git a/assets/js/base/context/providers/store-notices/index.ts b/assets/js/base/context/providers/store-notices/index.ts deleted file mode 100644 index b1f06241641..00000000000 --- a/assets/js/base/context/providers/store-notices/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './store-notice-context'; -export { default as StoreNoticesContainer } from './store-notices-container'; -export { default as SnackbarNoticesContainer } from './snackbar-notices-container'; diff --git a/assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx b/assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx deleted file mode 100644 index 11ba410c816..00000000000 --- a/assets/js/base/context/providers/store-notices/snackbar-notices-container/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { SnackbarList } from 'wordpress-components'; -import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import './style.scss'; -import type { - NoticeType, - NoticeOptions, - SnackbarNoticesContainerProps, -} from './types'; -import { useStoreNoticeContext } from '../store-notice-context'; - -const SnackbarNoticesContainer = ( { - className, - forceType = false, - context = 'default', -}: SnackbarNoticesContainerProps ): JSX.Element | null => { - const { suppressNotices } = useStoreNoticeContext(); - const { removeNotice } = useDispatch( 'core/notices' ); - - const notices = useSelect< Array< NoticeType & NoticeOptions > >( - ( select ) => - select( 'core/notices' ).getNotices( context ) as Array< - NoticeType & NoticeOptions - > - ); - - if ( suppressNotices ) { - return null; - } - - return ( - notice.type === 'snackbar' || forceType ) - .map( ( notice ) => { - return { - ...notice, - className: - 'components-snackbar--status-' + notice.status, - }; - } ) } - onRemove={ ( noticeId: string ) => { - notices.forEach( ( notice ) => { - if ( notice.explicitDismiss && notice.id === noticeId ) { - removeNotice( notice.id, context ); - } else { - removeNotice( notice.id, context ); - } - } ); - } } - /> - ); -}; - -export default SnackbarNoticesContainer; diff --git a/assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss b/assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss deleted file mode 100644 index e1d80e61243..00000000000 --- a/assets/js/base/context/providers/store-notices/snackbar-notices-container/style.scss +++ /dev/null @@ -1,28 +0,0 @@ -.wc-block-components-notices__snackbar { - position: fixed; - bottom: 20px; - left: 16px; - width: auto; - - @include breakpoint("<782px") { - position: fixed; - top: 10px; - left: 0; - bottom: auto; - } - - .components-snackbar-list__notice-container { - @include breakpoint("<782px") { - margin-left: 10px; - margin-right: 10px; - } - } - - .components-snackbar--status-error { - background-color: #e2401c; - } - - .components-snackbar--status-info { - background-color: #0073aa; - } -} diff --git a/assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts b/assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts deleted file mode 100644 index 6ddceca9178..00000000000 --- a/assets/js/base/context/providers/store-notices/snackbar-notices-container/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * External dependencies - */ -import type { - Notice as NoticeType, - Options as NoticeOptions, -} from '@wordpress/notices'; - -export interface SnackbarNoticesContainerProps { - className?: string; - context?: string; - forceType?: boolean; -} - -export { NoticeType, NoticeOptions }; diff --git a/assets/js/base/context/providers/store-notices/store-notice-context.tsx b/assets/js/base/context/providers/store-notices/store-notice-context.tsx deleted file mode 100644 index 044aaa79b99..00000000000 --- a/assets/js/base/context/providers/store-notices/store-notice-context.tsx +++ /dev/null @@ -1,144 +0,0 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { - createContext, - useContext, - useCallback, - useState, -} from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { Button } from '@wordpress/components'; -import type { Options as NoticeOptions } from '@wordpress/notices'; -import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; -import { noticeContexts } from '@woocommerce/blocks-checkout'; - -/** - * Internal dependencies - */ -import { StoreNoticeContextType } from './types'; -import { useEditorContext } from '../editor-context'; - -const StoreNoticeContext = createContext< StoreNoticeContextType >( { - createNotice: () => void 0, - registerContainer: () => void 0, - suppressNotices: false, -} ); - -export const useStoreNoticeContext = () => { - return useContext( StoreNoticeContext ); -}; - -export const StoreNoticeProvider = ( { - children, - defaultContext, -}: { - children: JSX.Element | JSX.Element[]; - defaultContext: string; -} ): JSX.Element => { - const { createNotice: dispatchCreateNotice } = - useDispatch( 'core/notices' ); - const { isEditor } = useEditorContext(); - const isExpressPaymentMethodActive = useSelect( ( select ) => - select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() - ); - - // Maintains a list of containers that have been registered in the hierarchy. - const [ containerRefs, setContainerRefs ] = useState( {} ); - const registerContainer = useCallback( - ( - containerContext: string, - ref: React.MutableRefObject< HTMLDivElement | null > - ) => { - setContainerRefs( ( current ) => ( { - ...current, - [ containerContext ]: ref, - } ) ); - }, - [ setContainerRefs ] - ); - - /** - * Wrapper for createNotice used to create the correct type of notice based on the provided context. - */ - const createNotice = useCallback< - StoreNoticeContextType[ 'createNotice' ] - >( - ( - status: 'error' | 'warning' | 'info', - message: string, - options: Partial< NoticeOptions > - ) => { - let noticeContext = options?.context || defaultContext; - - // If the container ref was not registered, show the notice in the default context instead so it is visible. - if ( ! Object.keys( containerRefs ).includes( noticeContext ) ) { - noticeContext = defaultContext; - } - - const type = - noticeContext === defaultContext ? 'snackbar' : 'default'; - - dispatchCreateNotice( status, message, { - isDismissible: true, - ...options, - type, - context: noticeContext, - } ); - }, - [ containerRefs, defaultContext, dispatchCreateNotice ] - ); - - const contextData = { - createNotice, - registerContainer, - suppressNotices: isEditor || isExpressPaymentMethodActive, - } as StoreNoticeContextType; - - return ( - - - { Object.values( noticeContexts ).map( ( contextValue ) => { - return ( - - ); - } ) } - { children } - - ); -}; diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/index.tsx b/assets/js/base/context/providers/store-notices/store-notices-container/index.tsx deleted file mode 100644 index 1b11f231ae1..00000000000 --- a/assets/js/base/context/providers/store-notices/store-notices-container/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { useRef, useEffect } from '@wordpress/element'; -import { Notice } from 'wordpress-components'; -import { sanitizeHTML } from '@woocommerce/utils'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { usePrevious } from '@woocommerce/base-hooks'; -import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import './style.scss'; -import { getClassNameFromStatus } from './utils'; -import type { - StoreNoticesContainerProps, - NoticeType, - NoticeOptions, -} from './types'; -import { useStoreNoticeContext } from '../store-notice-context'; - -const StoreNoticesContainer = ( { - className, - context = 'default', - additionalNotices = [], -}: StoreNoticesContainerProps ): JSX.Element | null => { - const ref = useRef< HTMLDivElement >( null ); - const { registerContainer, suppressNotices } = useStoreNoticeContext(); - const { removeNotice } = useDispatch( 'core/notices' ); - - const notices = useSelect< Array< NoticeType & NoticeOptions > >( - ( select ) => - select( 'core/notices' ).getNotices( context ) as Array< - NoticeType & NoticeOptions - > - ) - .filter( ( notice ) => notice.type !== 'snackbar' ) - .concat( additionalNotices ); - - // Register the container with the parent. - useEffect( () => { - registerContainer( context, ref ); - }, [ context, ref, registerContainer ] ); - - // Scroll to container when an error is added here. - const noticeIds = notices.map( ( notice ) => notice.id ); - const previousNoticeIds = usePrevious( noticeIds ); - - useEffect( () => { - const newNoticeIds = noticeIds.filter( - ( value ) => - ! previousNoticeIds || ! previousNoticeIds.includes( value ) - ); - - if ( newNoticeIds.length ) { - ref.current?.scrollIntoView( { - behavior: 'smooth', - } ); - } - }, [ noticeIds, previousNoticeIds, ref ] ); - - if ( suppressNotices ) { - return null; - } - - return ( -
- { notices.map( ( props ) => ( - { - if ( props.isDismissible ) { - removeNotice( props.id, context ); - } - } } - > - { sanitizeHTML( decodeEntities( props.content ) ) } - - ) ) } -
- ); -}; - -export default StoreNoticesContainer; diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/style.scss b/assets/js/base/context/providers/store-notices/store-notices-container/style.scss deleted file mode 100644 index d4b58526489..00000000000 --- a/assets/js/base/context/providers/store-notices/store-notices-container/style.scss +++ /dev/null @@ -1,43 +0,0 @@ -.wc-block-components-notices { - display: block; - margin-bottom: 2em; - .wc-block-components-notices__notice { - margin: 0; - display: flex; - flex-wrap: nowrap; - a { - text-decoration: underline; - } - .components-notice__dismiss { - background: transparent none; - padding: 0; - margin: 0 0 0 auto; - border: 0; - outline: 0; - color: currentColor; - svg { - fill: currentColor; - vertical-align: text-top; - } - } - .components-notice__content > div:not(.components-notice__actions) { - *:first-child { - margin-top: 0; - } - *:last-child { - margin-bottom: 0; - } - } - } - .wc-block-components-notices__notice + .wc-block-components-notices__notice { - margin-top: 1em; - } -} - -// @todo Either move notice style fixes to Woo core, or take full control over notice component styling in blocks. -.theme-twentytwentyone, -.theme-twentytwenty { - .wc-block-components-notices__notice { - padding: 1.5rem 3rem; - } -} diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/types.ts b/assets/js/base/context/providers/store-notices/store-notices-container/types.ts deleted file mode 100644 index 5eafe6de196..00000000000 --- a/assets/js/base/context/providers/store-notices/store-notices-container/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * External dependencies - */ -import type { - Notice as NoticeType, - Options as NoticeOptions, -} from '@wordpress/notices'; - -export interface StoreNotice { - content: string; - id: string; - status: string; - isDismissible?: boolean; - type: 'default' | 'snackbar'; -} - -export interface StoreNoticesContainerProps { - className?: string; - context?: string; - additionalNotices?: ( NoticeType & NoticeOptions )[]; -} - -export { NoticeType, NoticeOptions }; diff --git a/assets/js/base/context/providers/store-notices/store-notices-container/utils.ts b/assets/js/base/context/providers/store-notices/store-notices-container/utils.ts deleted file mode 100644 index e7706a9fe14..00000000000 --- a/assets/js/base/context/providers/store-notices/store-notices-container/utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -export const getClassNameFromStatus = ( { status = 'default' } ): string => { - switch ( status ) { - case 'error': - return 'woocommerce-error'; - case 'success': - return 'woocommerce-message'; - case 'info': - case 'warning': - return 'woocommerce-info'; - } - return ''; -}; diff --git a/assets/js/base/context/providers/store-notices/types.ts b/assets/js/base/context/providers/store-notices/types.ts deleted file mode 100644 index 4570e9470c8..00000000000 --- a/assets/js/base/context/providers/store-notices/types.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * External dependencies - */ -import type { Options as NoticeOptions } from '@wordpress/notices'; - -export type StoreNoticeContextType = { - createNotice: ( - status: 'error' | 'warning' | 'info', - message: string, - options: Partial< - Omit< NoticeOptions, 'context' > & { - context: string; - } - > - ) => void; - registerContainer: ( - context: string, - ref: React.MutableRefObject< HTMLDivElement | null > - ) => void; - suppressNotices: boolean; -}; From 98df34ccc3411ff7135f128cc22be38fc6fe4c67 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 10:22:31 +0000 Subject: [PATCH 06/34] Update data store --- assets/js/data/index.ts | 2 +- .../js/data/store-notice-containers/index.ts | 41 ------------------- assets/js/data/store-notices/actions.ts | 17 ++++++++ assets/js/data/store-notices/default-state.ts | 10 +++++ assets/js/data/store-notices/index.ts | 36 ++++++++++++++++ assets/js/data/store-notices/reducers.ts | 35 ++++++++++++++++ assets/js/data/store-notices/selectors.ts | 8 ++++ .../store-notices-container/store-notices.tsx | 11 +++-- packages/checkout/utils/create-notice.ts | 15 +++---- 9 files changed, 119 insertions(+), 56 deletions(-) delete mode 100644 assets/js/data/store-notice-containers/index.ts create mode 100644 assets/js/data/store-notices/actions.ts create mode 100644 assets/js/data/store-notices/default-state.ts create mode 100644 assets/js/data/store-notices/index.ts create mode 100644 assets/js/data/store-notices/reducers.ts create mode 100644 assets/js/data/store-notices/selectors.ts diff --git a/assets/js/data/index.ts b/assets/js/data/index.ts index 2f0e0b07488..8be09cf717f 100644 --- a/assets/js/data/index.ts +++ b/assets/js/data/index.ts @@ -13,6 +13,6 @@ export { CHECKOUT_STORE_KEY } from './checkout'; export { PAYMENT_STORE_KEY } from './payment'; export { VALIDATION_STORE_KEY } from './validation'; export { QUERY_STATE_STORE_KEY } from './query-state'; -export { STORE_NOTICE_CONTAINERS_STORE_KEY } from './store-notice-containers'; +export { STORE_NOTICES_STORE_KEY } from './store-notices'; export * from './constants'; export * from './types'; diff --git a/assets/js/data/store-notice-containers/index.ts b/assets/js/data/store-notice-containers/index.ts deleted file mode 100644 index fa89367df1e..00000000000 --- a/assets/js/data/store-notice-containers/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * External dependencies - */ -import { createReduxStore, register } from '@wordpress/data'; - -export const STORE_KEY = 'wc/store/notice-containers'; - -const store = createReduxStore( STORE_KEY, { - reducer( state = {}, action ) { - switch ( action.type ) { - case 'REGISTER_CONTAINER': - return { - ...state, - [ action.containerContext ]: action.ref, - }; - } - return state; - }, - actions: { - registerContainer( - containerContext: string, - ref: React.MutableRefObject< HTMLDivElement | null > - ) { - return { - type: 'REGISTER_CONTAINER', - containerContext, - ref, - }; - }, - }, - selectors: { - getContainers: ( - state - ): Record< string, React.MutableRefObject< HTMLDivElement | null > > => - state, - }, -} ); - -register( store ); - -export const STORE_NOTICE_CONTAINERS_STORE_KEY = STORE_KEY; diff --git a/assets/js/data/store-notices/actions.ts b/assets/js/data/store-notices/actions.ts new file mode 100644 index 00000000000..9654afd0542 --- /dev/null +++ b/assets/js/data/store-notices/actions.ts @@ -0,0 +1,17 @@ +export const registerContainer = ( + containerContext: string, + ref: React.MutableRefObject< HTMLDivElement | null > +) => { + return { + type: 'REGISTER_CONTAINER', + containerContext, + ref, + }; +}; + +export const unregisterContainer = ( containerContext: string ) => { + return { + type: 'UNREGISTER_CONTAINER', + containerContext, + }; +}; diff --git a/assets/js/data/store-notices/default-state.ts b/assets/js/data/store-notices/default-state.ts new file mode 100644 index 00000000000..502763f8694 --- /dev/null +++ b/assets/js/data/store-notices/default-state.ts @@ -0,0 +1,10 @@ +export interface StoreNoticesState { + containers: Record< + string, + React.MutableRefObject< HTMLDivElement | null > + >; +} + +export const defaultStoreNoticesState: StoreNoticesState = { + containers: {}, +}; diff --git a/assets/js/data/store-notices/index.ts b/assets/js/data/store-notices/index.ts new file mode 100644 index 00000000000..efa9a208cd9 --- /dev/null +++ b/assets/js/data/store-notices/index.ts @@ -0,0 +1,36 @@ +/** + * External dependencies + */ +import { createReduxStore, register } from '@wordpress/data'; +import { controls } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import * as actions from './actions'; +import * as selectors from './selectors'; +import reducer from './reducers'; +import { DispatchFromMap, SelectFromMap } from '../mapped-types'; + +const STORE_KEY = 'wc/store/notices'; +const config = { + reducer, + actions, + selectors, + controls, +}; +const store = createReduxStore( STORE_KEY, config ); +register( store ); + +export const STORE_NOTICES_STORE_KEY = STORE_KEY; + +declare module '@wordpress/data' { + function dispatch( + key: typeof STORE_KEY + ): DispatchFromMap< typeof actions >; + function select( key: typeof STORE_KEY ): SelectFromMap< + typeof selectors + > & { + hasFinishedResolution: ( selector: string ) => boolean; + }; +} diff --git a/assets/js/data/store-notices/reducers.ts b/assets/js/data/store-notices/reducers.ts new file mode 100644 index 00000000000..44e3fee0b92 --- /dev/null +++ b/assets/js/data/store-notices/reducers.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import type { Reducer } from 'redux'; + +/** + * Internal dependencies + */ +import { defaultStoreNoticesState, StoreNoticesState } from './default-state'; + +const reducer: Reducer< StoreNoticesState > = ( + state = defaultStoreNoticesState, + action +) => { + switch ( action.type ) { + case 'REGISTER_CONTAINER': + return { + ...state, + containers: { + ...state.containers, + [ action.containerContext ]: action.ref, + }, + }; + case 'UNREGISTER_CONTAINER': + const { [ action.containerContext ]: _, ...containers } = + state.containers; + return { + ...state, + containers, + }; + } + return state; +}; + +export default reducer; diff --git a/assets/js/data/store-notices/selectors.ts b/assets/js/data/store-notices/selectors.ts new file mode 100644 index 00000000000..6008d68ce45 --- /dev/null +++ b/assets/js/data/store-notices/selectors.ts @@ -0,0 +1,8 @@ +/** + * Internal dependencies + */ +import { StoreNoticesState } from './default-state'; + +export const getContainers = ( + state: StoreNoticesState +): StoreNoticesState[ 'containers' ] => state.containers; diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index 1fbeffadb58..856ebb4ebe9 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -8,7 +8,7 @@ import { sanitizeHTML } from '@woocommerce/utils'; import { useDispatch } from '@wordpress/data'; import { usePrevious } from '@woocommerce/base-hooks'; import { decodeEntities } from '@wordpress/html-entities'; -import { STORE_NOTICE_CONTAINERS_STORE_KEY } from '@woocommerce/block-data'; +import { STORE_NOTICES_STORE_KEY } from '@woocommerce/block-data'; /** * Internal dependencies @@ -27,14 +27,17 @@ const StoreNotices = ( { } ): JSX.Element => { const { removeNotice } = useDispatch( 'core/notices' ); const ref = useRef< HTMLDivElement >( null ); - const { registerContainer } = useDispatch( - STORE_NOTICE_CONTAINERS_STORE_KEY + const { registerContainer, unregisterContainer } = useDispatch( + STORE_NOTICES_STORE_KEY ); // Register the container with the parent. useEffect( () => { registerContainer( context, ref ); - }, [ context, ref, registerContainer ] ); + return function cleanup() { + unregisterContainer( context ); + }; + }, [ context, ref, registerContainer, unregisterContainer ] ); // Scroll to container when an error is added here. const noticeIds = notices.map( ( notice ) => notice.id ); diff --git a/packages/checkout/utils/create-notice.ts b/packages/checkout/utils/create-notice.ts index 48ebbdd1acb..b225633d164 100644 --- a/packages/checkout/utils/create-notice.ts +++ b/packages/checkout/utils/create-notice.ts @@ -3,7 +3,7 @@ */ import type { Options as NoticeOptions } from '@wordpress/notices'; import { - STORE_NOTICE_CONTAINERS_STORE_KEY, + STORE_NOTICES_STORE_KEY, PAYMENT_STORE_KEY, } from '@woocommerce/block-data'; import { select, dispatch } from '@wordpress/data'; @@ -33,13 +33,6 @@ export const createNotice = ( ) => { let noticeContext = options?.context || 'wc'; - const containerRefs = select( - STORE_NOTICE_CONTAINERS_STORE_KEY - ).getContainers() as Record< - string, - React.MutableRefObject< HTMLDivElement | null > - >; - const suppressNotices = select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive(); @@ -47,12 +40,14 @@ export const createNotice = ( return; } + const containerRefs = select( STORE_NOTICES_STORE_KEY ).getContainers(); + // If the container ref was not registered, use the parent context instead. if ( ! Object.keys( containerRefs ).includes( noticeContext ) ) { if ( noticeContext.includes( 'wc/checkout/' ) ) { - noticeContext = 'wc/checkout'; + noticeContext = noticeContexts.CHECKOUT; } else if ( noticeContext.includes( 'wc/cart/' ) ) { - noticeContext = 'wc/cart'; + noticeContext = noticeContexts.CART; } else { noticeContext = 'wc/global'; } From f3437f63b9be3eb8082e00d5a8d7e3a4918d5d8e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 10:51:21 +0000 Subject: [PATCH 07/34] Fix 502 --- assets/js/base/context/event-emit/utils.ts | 10 ++++++++-- .../use-payment-method-interface.ts | 7 ++----- .../base/context/hooks/use-checkout-notices.js | 6 +++++- .../express-payment/cart-express-payment.js | 6 ++---- .../express-payment/checkout-express-payment.js | 7 ++----- .../payment-method-error-boundary.js | 6 ++---- .../payment-methods/payment-method-options.js | 2 +- .../saved-payment-method-options.js | 2 +- assets/js/blocks/cart/block.js | 3 +-- assets/js/blocks/checkout/block.tsx | 3 +-- .../checkout-payment-block/block.tsx | 6 ++---- .../checkout-shipping-address-block/block.tsx | 2 +- .../checkout-shipping-methods-block/block.tsx | 7 ++----- assets/js/data/payment/thunks.ts | 2 +- .../data/payment/utils/check-payment-methods.ts | 2 +- .../types/type-defs/payment-method-interface.ts | 6 ++---- packages/checkout/utils/create-notice.ts | 16 ++-------------- 17 files changed, 36 insertions(+), 57 deletions(-) diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index 84759c2e1ed..d15a127af38 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -2,7 +2,6 @@ * External dependencies */ import { isObject } from '@woocommerce/types'; -import { noticeContexts as storeNoticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -26,7 +25,14 @@ export enum responseTypes { ERROR = 'error', } -export const noticeContexts = storeNoticeContexts; +export enum noticeContexts { + CART = 'wc/cart', + CHECKOUT = 'wc/checkout', + PAYMENTS = 'wc/checkout/payments', + EXPRESS_PAYMENTS = 'wc/checkout/express-payments', + SHIPPING_ADDRESS = 'wc/checkout/shipping-address', + SHIPPING_METHODS = 'wc/checkout/shipping-methods', +} export interface ResponseType extends Record< string, unknown > { type: responseTypes; diff --git a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts index 83cf3a40560..21a2ebf7536 100644 --- a/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts +++ b/assets/js/base/context/hooks/payment-methods/use-payment-method-interface.ts @@ -12,17 +12,14 @@ import LoadingMask from '@woocommerce/base-components/loading-mask'; import type { PaymentMethodInterface } from '@woocommerce/types'; import { useSelect, useDispatch } from '@wordpress/data'; import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data'; -import { - ValidationInputError, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { ValidationInputError } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import { useStoreCart } from '../cart/use-store-cart'; import { useStoreCartCoupons } from '../cart/use-store-cart-coupons'; -import { responseTypes } from '../../event-emit'; +import { noticeContexts, responseTypes } from '../../event-emit'; import { useCheckoutEventsContext } from '../../providers/cart-checkout/checkout-events'; import { usePaymentEventsContext } from '../../providers/cart-checkout/payment-events'; import { useShippingDataContext } from '../../providers/cart-checkout/shipping'; diff --git a/assets/js/base/context/hooks/use-checkout-notices.js b/assets/js/base/context/hooks/use-checkout-notices.js index d226449fbb1..c211bcabc14 100644 --- a/assets/js/base/context/hooks/use-checkout-notices.js +++ b/assets/js/base/context/hooks/use-checkout-notices.js @@ -2,7 +2,11 @@ * External dependencies */ import { useSelect } from '@wordpress/data'; -import { noticeContexts } from '@woocommerce/blocks-checkout'; + +/** + * Internal dependencies + */ +import { noticeContexts } from '../event-emit'; /** * @typedef {import('@woocommerce/type-defs/contexts').StoreNoticeObject} StoreNoticeObject diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js index 609261adb85..22e43edc394 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js @@ -3,10 +3,8 @@ */ import { __ } from '@wordpress/i18n'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; -import { - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { noticeContexts } from '@woocommerce/base-context'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data'; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js index 7309d5c1502..957e90fdc98 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/checkout-express-payment.js @@ -2,11 +2,8 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; -import { useEditorContext } from '@woocommerce/base-context'; -import { - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { useEditorContext, noticeContexts } from '@woocommerce/base-context'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; import Title from '@woocommerce/base-components/title'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js index 280d3ceff61..42f90ed26d3 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-error-boundary.js @@ -5,10 +5,8 @@ import { __ } from '@wordpress/i18n'; import { Component } from 'react'; import PropTypes from 'prop-types'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; -import { - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { noticeContexts } from '@woocommerce/base-context'; class PaymentMethodErrorBoundary extends Component { state = { errorMessage: '', hasError: false }; diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js index aeba8651d89..ec80578572b 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/payment-method-options.js @@ -11,12 +11,12 @@ import classNames from 'classnames'; import RadioControlAccordion from '@woocommerce/base-components/radio-control-accordion'; import { useDispatch, useSelect } from '@wordpress/data'; import { getPaymentMethods } from '@woocommerce/blocks-registry'; -import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import PaymentMethodCard from './payment-method-card'; +import { noticeContexts } from '../../../base/context/event-emit'; import { STORE_KEY as PAYMENT_STORE_KEY } from '../../../data/payment/constants'; /** diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js b/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js index 53f728c5adf..82dfeb8ad66 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/saved-payment-method-options.js @@ -3,6 +3,7 @@ */ import { useMemo, cloneElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { noticeContexts } from '@woocommerce/base-context'; import RadioControl from '@woocommerce/base-components/radio-control'; import { usePaymentMethodInterface, @@ -11,7 +12,6 @@ import { import { PAYMENT_STORE_KEY } from '@woocommerce/block-data'; import { useDispatch, useSelect } from '@wordpress/data'; import { getPaymentMethods } from '@woocommerce/blocks-registry'; -import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod diff --git a/assets/js/blocks/cart/block.js b/assets/js/blocks/cart/block.js index 32954d0e3b8..e2b2fc829e6 100644 --- a/assets/js/blocks/cart/block.js +++ b/assets/js/blocks/cart/block.js @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import { useStoreCart } from '@woocommerce/base-context/hooks'; import { useEffect } from '@wordpress/element'; import LoadingMask from '@woocommerce/base-components/loading-mask'; -import { CartProvider } from '@woocommerce/base-context'; +import { CartProvider, noticeContexts } from '@woocommerce/base-context'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import { translateJQueryEventToNative } from '@woocommerce/base-utils'; @@ -13,7 +13,6 @@ import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { SlotFillProvider, StoreNoticesContainer, - noticeContexts, } from '@woocommerce/blocks-checkout'; /** diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 2a7b0ff6ba0..004e94c70b0 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -5,7 +5,7 @@ import { __ } from '@wordpress/i18n'; import classnames from 'classnames'; import { createInterpolateElement, useEffect } from '@wordpress/element'; import { useStoreCart } from '@woocommerce/base-context/hooks'; -import { CheckoutProvider } from '@woocommerce/base-context'; +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'; @@ -13,7 +13,6 @@ import { SlotFillProvider, StoreNoticesContainer, createNotice, - noticeContexts, } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { useDispatch, useSelect } from '@wordpress/data'; diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx index 234b2f49623..1edd34bdb42 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-payment-block/block.tsx @@ -1,10 +1,8 @@ /** * External dependencies */ -import { - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; +import { noticeContexts } from '@woocommerce/base-context'; /** * Internal dependencies diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx index 53b930f6c7b..528bfdb10f5 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-address-block/block.tsx @@ -8,11 +8,11 @@ import { useCheckoutAddress, useStoreEvents, useEditorContext, + noticeContexts, } from '@woocommerce/base-context'; import { CheckboxControl, StoreNoticesContainer, - noticeContexts, } from '@woocommerce/blocks-checkout'; import Noninteractive from '@woocommerce/base-components/noninteractive'; import type { diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx index efa3131b5bf..a26e4606514 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-shipping-methods-block/block.tsx @@ -7,11 +7,8 @@ import { ShippingRatesControl } from '@woocommerce/base-components/cart-checkout import { getShippingRatesPackageCount } from '@woocommerce/base-utils'; import { getCurrencyFromPriceResponse } from '@woocommerce/price-format'; import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount'; -import { useEditorContext } from '@woocommerce/base-context'; -import { - StoreNoticesContainer, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { useEditorContext, noticeContexts } from '@woocommerce/base-context'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; import { decodeEntities } from '@wordpress/html-entities'; import { Notice } from 'wordpress-components'; import classnames from 'classnames'; diff --git a/assets/js/data/payment/thunks.ts b/assets/js/data/payment/thunks.ts index c41dc344ba5..65516525b4d 100644 --- a/assets/js/data/payment/thunks.ts +++ b/assets/js/data/payment/thunks.ts @@ -2,7 +2,6 @@ * External dependencies */ import { store as noticesStore } from '@wordpress/notices'; -import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -13,6 +12,7 @@ import { isErrorResponse, isFailResponse, isSuccessResponse, + noticeContexts, } from '../../base/context/event-emit'; import { EMIT_TYPES } from '../../base/context/providers/cart-checkout/payment-events/event-emit'; import type { emitProcessingEventType } from './types'; diff --git a/assets/js/data/payment/utils/check-payment-methods.ts b/assets/js/data/payment/utils/check-payment-methods.ts index 3de666ed119..b85734e04e0 100644 --- a/assets/js/data/payment/utils/check-payment-methods.ts +++ b/assets/js/data/payment/utils/check-payment-methods.ts @@ -19,13 +19,13 @@ import { getPaymentMethods, } from '@woocommerce/blocks-registry'; import { previewCart } from '@woocommerce/resource-previews'; -import { noticeContexts } from '@woocommerce/blocks-checkout'; /** * Internal dependencies */ import { STORE_KEY as CART_STORE_KEY } from '../../cart/constants'; import { STORE_KEY as PAYMENT_STORE_KEY } from '../constants'; +import { noticeContexts } from '../../../base/context/event-emit'; import { EMPTY_CART_ERRORS, EMPTY_CART_ITEM_ERRORS, diff --git a/assets/js/types/type-defs/payment-method-interface.ts b/assets/js/types/type-defs/payment-method-interface.ts index 92ca16995da..84dd7ef4c49 100644 --- a/assets/js/types/type-defs/payment-method-interface.ts +++ b/assets/js/types/type-defs/payment-method-interface.ts @@ -5,10 +5,7 @@ import type PaymentMethodLabel from '@woocommerce/base-components/cart-checkout/payment-method-label'; import type PaymentMethodIcons from '@woocommerce/base-components/cart-checkout/payment-method-icons'; import type LoadingMask from '@woocommerce/base-components/loading-mask'; -import { - ValidationInputError, - noticeContexts, -} from '@woocommerce/blocks-checkout'; +import { ValidationInputError } from '@woocommerce/blocks-checkout'; /** * Internal dependencies @@ -18,6 +15,7 @@ import type { CartBillingAddress, CartShippingRate } from './cart'; import type { emitterCallback, responseTypes, + noticeContexts, } from '../../base/context/event-emit'; import type { CartResponseShippingAddress, diff --git a/packages/checkout/utils/create-notice.ts b/packages/checkout/utils/create-notice.ts index b225633d164..ca66f08bff5 100644 --- a/packages/checkout/utils/create-notice.ts +++ b/packages/checkout/utils/create-notice.ts @@ -8,18 +8,6 @@ import { } from '@woocommerce/block-data'; import { select, dispatch } from '@wordpress/data'; -/** - * List of valid notice contexts included in blocks. - */ -export const noticeContexts = { - CART: 'wc/cart', - CHECKOUT: 'wc/checkout', - PAYMENTS: 'wc/checkout/payments', - EXPRESS_PAYMENTS: 'wc/checkout/express-payments', - SHIPPING_ADDRESS: 'wc/checkout/shipping-address', - SHIPPING_METHODS: 'wc/checkout/shipping-methods', -}; - /** * Wrapper for @wordpress/notices createNotice. * @@ -45,9 +33,9 @@ export const createNotice = ( // If the container ref was not registered, use the parent context instead. if ( ! Object.keys( containerRefs ).includes( noticeContext ) ) { if ( noticeContext.includes( 'wc/checkout/' ) ) { - noticeContext = noticeContexts.CHECKOUT; + noticeContext = 'wc/checkout'; } else if ( noticeContext.includes( 'wc/cart/' ) ) { - noticeContext = noticeContexts.CART; + noticeContext = 'wc/cart'; } else { noticeContext = 'wc/global'; } From d9c21d833145ec93538d3ae81069eec5d2716b28 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 11:23:35 +0000 Subject: [PATCH 08/34] Add new error contexts --- assets/js/base/context/event-emit/utils.ts | 3 + assets/js/blocks/checkout/block.tsx | 13 +++- .../checkout-actions-block/block.tsx | 19 ++++-- .../checkout-actions-block/style.scss | 26 ++++---- .../checkout-billing-address-block/block.tsx | 65 ++++++++++--------- .../block.tsx | 8 ++- assets/js/utils/notices.ts | 14 ++-- .../snackbar-notices.tsx | 6 +- .../store-notices-container/style.scss | 8 ++- 9 files changed, 105 insertions(+), 57 deletions(-) diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index d15a127af38..512882535d4 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -30,8 +30,11 @@ export enum noticeContexts { CHECKOUT = 'wc/checkout', PAYMENTS = 'wc/checkout/payments', EXPRESS_PAYMENTS = 'wc/checkout/express-payments', + CONTACT_INFO = 'wc/checkout/contact-info', SHIPPING_ADDRESS = 'wc/checkout/shipping-address', + BILLING_ADDRESS = 'wc/checkout/billing-address', SHIPPING_METHODS = 'wc/checkout/shipping-methods', + CHECKOUT_ACTIONS = 'wc/checkout/checkout-actions', } export interface ResponseType extends Record< string, unknown > { diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 004e94c70b0..13bc73b9178 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -31,7 +31,7 @@ import CheckoutOrderError from './checkout-order-error'; import { LOGIN_TO_CHECKOUT_URL, isLoginRequired, reloadPage } from './utils'; import type { Attributes } from './types'; import { CheckoutBlockContext } from './context'; -import { hasNoticesOfType } from '../../utils/notices'; +import { hasNoticesOfType, removeNoticesByStatus } from '../../utils/notices'; const LoginPrompt = () => { return ( @@ -185,6 +185,17 @@ const Block = ( { showErrorMessage={ CURRENT_USER_IS_ADMIN } > + + + + { Object.values( noticeContexts ).map( ( contextValue ) => { + return ( + ); - } } - > - { __( 'Clear all errors', 'woo-gutenberg-products-block' ) } - - - - { Object.values( noticeContexts ).map( ( contextValue ) => { - return ( - - ); - } ) } + } ) } + { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index fdd468117fe..c74a62d89cb 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -19,30 +19,45 @@ import type { const StoreNoticesContainer = ( { className = '', context = 'default', + forceType = null, + showGlobal = false, additionalNotices = [], }: StoreNoticesContainerProps ): JSX.Element | null => { const suppressNotices = useSelect( ( select ) => select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive() ); - const notices = useSelect< Array< NoticeType & NoticeOptions > >( - ( select ) => - select( 'core/notices' ).getNotices( context ) as Array< - NoticeType & NoticeOptions - > + let notices = useSelect< Array< NoticeType & NoticeOptions > >( + ( select ) => { + const { getNotices } = select( 'core/notices' ); + + const contextNotices = getNotices( context ); + const globalNotices = showGlobal ? getNotices( 'wc/global' ) : []; + + return [ ...contextNotices, ...globalNotices ].filter( + Boolean + ) as Array< NoticeType & NoticeOptions >; + } ); if ( suppressNotices ) { return null; } + if ( forceType !== null ) { + notices = notices.map( ( notice ) => ( { + ...notice, + type: forceType, + } ) ); + } + return ( <> notice.type !== 'snackbar' ) + .filter( ( notice ) => notice.type === 'default' ) .concat( additionalNotices ) } /> Date: Fri, 18 Nov 2022 11:50:05 +0000 Subject: [PATCH 10/34] Unnecessary reorder of imports --- .../payment-methods/express-payment/cart-express-payment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js index 22e43edc394..19bd9b914ed 100644 --- a/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js +++ b/assets/js/blocks/cart-checkout-shared/payment-methods/express-payment/cart-express-payment.js @@ -3,8 +3,8 @@ */ import { __ } from '@wordpress/i18n'; import { useExpressPaymentMethods } from '@woocommerce/base-context/hooks'; -import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; import { noticeContexts } from '@woocommerce/base-context'; +import { StoreNoticesContainer } from '@woocommerce/blocks-checkout'; import LoadingMask from '@woocommerce/base-components/loading-mask'; import { useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY } from '@woocommerce/block-data'; From f89919bda8b9ec611d53dd86940ea6abb8db4381 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Fri, 18 Nov 2022 12:59:34 +0000 Subject: [PATCH 11/34] Fix global handling --- assets/js/base/context/event-emit/utils.ts | 1 + assets/js/blocks/checkout/block.tsx | 2 +- .../store-notices-container/index.tsx | 65 +++++++++++-------- .../snackbar-notices.tsx | 8 +-- .../store-notices-container/store-notices.tsx | 18 ++--- .../store-notices-container/types.ts | 12 +--- 6 files changed, 56 insertions(+), 50 deletions(-) diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index 512882535d4..aacb356d9ec 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -26,6 +26,7 @@ export enum responseTypes { } export enum noticeContexts { + GLOBAL = 'wc/global', CART = 'wc/cart', CHECKOUT = 'wc/checkout', PAYMENTS = 'wc/checkout/payments', diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index 9a7157be2b1..cc601b214fb 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -186,8 +186,8 @@ const Block = ( { > <>
diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts index 76392a82aaa..30fc7f10f81 100644 --- a/packages/checkout/components/store-notices-container/types.ts +++ b/packages/checkout/components/store-notices-container/types.ts @@ -6,20 +6,12 @@ import type { Options as NoticeOptions, } from '@wordpress/notices'; -export interface StoreNotice { - content: string; - id: string; - status: string; - isDismissible?: boolean; - type: 'default' | 'snackbar'; -} - export interface StoreNoticesContainerProps { className?: string; context?: string; - additionalNotices?: ( NoticeType & NoticeOptions )[]; showGlobal: boolean; + additionalNotices?: ( NoticeType & NoticeOptions )[]; forceType: 'default' | 'snackbar' | null; } -export { NoticeType, NoticeOptions }; +export type StoreNotice = NoticeType & NoticeOptions; From 7b072448ab3dce04563f3e2beb4c8f8825b8a618 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:18:37 +0000 Subject: [PATCH 12/34] Document forceType --- packages/checkout/components/store-notices-container/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts index 30fc7f10f81..46b143532a9 100644 --- a/packages/checkout/components/store-notices-container/types.ts +++ b/packages/checkout/components/store-notices-container/types.ts @@ -9,8 +9,11 @@ import type { export interface StoreNoticesContainerProps { className?: string; context?: string; + // If true, notices under the `wc/global` context will be shown as well as notices in the given context. showGlobal: boolean; + // List of additional notices that were added inline and not stored in the `core/notices` store. additionalNotices?: ( NoticeType & NoticeOptions )[]; + // Passing forceType will force all notices in the context to be of a certain type. e.g. Regular notices would be displayed as Snack Bars. forceType: 'default' | 'snackbar' | null; } From 7881fefe85a1e35ef2111c4abe93b49319eeac68 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:19:16 +0000 Subject: [PATCH 13/34] Optional props are undefined --- packages/checkout/components/store-notices-container/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/checkout/components/store-notices-container/types.ts b/packages/checkout/components/store-notices-container/types.ts index 46b143532a9..147afa385ad 100644 --- a/packages/checkout/components/store-notices-container/types.ts +++ b/packages/checkout/components/store-notices-container/types.ts @@ -7,8 +7,8 @@ import type { } from '@wordpress/notices'; export interface StoreNoticesContainerProps { - className?: string; - context?: string; + className?: string | undefined; + context?: string | undefined; // If true, notices under the `wc/global` context will be shown as well as notices in the given context. showGlobal: boolean; // List of additional notices that were added inline and not stored in the `core/notices` store. From f3846020430cd1e57ad5aa6beadc5f594f57549e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:20:27 +0000 Subject: [PATCH 14/34] Remove function name --- .../components/store-notices-container/store-notices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index a050627c17a..037cd22cad2 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -34,7 +34,7 @@ const StoreNotices = ( { // Register the container with the parent. useEffect( () => { registerContainer( context, ref ); - return function cleanup() { + return () => { unregisterContainer( context ); }; }, [ context, ref, registerContainer, unregisterContainer ] ); From ea0412d47d8183d91e1906e01ba512b446be8777 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:48:35 +0000 Subject: [PATCH 15/34] Missing condition --- .../components/store-notices-container/snackbar-notices.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/checkout/components/store-notices-container/snackbar-notices.tsx b/packages/checkout/components/store-notices-container/snackbar-notices.tsx index 85ba8e05495..beb82c68c57 100644 --- a/packages/checkout/components/store-notices-container/snackbar-notices.tsx +++ b/packages/checkout/components/store-notices-container/snackbar-notices.tsx @@ -41,7 +41,7 @@ const SnackbarNotices = ( { notices.forEach( ( notice ) => { if ( notice.explicitDismiss && notice.id === noticeId ) { removeNotice( notice.id, notice.context ); - } else { + } else if ( ! notice.explicitDismiss ) { removeNotice( notice.id, notice.context ); } } ); From be6868d766baec6c572c7c184dfc295278582d19 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:48:44 +0000 Subject: [PATCH 16/34] Remove context prop --- packages/checkout/components/store-notices-container/index.tsx | 1 - .../components/store-notices-container/snackbar-notices.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index c71ad0b7364..412c1c3a134 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -75,7 +75,6 @@ const StoreNoticesContainer = ( { /> notice.type === 'snackbar' ) } diff --git a/packages/checkout/components/store-notices-container/snackbar-notices.tsx b/packages/checkout/components/store-notices-container/snackbar-notices.tsx index beb82c68c57..f5b591cbb75 100644 --- a/packages/checkout/components/store-notices-container/snackbar-notices.tsx +++ b/packages/checkout/components/store-notices-container/snackbar-notices.tsx @@ -12,10 +12,8 @@ import type { StoreNotice } from './types'; const SnackbarNotices = ( { className, - context, notices, }: { - context: string; className: string; notices: StoreNotice[]; } ): JSX.Element | null => { From b26e88c40e87764fa1c46e3b8a877e0a67e80852 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:51:27 +0000 Subject: [PATCH 17/34] Define ACTION_TYPES --- assets/js/data/store-notices/action-types.ts | 4 ++++ assets/js/data/store-notices/actions.ts | 9 +++++++-- assets/js/data/store-notices/reducers.ts | 5 +++-- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 assets/js/data/store-notices/action-types.ts diff --git a/assets/js/data/store-notices/action-types.ts b/assets/js/data/store-notices/action-types.ts new file mode 100644 index 00000000000..1b8b1dd9b52 --- /dev/null +++ b/assets/js/data/store-notices/action-types.ts @@ -0,0 +1,4 @@ +export enum ACTION_TYPES { + REGISTER_CONTAINER = 'REGISTER_CONTAINER', + UNREGISTER_CONTAINER = 'UNREGISTER_CONTAINER', +} diff --git a/assets/js/data/store-notices/actions.ts b/assets/js/data/store-notices/actions.ts index 9654afd0542..a99fa758121 100644 --- a/assets/js/data/store-notices/actions.ts +++ b/assets/js/data/store-notices/actions.ts @@ -1,9 +1,14 @@ +/** + * Internal dependencies + */ +import { ACTION_TYPES } from './action-types'; + export const registerContainer = ( containerContext: string, ref: React.MutableRefObject< HTMLDivElement | null > ) => { return { - type: 'REGISTER_CONTAINER', + type: ACTION_TYPES.REGISTER_CONTAINER, containerContext, ref, }; @@ -11,7 +16,7 @@ export const registerContainer = ( export const unregisterContainer = ( containerContext: string ) => { return { - type: 'UNREGISTER_CONTAINER', + type: ACTION_TYPES.UNREGISTER_CONTAINER, containerContext, }; }; diff --git a/assets/js/data/store-notices/reducers.ts b/assets/js/data/store-notices/reducers.ts index 44e3fee0b92..234dcf5c678 100644 --- a/assets/js/data/store-notices/reducers.ts +++ b/assets/js/data/store-notices/reducers.ts @@ -7,13 +7,14 @@ import type { Reducer } from 'redux'; * Internal dependencies */ import { defaultStoreNoticesState, StoreNoticesState } from './default-state'; +import { ACTION_TYPES } from './action-types'; const reducer: Reducer< StoreNoticesState > = ( state = defaultStoreNoticesState, action ) => { switch ( action.type ) { - case 'REGISTER_CONTAINER': + case ACTION_TYPES.REGISTER_CONTAINER: return { ...state, containers: { @@ -21,7 +22,7 @@ const reducer: Reducer< StoreNoticesState > = ( [ action.containerContext ]: action.ref, }, }; - case 'UNREGISTER_CONTAINER': + case ACTION_TYPES.UNREGISTER_CONTAINER: const { [ action.containerContext ]: _, ...containers } = state.containers; return { From 35d584e1c85197575ad0d694f9eba591d1522472 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:52:10 +0000 Subject: [PATCH 18/34] Remove controls --- assets/js/data/store-notices/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/assets/js/data/store-notices/index.ts b/assets/js/data/store-notices/index.ts index efa9a208cd9..8ac6c59cf71 100644 --- a/assets/js/data/store-notices/index.ts +++ b/assets/js/data/store-notices/index.ts @@ -2,7 +2,6 @@ * External dependencies */ import { createReduxStore, register } from '@wordpress/data'; -import { controls } from '@wordpress/data-controls'; /** * Internal dependencies @@ -17,7 +16,6 @@ const config = { reducer, actions, selectors, - controls, }; const store = createReduxStore( STORE_KEY, config ); register( store ); From 6ffc04289451eae98569e8e06073cb3e80f2a4e6 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:53:40 +0000 Subject: [PATCH 19/34] Update assets/js/base/context/event-emit/utils.ts Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> --- assets/js/base/context/event-emit/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/base/context/event-emit/utils.ts b/assets/js/base/context/event-emit/utils.ts index aacb356d9ec..03f182512bb 100644 --- a/assets/js/base/context/event-emit/utils.ts +++ b/assets/js/base/context/event-emit/utils.ts @@ -31,7 +31,7 @@ export enum noticeContexts { CHECKOUT = 'wc/checkout', PAYMENTS = 'wc/checkout/payments', EXPRESS_PAYMENTS = 'wc/checkout/express-payments', - CONTACT_INFO = 'wc/checkout/contact-info', + CONTACT_INFORMATION = 'wc/checkout/contact-information', SHIPPING_ADDRESS = 'wc/checkout/shipping-address', BILLING_ADDRESS = 'wc/checkout/billing-address', SHIPPING_METHODS = 'wc/checkout/shipping-methods', From c129e057182fc5bd7dabf7ff028e9cf258fb2851 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 10:54:32 +0000 Subject: [PATCH 20/34] CONTACT_INFORMATION --- .../inner-blocks/checkout-contact-information-block/block.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx index 7790fdc920b..24102f746b7 100644 --- a/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx +++ b/assets/js/blocks/checkout/inner-blocks/checkout-contact-information-block/block.tsx @@ -58,7 +58,9 @@ const Block = ( { return ( <> - + Date: Mon, 21 Nov 2022 11:10:44 +0000 Subject: [PATCH 21/34] Remove ref from registerContainer --- assets/js/data/store-notices/actions.ts | 6 +----- assets/js/data/store-notices/default-state.ts | 7 ++----- assets/js/data/store-notices/reducers.ts | 12 +++++------ .../store-notices-container/store-notices.tsx | 21 +++++++++---------- .../store-notices-container/types.ts | 4 ++-- 5 files changed, 20 insertions(+), 30 deletions(-) diff --git a/assets/js/data/store-notices/actions.ts b/assets/js/data/store-notices/actions.ts index a99fa758121..1ffcc465866 100644 --- a/assets/js/data/store-notices/actions.ts +++ b/assets/js/data/store-notices/actions.ts @@ -3,14 +3,10 @@ */ import { ACTION_TYPES } from './action-types'; -export const registerContainer = ( - containerContext: string, - ref: React.MutableRefObject< HTMLDivElement | null > -) => { +export const registerContainer = ( containerContext: string ) => { return { type: ACTION_TYPES.REGISTER_CONTAINER, containerContext, - ref, }; }; diff --git a/assets/js/data/store-notices/default-state.ts b/assets/js/data/store-notices/default-state.ts index 502763f8694..90174b2ed3f 100644 --- a/assets/js/data/store-notices/default-state.ts +++ b/assets/js/data/store-notices/default-state.ts @@ -1,10 +1,7 @@ export interface StoreNoticesState { - containers: Record< - string, - React.MutableRefObject< HTMLDivElement | null > - >; + containers: string[]; } export const defaultStoreNoticesState: StoreNoticesState = { - containers: {}, + containers: [], }; diff --git a/assets/js/data/store-notices/reducers.ts b/assets/js/data/store-notices/reducers.ts index 234dcf5c678..a7b0643da36 100644 --- a/assets/js/data/store-notices/reducers.ts +++ b/assets/js/data/store-notices/reducers.ts @@ -17,17 +17,15 @@ const reducer: Reducer< StoreNoticesState > = ( case ACTION_TYPES.REGISTER_CONTAINER: return { ...state, - containers: { - ...state.containers, - [ action.containerContext ]: action.ref, - }, + containers: [ ...state.containers, action.containerContext ], }; case ACTION_TYPES.UNREGISTER_CONTAINER: - const { [ action.containerContext ]: _, ...containers } = - state.containers; + const newContainers = state.containers.filter( + ( container ) => container !== action.containerContext + ); return { ...state, - containers, + containers: newContainers, }; } return state; diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index 037cd22cad2..07e99d70b5c 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -25,25 +25,16 @@ const StoreNotices = ( { className: string; notices: StoreNotice[]; } ): JSX.Element => { - const { removeNotice } = useDispatch( 'core/notices' ); const ref = useRef< HTMLDivElement >( null ); + const { removeNotice } = useDispatch( 'core/notices' ); const { registerContainer, unregisterContainer } = useDispatch( STORE_NOTICES_STORE_KEY ); - - // Register the container with the parent. - useEffect( () => { - registerContainer( context, ref ); - return () => { - unregisterContainer( context ); - }; - }, [ context, ref, registerContainer, unregisterContainer ] ); - - // Scroll to container when an error is added here. const noticeIds = notices.map( ( notice ) => notice.id ); const previousNoticeIds = usePrevious( noticeIds ); useEffect( () => { + // Scroll to container when an error is added here. const newNoticeIds = noticeIds.filter( ( value ) => ! previousNoticeIds || ! previousNoticeIds.includes( value ) @@ -56,6 +47,14 @@ const StoreNotices = ( { } }, [ noticeIds, previousNoticeIds, ref ] ); + // Register the container context with the parent. + useEffect( () => { + registerContainer( context ); + return () => { + unregisterContainer( context ); + }; + }, [ context, registerContainer, unregisterContainer ] ); + return (
Date: Mon, 21 Nov 2022 11:19:37 +0000 Subject: [PATCH 22/34] Abstract container locating methods --- packages/checkout/utils/create-notice.ts | 46 ++++++++++++++---------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/checkout/utils/create-notice.ts b/packages/checkout/utils/create-notice.ts index 78dd606796f..4237d6bded0 100644 --- a/packages/checkout/utils/create-notice.ts +++ b/packages/checkout/utils/create-notice.ts @@ -8,6 +8,29 @@ import { } from '@woocommerce/block-data'; import { select, dispatch } from '@wordpress/data'; +const DEFAULT_CONTEXT = 'wc/global'; + +const hasContainer = ( container: string ): boolean => { + const containers = select( STORE_NOTICES_STORE_KEY ).getContainers(); + return containers.includes( container ); +}; + +const findParentContainer = ( container: string ): string => { + let parentContainer = DEFAULT_CONTEXT; + if ( + container.includes( 'wc/checkout/' ) && + hasContainer( 'wc/checkout' ) + ) { + parentContainer = 'wc/checkout'; + } else if ( + container.includes( 'wc/cart/' ) && + hasContainer( 'wc/cart' ) + ) { + parentContainer = 'wc/cart'; + } + return parentContainer; +}; + /** * Wrapper for @wordpress/notices createNotice. * @@ -19,7 +42,7 @@ export const createNotice = ( message: string, options: Partial< NoticeOptions > ) => { - let noticeContext = options?.context || 'wc'; + let noticeContext = options?.context || DEFAULT_CONTEXT; const suppressNotices = select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive(); @@ -28,24 +51,9 @@ export const createNotice = ( return; } - const containerRefs = select( STORE_NOTICES_STORE_KEY ).getContainers(); - const registeredContext = Object.keys( containerRefs ); - - // If the container ref was not registered, use the parent context instead. - if ( ! registeredContext.includes( noticeContext ) ) { - if ( - noticeContext.includes( 'wc/checkout/' ) && - registeredContext.includes( 'wc/checkout' ) - ) { - noticeContext = 'wc/checkout'; - } else if ( - noticeContext.includes( 'wc/cart/' ) && - registeredContext.includes( 'wc/cart' ) - ) { - noticeContext = 'wc/cart'; - } else { - noticeContext = 'wc/global'; - } + if ( ! hasContainer( noticeContext ) ) { + // If the container ref was not registered, use the parent context instead. + noticeContext = findParentContainer( noticeContext ); } const { createNotice: dispatchCreateNotice } = dispatch( 'core/notices' ); From 4efa4af473cd292411d708d5dc3888476619bb00 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 11:44:17 +0000 Subject: [PATCH 23/34] pass context correctly when displaying notices --- .../components/store-notices-container/index.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/checkout/components/store-notices-container/index.tsx b/packages/checkout/components/store-notices-container/index.tsx index 412c1c3a134..34459e6ae16 100644 --- a/packages/checkout/components/store-notices-container/index.tsx +++ b/packages/checkout/components/store-notices-container/index.tsx @@ -17,13 +17,11 @@ const formatNotices = ( forceType: 'default' | 'snackbar' | null = null, context: string ): StoreNotice[] => { - return forceType !== null - ? notices.map( ( notice ) => ( { - ...notice, - type: forceType, - context, - } ) ) - : notices; + return notices.map( ( notice ) => ( { + ...notice, + type: forceType !== null ? forceType : notice.type, + context, + } ) ); }; const StoreNoticesContainer = ( { From e4a05f99dcb688c4969974a69d2a60e37b4bf4a2 Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 12:46:23 +0000 Subject: [PATCH 24/34] Remove debugging buttons --- assets/js/blocks/checkout/block.tsx | 63 +---------------------------- 1 file changed, 2 insertions(+), 61 deletions(-) diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index cc601b214fb..f2a4de004e9 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -12,7 +12,6 @@ import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings'; import { SlotFillProvider, StoreNoticesContainer, - createNotice, } from '@woocommerce/blocks-checkout'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -20,7 +19,6 @@ import { CHECKOUT_STORE_KEY, VALIDATION_STORE_KEY, } from '@woocommerce/block-data'; -import { Button } from '@wordpress/components'; /** * Internal dependencies @@ -31,7 +29,7 @@ import CheckoutOrderError from './checkout-order-error'; import { LOGIN_TO_CHECKOUT_URL, isLoginRequired, reloadPage } from './utils'; import type { Attributes } from './types'; import { CheckoutBlockContext } from './context'; -import { hasNoticesOfType, removeNoticesByStatus } from '../../utils/notices'; +import { hasNoticesOfType } from '../../utils/notices'; const LoginPrompt = () => { return ( @@ -130,9 +128,7 @@ const ScrollOnError = ( { const { showAllValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); const hasErrorsToDisplay = - checkoutIsIdle && - checkoutHasError && - ( hasValidationErrors || hasNoticesOfType( 'wc/checkout', 'default' ) ); + checkoutIsIdle && checkoutHasError && hasValidationErrors; useEffect( () => { let scrollToTopTimeout: number; @@ -189,61 +185,6 @@ const Block = ( { showGlobal={ true } forceType="snackbar" /> - <> - - - - { Object.values( noticeContexts ).map( ( contextValue ) => { - return ( - - ); - } ) } - { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } From 39fa3afaaaca593de145f6977f9bd212ac3fa10e Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Mon, 21 Nov 2022 13:38:31 +0000 Subject: [PATCH 25/34] Update filter usage - remove useMemo so filter can work inline --- .../hooks/cart/use-store-cart-coupons.ts | 54 +++++------- .../checkout-block/available-filters.md | 74 +++------------- packages/checkout/filter-registry/index.ts | 85 ++++++++----------- 3 files changed, 68 insertions(+), 145 deletions(-) diff --git a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts index f72ffa10753..3fdc41b5c75 100644 --- a/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts +++ b/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts @@ -1,14 +1,9 @@ -/** @typedef { import('@woocommerce/type-defs/hooks').StoreCartCoupon } StoreCartCoupon */ - /** * External dependencies */ import { __, sprintf } from '@wordpress/i18n'; import { useDispatch, useSelect } from '@wordpress/data'; -import { - CART_STORE_KEY as storeKey, - VALIDATION_STORE_KEY, -} from '@woocommerce/block-data'; +import { CART_STORE_KEY, VALIDATION_STORE_KEY } from '@woocommerce/block-data'; import { decodeEntities } from '@wordpress/html-entities'; import type { StoreCartCoupon } from '@woocommerce/types'; import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout'; @@ -22,9 +17,6 @@ import { useStoreCart } from './use-store-cart'; * This is a custom hook for loading the Store API /cart/coupons endpoint and an * action for adding a coupon _to_ the cart. * See also: https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/trunk/src/RestApi/StoreApi - * - * @return {StoreCartCoupon} An object exposing data and actions from/for the - * store api /cart/coupons endpoint. */ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { const { cartCoupons, cartIsLoading } = useStoreCart(); @@ -32,29 +24,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { const { createNotice } = useDispatch( 'core/notices' ); const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); - const { - applyCoupon, - removeCoupon, - isApplyingCoupon, - isRemovingCoupon, - }: Pick< - StoreCartCoupon, - | 'applyCoupon' - | 'removeCoupon' - | 'isApplyingCoupon' - | 'isRemovingCoupon' - | 'receiveApplyingCoupon' - > = useSelect( - ( select, { dispatch } ) => { - const store = select( storeKey ); - const actions = dispatch( storeKey ); - + const { applyCoupon, removeCoupon, receiveApplyingCoupon } = + useDispatch( CART_STORE_KEY ); + const { isApplyingCoupon, isRemovingCoupon } = useSelect( + ( select ) => { + const store = select( CART_STORE_KEY ); return { - applyCoupon: actions.applyCoupon, - removeCoupon: actions.removeCoupon, isApplyingCoupon: store.isApplyingCoupon(), isRemovingCoupon: store.isRemovingCoupon(), - receiveApplyingCoupon: actions.receiveApplyingCoupon, }; }, [ createErrorNotice, createNotice ] @@ -65,11 +42,11 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { .then( ( result ) => { if ( result === true && - __experimentalApplyCheckoutFilter( - 'showApplyCouponNotice', - true, - { couponCode, context } - ) + __experimentalApplyCheckoutFilter( { + filterName: 'showApplyCouponNotice', + defaultValue: true, + arg: { couponCode, context }, + } ) ) { createNotice( 'info', @@ -104,7 +81,14 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { const removeCouponWithNotices = ( couponCode: string ) => { removeCoupon( couponCode ) .then( ( result ) => { - if ( result === true ) { + if ( + result === true && + __experimentalApplyCheckoutFilter( { + filterName: 'showRemoveCouponNotice', + defaultValue: true, + arg: { couponCode, context }, + } ) + ) { createNotice( 'info', sprintf( diff --git a/docs/third-party-developers/extensibility/checkout-block/available-filters.md b/docs/third-party-developers/extensibility/checkout-block/available-filters.md index d9262204fe2..ee0dddf0139 100644 --- a/docs/third-party-developers/extensibility/checkout-block/available-filters.md +++ b/docs/third-party-developers/extensibility/checkout-block/available-filters.md @@ -2,19 +2,18 @@ ## Table of Contents -- [Cart Line Items](#cart-line-items) -- [Order Summary Items](#order-summary-items) -- [Totals footer item (in Mini Cart, Cart and Checkout)](#totals-footer-item-in-mini-cart-cart-and-checkout) -- [Coupons](#coupons) -- [Snackbar notices](#snackbar-notices) -- [Place Order Button Label](#place-order-button-label) -- [Examples](#examples) - - [Changing the wording of the Totals label in the Mini Cart, Cart and Checkout](#changing-the-wording-of-the-totals-label-in-the-mini-cart-cart-and-checkout) - - [Changing the format of the item's single price](#changing-the-format-of-the-items-single-price) - - [Change the name of a coupon](#change-the-name-of-a-coupon) - - [Hide a snackbar notice containing a certain string](#hide-a-snackbar-notice-containing-a-certain-string) - - [Change the label of the Place Order button](#change-the-label-of-the-place-order-button) -- [Troubleshooting](#troubleshooting) +- [Cart Line Items](#cart-line-items) +- [Order Summary Items](#order-summary-items) +- [Totals footer item (in Mini Cart, Cart and Checkout)](#totals-footer-item-in-mini-cart-cart-and-checkout) +- [Coupons](#coupons) +- [Place Order Button Label](#place-order-button-label) +- [Examples](#examples) + - [Changing the wording of the Totals label in the Mini Cart, Cart and Checkout](#changing-the-wording-of-the-totals-label-in-the-mini-cart-cart-and-checkout) + - [Changing the format of the item's single price](#changing-the-format-of-the-items-single-price) + - [Change the name of a coupon](#change-the-name-of-a-coupon) + - [Hide the "Remove item" link on a cart item](#hide-the-remove-item-link-on-a-cart-item) + - [Change the label of the Place Order button](#change-the-label-of-the-place-order-button) +- [Troubleshooting](#troubleshooting) This document lists the filters that are currently available to extensions and offers usage information for each one of them. Information on registering filters can be found on the [Checkout - Filter Registry](../../../../packages/checkout/filter-registry/README.md) page. @@ -27,7 +26,7 @@ Line items refer to each item listed in the cart or checkout. For instance, the The following filters are available for line items: | Filter name | Description | Return type | -| ---------------------- |----------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------| +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `itemName` | Used to change the name of the item before it is rendered onto the page | `string` | | `cartItemPrice` | This is the price of the item, multiplied by the number of items in the cart. | `string` and **must** contain the substring `` where the price should appear. | | `cartItemClass` | This is the className of the item cell. | `string` | @@ -91,31 +90,6 @@ CartCoupon { } ``` -## Snackbar notices - -There is a snackbar at the bottom of the page used to display notices to the customer, it looks like this: - -![Snackbar notices](https://user-images.githubusercontent.com/5656702/120882329-d573c100-c5ce-11eb-901b-d7f206f74a66.png) - -It may be desirable to hide this if there's a notice you don't want the shopper to see. - -| Filter name | Description | Return type | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| `snackbarNoticeVisibility` | An object keyed by the content of the notices slated to be displayed. The value of each member of this object will initially be true. | `object` | - -The filter passes an object whose keys are the `content` of each notice. - -If there are two notices slated to be displayed ('Coupon code "10off" has been applied to your basket.', and 'Coupon code "50off" has been removed from your basket.'), the value passed to the filter would look like so: - -```js -{ - 'Coupon code "10off" has been applied to your basket.': true, - 'Coupon code "50off" has been removed from your basket.': true -} -``` - -To reiterate, the _value_ here will determine whether this notice gets displayed or not. It will display if true. - ## Place Order Button Label The Checkout block contains a button which is labelled 'Place Order' by default, but can be changed using the following filter. @@ -212,29 +186,10 @@ __experimentalRegisterCheckoutFilters( 'automatic-coupon-extension', { | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | ![image](https://user-images.githubusercontent.com/5656702/123768988-bc55eb80-d8c0-11eb-9262-5d629837706d.png) | ![image](https://user-images.githubusercontent.com/5656702/124126048-2c57a380-da72-11eb-9b45-b2cae0cffc37.png) | -### Hide a snackbar notice containing a certain string - -Let's say we want to hide all notices that contain the string `auto-generated-coupon`. We would do this by setting the value of the `snackbarNoticeVisibility` to false for the notices we would like to hide. - -```ts -import { __experimentalRegisterCheckoutFilters } from '@woocommerce/blocks-checkout'; - -__experimentalRegisterCheckoutFilters( 'automatic-coupon-extension', { - snackbarNoticeVisibility: ( value ) => { - // Copy the value so we don't mutate what is being passed by the filter. - const valueCopy = Object.assign( {}, value ); - Object.keys( value ).forEach( ( key ) => { - valueCopy[ key ] = key.indexOf( 'auto-generated-coupon' ) === -1; - } ); - return valueCopy; - }, -} ); -``` - ### Hide the "Remove item" link on a cart item If you want to stop customers from being able to remove a specific item from their cart **on the front end**, you can do -this by using the `showRemoveItemLink` filter. If it returns `false` for that line item the link will not show. +this by using the `showRemoveItemLink` filter. If it returns `false` for that line item the link will not show. An important caveat to note is this does _not_ prevent the item from being removed from the cart using StoreAPI or by removing it in the Mini Cart, or traditional shortcode cart. @@ -291,4 +246,3 @@ The error will also be shown in your console. 🐞 Found a mistake, or have a suggestion? [Leave feedback about this document here.](https://github.com/woocommerce/woocommerce-blocks/issues/new?assignees=&labels=type%3A+documentation&template=--doc-feedback.md&title=Feedback%20on%20./docs/third-party-developers/extensibility/checkout-block/available-filters.md) - diff --git a/packages/checkout/filter-registry/index.ts b/packages/checkout/filter-registry/index.ts index 312b5fd420a..058b7d70cc8 100644 --- a/packages/checkout/filter-registry/index.ts +++ b/packages/checkout/filter-registry/index.ts @@ -41,19 +41,6 @@ export const __experimentalRegisterCheckoutFilters = ( namespace: string, filters: Record< string, CheckoutFilterFunction > ): void => { - /** - * Let developers know snackbarNotices is no longer available as a filter. - * - * See: https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4417 - */ - if ( Object.keys( filters ).includes( 'couponName' ) ) { - deprecated( 'snackbarNotices', { - alternative: 'snackbarNoticeVisibility', - plugin: 'WooCommerce Blocks', - link: 'https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/4417', - } ); - } - /** * Let the user know couponName is no longer available as a filter. * @@ -211,42 +198,40 @@ export const __experimentalApplyCheckoutFilter = < T >( { /** Function that needs to return true when the filtered value is passed in order for the filter to be applied. */ validation?: ( value: T ) => true | Error; } ): T => { - return useMemo( () => { - if ( - ! shouldReRunFilters( filterName, arg, extensions, defaultValue ) && - cachedValues[ filterName ] !== undefined - ) { - return cachedValues[ filterName ]; - } - const filters = getCheckoutFilters( filterName ); - let value = defaultValue; - filters.forEach( ( filter ) => { - try { - const newValue = filter( value, extensions || {}, arg ); - if ( typeof newValue !== typeof value ) { - throw new Error( - sprintf( - /* translators: %1$s is the type of the variable passed to the filter function, %2$s is the type of the value returned by the filter function. */ - __( - 'The type returned by checkout filters must be the same as the type they receive. The function received %1$s but returned %2$s.', - 'woo-gutenberg-products-block' - ), - typeof value, - typeof newValue - ) - ); - } - value = validation( newValue ) ? newValue : value; - } catch ( e ) { - if ( CURRENT_USER_IS_ADMIN ) { - throw e; - } else { - // eslint-disable-next-line no-console - console.error( e ); - } + if ( + ! shouldReRunFilters( filterName, arg, extensions, defaultValue ) && + cachedValues[ filterName ] !== undefined + ) { + return cachedValues[ filterName ]; + } + const filters = getCheckoutFilters( filterName ); + let value = defaultValue; + filters.forEach( ( filter ) => { + try { + const newValue = filter( value, extensions || {}, arg ); + if ( typeof newValue !== typeof value ) { + throw new Error( + sprintf( + /* translators: %1$s is the type of the variable passed to the filter function, %2$s is the type of the value returned by the filter function. */ + __( + 'The type returned by checkout filters must be the same as the type they receive. The function received %1$s but returned %2$s.', + 'woo-gutenberg-products-block' + ), + typeof value, + typeof newValue + ) + ); } - } ); - cachedValues[ filterName ] = value; - return value; - }, [ arg, defaultValue, extensions, filterName, validation ] ); + value = validation( newValue ) ? newValue : value; + } catch ( e ) { + if ( CURRENT_USER_IS_ADMIN ) { + throw e; + } else { + // eslint-disable-next-line no-console + console.error( e ); + } + } + } ); + cachedValues[ filterName ] = value; + return value; }; From 3530786afc18de3d132ce3b269e58a2c0198cb5a Mon Sep 17 00:00:00 2001 From: Mike Jolley Date: Wed, 23 Nov 2022 16:00:09 +0000 Subject: [PATCH 26/34] Refactor existing error notices from the API (#7728) * Update API type defs * Move create notice utils * Replace useCheckoutNotices with new contexts * processCheckoutResponseHeaders should check headers are defined * Scroll to error notices only if we're not editing a field * Error handling utils * processErrorResponse when pushing changes * processErrorResponse when processing checkout * remove formatStoreApiErrorMessage * Add todo for cart errors * Remove unused deps * unused imports * Fix linting warnings * Unused dep * Update assets/js/types/type-defs/api-response.ts Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> * Add todo * Use generic * remove const * Update array types * Phone should be in address blocks Co-authored-by: Thomas Roberts <5656702+opr@users.noreply.github.com> --- assets/js/base/context/hooks/index.js | 1 - .../context/hooks/use-checkout-notices.js | 55 ------- .../cart-checkout/checkout-events/index.tsx | 35 +++-- ...out-processor.js => checkout-processor.ts} | 135 +++++++---------- .../cart-checkout/payment-events/index.tsx | 9 -- .../context/providers/cart-checkout/utils.ts | 7 +- assets/js/base/utils/create-notice.ts | 101 +++++++++++++ assets/js/base/utils/errors.js | 30 ---- assets/js/base/utils/index.js | 1 + .../filled-cart-block/frontend.tsx | 2 +- assets/js/blocks/checkout/block.tsx | 1 - assets/js/data/cart/actions.ts | 9 +- assets/js/data/cart/default-state.ts | 9 +- assets/js/data/cart/push-changes.ts | 17 +-- assets/js/data/cart/selectors.ts | 7 +- assets/js/data/checkout/types.ts | 2 +- assets/js/data/index.ts | 1 + assets/js/data/shared-controls.ts | 8 +- assets/js/data/types.ts | 49 ------ assets/js/data/utils/index.js | 1 + .../js/data/utils/process-error-response.ts | 139 ++++++++++++++++++ .../js/types/type-defs/api-error-response.ts | 28 ++++ assets/js/types/type-defs/api-response.ts | 37 +++++ assets/js/types/type-defs/checkout.ts | 13 +- assets/js/types/type-defs/hooks.ts | 20 +-- assets/js/types/type-defs/index.ts | 2 + .../store-notices-container/store-notices.tsx | 19 ++- packages/checkout/filter-registry/index.ts | 1 - packages/checkout/utils/create-notice.ts | 66 --------- packages/checkout/utils/index.js | 1 - 30 files changed, 444 insertions(+), 362 deletions(-) delete mode 100644 assets/js/base/context/hooks/use-checkout-notices.js rename assets/js/base/context/providers/cart-checkout/{checkout-processor.js => checkout-processor.ts} (74%) create mode 100644 assets/js/base/utils/create-notice.ts create mode 100644 assets/js/data/utils/process-error-response.ts create mode 100644 assets/js/types/type-defs/api-error-response.ts create mode 100644 assets/js/types/type-defs/api-response.ts delete mode 100644 packages/checkout/utils/create-notice.ts diff --git a/assets/js/base/context/hooks/index.js b/assets/js/base/context/hooks/index.js index b47f97613ff..7190de32465 100644 --- a/assets/js/base/context/hooks/index.js +++ b/assets/js/base/context/hooks/index.js @@ -8,7 +8,6 @@ export * from './use-store-products'; export * from './use-store-add-to-cart'; export * from './use-customer-data'; export * from './use-checkout-address'; -export * from './use-checkout-notices'; export * from './use-checkout-submit'; export * from './use-checkout-extension-data'; export * from './use-validation'; diff --git a/assets/js/base/context/hooks/use-checkout-notices.js b/assets/js/base/context/hooks/use-checkout-notices.js deleted file mode 100644 index c211bcabc14..00000000000 --- a/assets/js/base/context/hooks/use-checkout-notices.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * External dependencies - */ -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { noticeContexts } from '../event-emit'; - -/** - * @typedef {import('@woocommerce/type-defs/contexts').StoreNoticeObject} StoreNoticeObject - * @typedef {import('@woocommerce/type-defs/hooks').CheckoutNotices} CheckoutNotices - */ - -/** - * A hook that returns all notices visible in the Checkout block. - * - * @return {CheckoutNotices} Notices from the checkout form or payment methods. - */ -export const useCheckoutNotices = () => { - /** - * @type {StoreNoticeObject[]} - */ - const checkoutNotices = useSelect( - ( select ) => select( 'core/notices' ).getNotices( 'wc/checkout' ), - [] - ); - - /** - * @type {StoreNoticeObject[]} - */ - const expressPaymentNotices = useSelect( - ( select ) => - select( 'core/notices' ).getNotices( - noticeContexts.EXPRESS_PAYMENTS - ), - [ noticeContexts.EXPRESS_PAYMENTS ] - ); - - /** - * @type {StoreNoticeObject[]} - */ - const paymentNotices = useSelect( - ( select ) => - select( 'core/notices' ).getNotices( noticeContexts.PAYMENTS ), - [ noticeContexts.PAYMENTS ] - ); - - return { - checkoutNotices, - expressPaymentNotices, - paymentNotices, - }; -}; diff --git a/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx b/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx index 24e2e07c799..885833cbf2c 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx +++ b/assets/js/base/context/providers/cart-checkout/checkout-events/index.tsx @@ -24,10 +24,9 @@ import { * Internal dependencies */ import { useEventEmitters, reducer as emitReducer } from './event-emit'; -import type { emitterCallback } from '../../../event-emit'; +import { emitterCallback, noticeContexts } from '../../../event-emit'; import { STATUS } from '../../../../../data/checkout/constants'; import { useStoreEvents } from '../../../hooks/use-store-events'; -import { useCheckoutNotices } from '../../../hooks/use-checkout-notices'; import { CheckoutState } from '../../../../../data/checkout/default-state'; import { getExpressPaymentMethods, @@ -111,11 +110,29 @@ export const CheckoutEventsProvider = ( { } const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); - const { createErrorNotice } = useDispatch( 'core/notices' ); - const { dispatchCheckoutEvent } = useStoreEvents(); const { checkoutNotices, paymentNotices, expressPaymentNotices } = - useCheckoutNotices(); + useSelect( ( select ) => { + const { getNotices } = select( 'core/notices' ); + const checkoutContexts = Object.values( noticeContexts ).filter( + ( context ) => + context !== noticeContexts.PAYMENTS && + context !== noticeContexts.EXPRESS_PAYMENTS + ); + const allCheckoutNotices = checkoutContexts.reduce( + ( acc, context ) => { + return [ ...acc, ...getNotices( context ) ]; + }, + [] + ); + return { + checkoutNotices: allCheckoutNotices, + paymentNotices: getNotices( noticeContexts.PAYMENTS ), + expressPaymentNotices: getNotices( + noticeContexts.EXPRESS_PAYMENTS + ), + }; + }, [] ); const [ observers, observerDispatch ] = useReducer( emitReducer, {} ); const currentObservers = useRef( observers ); @@ -160,12 +177,7 @@ export const CheckoutEventsProvider = ( { setValidationErrors, } ); } - }, [ - checkoutState.status, - setValidationErrors, - createErrorNotice, - checkoutActions, - ] ); + }, [ checkoutState.status, setValidationErrors, checkoutActions ] ); const previousStatus = usePrevious( checkoutState.status ); const previousHasError = usePrevious( checkoutState.hasError ); @@ -199,7 +211,6 @@ export const CheckoutEventsProvider = ( { checkoutState.orderNotes, previousStatus, previousHasError, - createErrorNotice, checkoutNotices, expressPaymentNotices, paymentNotices, diff --git a/assets/js/base/context/providers/cart-checkout/checkout-processor.js b/assets/js/base/context/providers/cart-checkout/checkout-processor.ts similarity index 74% rename from assets/js/base/context/providers/cart-checkout/checkout-processor.js rename to assets/js/base/context/providers/cart-checkout/checkout-processor.ts index 19abb8dee56..b2bc13b13f3 100644 --- a/assets/js/base/context/providers/cart-checkout/checkout-processor.js +++ b/assets/js/base/context/providers/cart-checkout/checkout-processor.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import triggerFetch from '@wordpress/api-fetch'; import { useEffect, @@ -12,18 +12,25 @@ import { } from '@wordpress/element'; import { emptyHiddenAddressFields, - formatStoreApiErrorMessage, + removeAllNotices, } from '@woocommerce/base-utils'; import { useDispatch, useSelect } from '@wordpress/data'; import { CHECKOUT_STORE_KEY, PAYMENT_STORE_KEY, VALIDATION_STORE_KEY, + processErrorResponse, } from '@woocommerce/block-data'; import { getPaymentMethods, getExpressPaymentMethods, } from '@woocommerce/blocks-registry'; +import { + ApiResponse, + CheckoutResponseSuccess, + CheckoutResponseError, + assertResponseIsValid, +} from '@woocommerce/types'; /** * Internal dependencies @@ -41,7 +48,6 @@ import { useStoreCart } from '../../hooks/cart/use-store-cart'; */ const CheckoutProcessor = () => { const { onCheckoutValidationBeforeProcessing } = useCheckoutEventsContext(); - const { hasError: checkoutHasError, redirectUrl, @@ -60,17 +66,14 @@ const CheckoutProcessor = () => { isComplete: store.isComplete(), }; } ); - const { __internalSetHasError, __internalProcessCheckoutResponse } = useDispatch( CHECKOUT_STORE_KEY ); - const hasValidationErrors = useSelect( ( select ) => select( VALIDATION_STORE_KEY ).hasValidationErrors ); const { shippingErrorStatus } = useShippingDataContext(); const { billingAddress, shippingAddress } = useCustomerDataContext(); const { cartNeedsPayment, cartNeedsShipping, receiveCart } = useStoreCart(); - const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' ); const { activePaymentMethod, @@ -118,8 +121,8 @@ const CheckoutProcessor = () => { ( isPaymentSuccess || ! cartNeedsPayment ) && checkoutIsProcessing; - // Determine if checkout has an error. useEffect( () => { + // Determine if checkout has an error. if ( checkoutWillHaveError !== checkoutHasError && ( checkoutIsProcessing || checkoutIsBeforeProcessing ) && @@ -136,8 +139,8 @@ const CheckoutProcessor = () => { __internalSetHasError, ] ); - // Keep the billing, shipping and redirectUrl current useEffect( () => { + // Keep the billing, shipping and redirectUrl current currentBillingAddress.current = billingAddress; currentShippingAddress.current = shippingAddress; currentRedirectUrl.current = redirectUrl; @@ -167,9 +170,9 @@ const CheckoutProcessor = () => { return true; }, [ hasValidationErrors, hasPaymentError, shippingErrorStatus.hasError ] ); - // Validate the checkout using the CHECKOUT_VALIDATION_BEFORE_PROCESSING event useEffect( () => { - let unsubscribeProcessing; + // Validate the checkout using the CHECKOUT_VALIDATION_BEFORE_PROCESSING event. + let unsubscribeProcessing: () => void; if ( ! isExpressPaymentMethodActive ) { unsubscribeProcessing = onCheckoutValidationBeforeProcessing( checkValidation, @@ -177,7 +180,10 @@ const CheckoutProcessor = () => { ); } return () => { - if ( ! isExpressPaymentMethodActive ) { + if ( + ! isExpressPaymentMethodActive && + typeof unsubscribeProcessing === 'function' + ) { unsubscribeProcessing(); } }; @@ -187,8 +193,8 @@ const CheckoutProcessor = () => { isExpressPaymentMethodActive, ] ); - // Redirect when checkout is complete and there is a redirect url. useEffect( () => { + // Redirect when checkout is complete and there is a redirect url. if ( currentRedirectUrl.current ) { window.location.href = currentRedirectUrl.current; } @@ -199,8 +205,8 @@ const CheckoutProcessor = () => { if ( isProcessingOrder ) { return; } + removeAllNotices(); setIsProcessingOrder( true ); - removeNotice( 'checkout' ); const paymentData = cartNeedsPayment ? { @@ -213,97 +219,67 @@ const CheckoutProcessor = () => { } : {}; - const data = { - billing_address: emptyHiddenAddressFields( - currentBillingAddress.current - ), - customer_note: orderNotes, - create_account: shouldCreateAccount, - ...paymentData, - extensions: { ...extensionData }, - }; - - if ( cartNeedsShipping ) { - data.shipping_address = emptyHiddenAddressFields( - currentShippingAddress.current - ); - } - triggerFetch( { path: '/wc/store/v1/checkout', method: 'POST', - data, + data: { + shipping_address: cartNeedsShipping + ? emptyHiddenAddressFields( currentShippingAddress.current ) + : undefined, + billing_address: emptyHiddenAddressFields( + currentBillingAddress.current + ), + customer_note: orderNotes, + create_account: shouldCreateAccount, + ...paymentData, + extensions: { ...extensionData }, + }, cache: 'no-store', parse: false, } ) - .then( ( response ) => { + .then( ( response: unknown ) => { + assertResponseIsValid< CheckoutResponseSuccess >( response ); processCheckoutResponseHeaders( response.headers ); if ( ! response.ok ) { - throw new Error( response ); + throw response; } return response.json(); } ) - .then( ( responseJson ) => { + .then( ( responseJson: CheckoutResponseSuccess ) => { __internalProcessCheckoutResponse( responseJson ); setIsProcessingOrder( false ); } ) - .catch( ( errorResponse ) => { + .catch( ( errorResponse: ApiResponse< CheckoutResponseError > ) => { + processCheckoutResponseHeaders( errorResponse?.headers ); try { - if ( errorResponse?.headers ) { - processCheckoutResponseHeaders( errorResponse.headers ); - } // This attempts to parse a JSON error response where the status code was 4xx/5xx. - errorResponse.json().then( ( response ) => { - // If updated cart state was returned, update the store. - if ( response.data?.cart ) { - receiveCart( response.data.cart ); - } - createErrorNotice( - formatStoreApiErrorMessage( response ), - { - id: 'checkout', - context: 'wc/checkout', - __unstableHTML: true, + errorResponse + .json() + .then( + ( response ) => response as CheckoutResponseError + ) + .then( ( response: CheckoutResponseError ) => { + if ( response.data?.cart ) { + receiveCart( response.data.cart ); } - ); - response?.additional_errors?.forEach?.( - ( additionalError ) => { - createErrorNotice( additionalError.message, { - id: additionalError.error_code, - context: 'wc/checkout', - __unstableHTML: true, - } ); - } - ); - __internalProcessCheckoutResponse( response ); - } ); + processErrorResponse( response ); + __internalProcessCheckoutResponse( response ); + } ); } catch { - createErrorNotice( - sprintf( - // Translators: %s Error text. - __( - '%s Please try placing your order again.', - 'woo-gutenberg-products-block' - ), - errorResponse?.message ?? - __( - 'Something went wrong. Please contact us for assistance.', - 'woo-gutenberg-products-block' - ) + processErrorResponse( { + code: 'unknown_error', + message: __( + 'Something went wrong. Please try placing your order again.', + 'woo-gutenberg-products-block' ), - { - id: 'checkout', - context: 'wc/checkout', - __unstableHTML: true, - } - ); + data: null, + } ); } __internalSetHasError( true ); setIsProcessingOrder( false ); } ); }, [ isProcessingOrder, - removeNotice, cartNeedsPayment, paymentMethodId, paymentMethodData, @@ -313,7 +289,6 @@ const CheckoutProcessor = () => { shouldCreateAccount, extensionData, cartNeedsShipping, - createErrorNotice, receiveCart, __internalSetHasError, __internalProcessCheckoutResponse, diff --git a/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx b/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx index 085514f2855..10a38b2cea2 100644 --- a/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx +++ b/assets/js/base/context/providers/cart-checkout/payment-events/index.tsx @@ -19,7 +19,6 @@ import { * Internal dependencies */ import { useEventEmitters, reducer as emitReducer } from './event-emit'; -import { useCustomerData } from '../../../hooks/use-customer-data'; import { emitterCallback } from '../../../event-emit'; type PaymentEventsContextType = { @@ -73,7 +72,6 @@ export const PaymentEventsProvider = ( { }; } ); - const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' ); const { setValidationErrors } = useDispatch( VALIDATION_STORE_KEY ); const [ observers, observerDispatch ] = useReducer( emitReducer, {} ); const { onPaymentProcessing } = useEventEmitters( observerDispatch ); @@ -87,10 +85,8 @@ export const PaymentEventsProvider = ( { const { __internalSetPaymentProcessing, __internalSetPaymentPristine, - __internalSetPaymentMethodData, __internalEmitPaymentProcessingEvent, } = useDispatch( PAYMENT_STORE_KEY ); - const { setBillingAddress, setShippingAddress } = useCustomerData(); // flip payment to processing if checkout processing is complete, there are no errors, and payment status is started. useEffect( () => { @@ -139,11 +135,6 @@ export const PaymentEventsProvider = ( { }, [ isPaymentProcessing, setValidationErrors, - removeNotice, - createErrorNotice, - setBillingAddress, - __internalSetPaymentMethodData, - setShippingAddress, __internalEmitPaymentProcessingEvent, ] ); diff --git a/assets/js/base/context/providers/cart-checkout/utils.ts b/assets/js/base/context/providers/cart-checkout/utils.ts index 5151a987824..da9044f111a 100644 --- a/assets/js/base/context/providers/cart-checkout/utils.ts +++ b/assets/js/base/context/providers/cart-checkout/utils.ts @@ -31,7 +31,12 @@ export const preparePaymentData = ( /** * Process headers from an API response an dispatch updates. */ -export const processCheckoutResponseHeaders = ( headers: Headers ): void => { +export const processCheckoutResponseHeaders = ( + headers: Headers | undefined +): void => { + if ( ! headers ) { + return; + } const { __internalSetCustomerId } = dispatch( CHECKOUT_STORE_KEY ); if ( // eslint-disable-next-line @typescript-eslint/ban-ts-comment diff --git a/assets/js/base/utils/create-notice.ts b/assets/js/base/utils/create-notice.ts new file mode 100644 index 00000000000..feeeb5a6cd5 --- /dev/null +++ b/assets/js/base/utils/create-notice.ts @@ -0,0 +1,101 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import type { Options as NoticeOptions } from '@wordpress/notices'; +import { select, dispatch } from '@wordpress/data'; + +export const GLOBAL_CONTEXT = 'wc/global'; +export const DEFAULT_ERROR_MESSAGE = __( + 'Something went wrong. Please contact us to get assistance.', + 'woo-gutenberg-products-block' +); + +export const hasStoreNoticeContainer = ( container: string ): boolean => { + const containers = select( 'wc/store/notices' ).getContainers(); + return containers.includes( container ); +}; + +const findParentContainer = ( container: string ): string => { + let parentContainer = GLOBAL_CONTEXT; + if ( + container.includes( 'wc/checkout/' ) && + hasStoreNoticeContainer( 'wc/checkout' ) + ) { + parentContainer = 'wc/checkout'; + } else if ( + container.includes( 'wc/cart/' ) && + hasStoreNoticeContainer( 'wc/cart' ) + ) { + parentContainer = 'wc/cart'; + } + return parentContainer; +}; + +/** + * Wrapper for @wordpress/notices createNotice. + * + * This is used to create the correct type of notice based on the provided context, and to ensure the notice container + * exists first, otherwise it uses the default context instead. + */ +export const createNotice = ( + status: 'error' | 'warning' | 'info' | 'success', + message: string, + options: Partial< NoticeOptions > +) => { + let noticeContext = options?.context || GLOBAL_CONTEXT; + + const suppressNotices = + select( 'wc/store/payment' ).isExpressPaymentMethodActive(); + + if ( suppressNotices ) { + return; + } + + if ( ! hasStoreNoticeContainer( noticeContext ) ) { + // If the container ref was not registered, use the parent context instead. + noticeContext = findParentContainer( noticeContext ); + } + + const { createNotice: dispatchCreateNotice } = dispatch( 'core/notices' ); + + dispatchCreateNotice( status, message, { + isDismissible: true, + __unstableHTML: true, + ...options, + context: noticeContext, + } ); +}; + +/** + * Creates a notice only if the Store Notice Container is visible. + */ +export const createNoticeIfVisible = ( + status: 'error' | 'warning' | 'info' | 'success', + message: string, + options: Partial< NoticeOptions > +) => { + const noticeContext = options?.context || GLOBAL_CONTEXT; + + if ( hasStoreNoticeContainer( noticeContext ) ) { + createNotice( status, message, options ); + } +}; + +/** + * Remove notices from all contexts. + * + * @todo Remove this when supported in Gutenberg. + * @see https://github.com/WordPress/gutenberg/pull/44059 + */ +export const removeAllNotices = () => { + const containers = select( 'wc/store/notices' ).getContainers(); + const { removeNotice } = dispatch( 'core/notices' ); + const { getNotices } = select( 'core/notices' ); + + containers.forEach( ( container ) => { + getNotices( container ).forEach( ( notice ) => { + removeNotice( notice.id, container ); + } ); + } ); +}; diff --git a/assets/js/base/utils/errors.js b/assets/js/base/utils/errors.js index dd8c5020851..a2bbb56b926 100644 --- a/assets/js/base/utils/errors.js +++ b/assets/js/base/utils/errors.js @@ -1,9 +1,3 @@ -/** - * External dependencies - */ -import { __ } from '@wordpress/i18n'; -import { decodeEntities } from '@wordpress/html-entities'; - /** * Given a JS error or a fetch response error, parse and format it, so it can be displayed to the user. * @@ -34,27 +28,3 @@ export const formatError = async ( error ) => { type: error.type || 'general', }; }; - -/** - * Given an API response object, formats the error message into something more human-readable. - * - * @param {Object} response Response object. - * @return {string} Error message. - */ -export const formatStoreApiErrorMessage = ( response ) => { - if ( response.data && response.code === 'rest_invalid_param' ) { - const invalidParams = Object.values( response.data.params ); - if ( invalidParams[ 0 ] ) { - return invalidParams[ 0 ]; - } - } - - if ( ! response?.message ) { - return __( - 'Something went wrong. Please contact us to get assistance.', - 'woo-gutenberg-products-block' - ); - } - - return decodeEntities( response.message ); -}; diff --git a/assets/js/base/utils/index.js b/assets/js/base/utils/index.js index c81fa33c4b2..3439277d829 100644 --- a/assets/js/base/utils/index.js +++ b/assets/js/base/utils/index.js @@ -8,3 +8,4 @@ export * from './product-data'; export * from './derive-selected-shipping-rates'; export * from './get-icons-from-payment-methods'; export * from './parse-style'; +export * from './create-notice'; diff --git a/assets/js/blocks/cart/inner-blocks/filled-cart-block/frontend.tsx b/assets/js/blocks/cart/inner-blocks/filled-cart-block/frontend.tsx index 03ef72c9e34..e34d7b9d69f 100644 --- a/assets/js/blocks/cart/inner-blocks/filled-cart-block/frontend.tsx +++ b/assets/js/blocks/cart/inner-blocks/filled-cart-block/frontend.tsx @@ -24,7 +24,7 @@ const FrontendBlock = ( { const { hasDarkControls } = useCartBlockContext(); const { createErrorNotice } = useDispatch( 'core/notices' ); - // Ensures any cart errors listed in the API response get shown. + // @todo Cart errors need to be watched for and created as notices elsewhere. useEffect( () => { cartItemErrors.forEach( ( error ) => { createErrorNotice( decodeEntities( error.message ), { diff --git a/assets/js/blocks/checkout/block.tsx b/assets/js/blocks/checkout/block.tsx index f2a4de004e9..0a323fe7391 100644 --- a/assets/js/blocks/checkout/block.tsx +++ b/assets/js/blocks/checkout/block.tsx @@ -29,7 +29,6 @@ import CheckoutOrderError from './checkout-order-error'; import { LOGIN_TO_CHECKOUT_URL, isLoginRequired, reloadPage } from './utils'; import type { Attributes } from './types'; import { CheckoutBlockContext } from './context'; -import { hasNoticesOfType } from '../../utils/notices'; const LoginPrompt = () => { return ( diff --git a/assets/js/data/cart/actions.ts b/assets/js/data/cart/actions.ts index 8a1fdb037ce..c8928261dc9 100644 --- a/assets/js/data/cart/actions.ts +++ b/assets/js/data/cart/actions.ts @@ -7,6 +7,7 @@ import type { CartResponseItem, ExtensionCartUpdateArgs, BillingAddressShippingAddress, + ApiErrorResponse, } from '@woocommerce/types'; import { camelCase, mapKeys } from 'lodash'; import type { AddToCartEventDetail } from '@woocommerce/type-defs/events'; @@ -19,7 +20,6 @@ import { controls } from '@wordpress/data'; import { ACTION_TYPES as types } from './action-types'; import { STORE_KEY as CART_STORE_KEY } from './constants'; import { apiFetchWithHeaders } from '../shared-controls'; -import type { ResponseError } from '../types'; import { ReturnOrGeneratorYieldUnion } from '../mapped-types'; /** @@ -67,14 +67,9 @@ export const receiveCartContents = ( /** * Returns an action object used for receiving customer facing errors from the API. - * - * @param {ResponseError|null} [error=null] An error object containing the error - * message and response code. - * @param {boolean} [replace=true] Should existing errors be replaced, - * or should the error be appended. */ export const receiveError = ( - error: ResponseError | null = null, + error: ApiErrorResponse | null = null, replace = true ) => ( { diff --git a/assets/js/data/cart/default-state.ts b/assets/js/data/cart/default-state.ts index 066a1a2e889..f7f1d65fdd2 100644 --- a/assets/js/data/cart/default-state.ts +++ b/assets/js/data/cart/default-state.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { Cart, CartMeta } from '@woocommerce/types'; +import type { Cart, CartMeta, ApiErrorResponse } from '@woocommerce/types'; /** * Internal dependencies @@ -18,17 +18,16 @@ import { EMPTY_PAYMENT_REQUIREMENTS, EMPTY_EXTENSIONS, } from '../constants'; -import type { ResponseError } from '../types'; const EMPTY_PENDING_QUANTITY: [] = []; const EMPTY_PENDING_DELETE: [] = []; export interface CartState { - cartItemsPendingQuantity: Array< string >; - cartItemsPendingDelete: Array< string >; + cartItemsPendingQuantity: string[]; + cartItemsPendingDelete: string[]; cartData: Cart; metaData: CartMeta; - errors: Array< ResponseError >; + errors: ApiErrorResponse[]; } export const defaultCartState: CartState = { cartItemsPendingQuantity: EMPTY_PENDING_QUANTITY, diff --git a/assets/js/data/cart/push-changes.ts b/assets/js/data/cart/push-changes.ts index b55bc0bbdf6..d49680ea95c 100644 --- a/assets/js/data/cart/push-changes.ts +++ b/assets/js/data/cart/push-changes.ts @@ -4,9 +4,9 @@ import { debounce } from 'lodash'; import { select, dispatch } from '@wordpress/data'; import { - formatStoreApiErrorMessage, pluckAddress, pluckEmail, + removeAllNotices, } from '@woocommerce/base-utils'; import { CartResponseBillingAddress, @@ -19,6 +19,7 @@ import { BillingAddressShippingAddress } from '@woocommerce/type-defs/cart'; * Internal dependencies */ import { STORE_KEY } from './constants'; +import { processErrorResponse } from '../utils'; declare type CustomerData = { billingAddress: CartResponseBillingAddress; @@ -102,20 +103,10 @@ const updateCustomerData = debounce( (): void => { dispatch( STORE_KEY ) .updateCustomerData( customerDataToUpdate ) .then( () => { - dispatch( 'core/notices' ).removeNotice( - 'checkout', - 'wc/checkout' - ); + removeAllNotices(); } ) .catch( ( response ) => { - dispatch( 'core/notices' ).createNotice( - 'error', - formatStoreApiErrorMessage( response ), - { - id: 'checkout', - context: 'wc/checkout', - } - ); + processErrorResponse( response ); } ); } }, 1000 ); diff --git a/assets/js/data/cart/selectors.ts b/assets/js/data/cart/selectors.ts index 73b88c13d22..a466a9950ae 100644 --- a/assets/js/data/cart/selectors.ts +++ b/assets/js/data/cart/selectors.ts @@ -7,6 +7,7 @@ import type { CartMeta, CartItem, CartShippingRate, + ApiErrorResponse, } from '@woocommerce/types'; import { BillingAddress, ShippingAddress } from '@woocommerce/settings'; @@ -14,7 +15,6 @@ import { BillingAddress, ShippingAddress } from '@woocommerce/settings'; * Internal dependencies */ import { CartState, defaultCartState } from './default-state'; -import type { ResponseError } from '../types'; /** * Retrieves cart data from state. @@ -90,11 +90,8 @@ export const getCartMeta = ( state: CartState ): CartMeta => { /** * Retrieves cart errors from state. - * - * @param {CartState} state The current state. - * @return {Array} Array of errors. */ -export const getCartErrors = ( state: CartState ): Array< ResponseError > => { +export const getCartErrors = ( state: CartState ): ApiErrorResponse[] => { return state.errors; }; diff --git a/assets/js/data/checkout/types.ts b/assets/js/data/checkout/types.ts index 6057407e1da..448844f5757 100644 --- a/assets/js/data/checkout/types.ts +++ b/assets/js/data/checkout/types.ts @@ -13,7 +13,7 @@ import type { PaymentState } from '../payment/default-state'; import type { DispatchFromMap, SelectFromMap } from '../mapped-types'; import * as selectors from './selectors'; import * as actions from './actions'; -import { FieldValidationStatus } from '../types'; +import type { FieldValidationStatus } from '../types'; export type CheckoutAfterProcessingWithErrorEventData = { redirectUrl: CheckoutState[ 'redirectUrl' ]; diff --git a/assets/js/data/index.ts b/assets/js/data/index.ts index 8be09cf717f..5367e06bb06 100644 --- a/assets/js/data/index.ts +++ b/assets/js/data/index.ts @@ -16,3 +16,4 @@ export { QUERY_STATE_STORE_KEY } from './query-state'; export { STORE_NOTICES_STORE_KEY } from './store-notices'; export * from './constants'; export * from './types'; +export * from './utils'; diff --git a/assets/js/data/shared-controls.ts b/assets/js/data/shared-controls.ts index 6631f86b9d5..b2176112ca4 100644 --- a/assets/js/data/shared-controls.ts +++ b/assets/js/data/shared-controls.ts @@ -5,15 +5,11 @@ import { __ } from '@wordpress/i18n'; import triggerFetch, { APIFetchOptions } from '@wordpress/api-fetch'; import DataLoader from 'dataloader'; import { isWpVersion } from '@woocommerce/settings'; - -/** - * Internal dependencies - */ import { + ApiResponse, assertBatchResponseIsValid, assertResponseIsValid, - ApiResponse, -} from './types'; +} from '@woocommerce/types'; /** * Dispatched a control action for triggering an api fetch call with no parsing. diff --git a/assets/js/data/types.ts b/assets/js/data/types.ts index 491ff5ed4ed..e69de29bb2d 100644 --- a/assets/js/data/types.ts +++ b/assets/js/data/types.ts @@ -1,49 +0,0 @@ -export interface ResponseError { - code: string; - message: string; - data: { - status: number; - [ key: string ]: unknown; - }; -} - -export interface ApiResponse { - body: Record< string, unknown >; - headers: Headers; - status: number; -} - -export function assertBatchResponseIsValid( - response: unknown -): asserts response is { - responses: ApiResponse[]; - headers: Headers; -} { - if ( - typeof response === 'object' && - response !== null && - response.hasOwnProperty( 'responses' ) - ) { - return; - } - throw new Error( 'Response not valid' ); -} - -export function assertResponseIsValid( - response: unknown -): asserts response is ApiResponse { - if ( - typeof response === 'object' && - response !== null && - response.hasOwnProperty( 'body' ) && - response.hasOwnProperty( 'headers' ) - ) { - return; - } - throw new Error( 'Response not valid' ); -} - -export interface FieldValidationStatus { - message: string; - hidden: boolean; -} diff --git a/assets/js/data/utils/index.js b/assets/js/data/utils/index.js index c8d47bd6f98..19a8b886967 100644 --- a/assets/js/data/utils/index.js +++ b/assets/js/data/utils/index.js @@ -1,2 +1,3 @@ export { default as hasInState } from './has-in-state'; export { default as updateState } from './update-state'; +export { default as processErrorResponse } from './process-error-response'; diff --git a/assets/js/data/utils/process-error-response.ts b/assets/js/data/utils/process-error-response.ts new file mode 100644 index 00000000000..d7044d82163 --- /dev/null +++ b/assets/js/data/utils/process-error-response.ts @@ -0,0 +1,139 @@ +/** + * External dependencies + */ +import { + createNotice, + createNoticeIfVisible, + DEFAULT_ERROR_MESSAGE, +} from '@woocommerce/base-utils'; +import { decodeEntities } from '@wordpress/html-entities'; +import { isObject, objectHasProp, ApiErrorResponse } from '@woocommerce/types'; +import { noticeContexts } from '@woocommerce/base-context/event-emit/utils'; + +type ApiParamError = { + param: string; + id: string; + code: string; + message: string; +}; + +const isApiResponse = ( response: unknown ): response is ApiErrorResponse => { + return ( + isObject( response ) && + objectHasProp( response, 'code' ) && + objectHasProp( response, 'message' ) && + objectHasProp( response, 'data' ) + ); +}; + +/** + * Flattens error details which are returned from the API when multiple params are not valid. + * + * - Codes will be prefixed with the param. For example, `invalid_email` becomes `billing_address_invalid_email`. + * - Additional error messages will be flattened alongside the main error message. + * - Supports 1 level of nesting. + * - Decodes HTML entities in error messages. + */ +const getErrorDetails = ( response: ApiErrorResponse ): ApiParamError[] => { + const errorDetails = objectHasProp( response.data, 'details' ) + ? Object.entries( response.data.details ) + : null; + + if ( ! errorDetails ) { + return []; + } + + return errorDetails.reduce( + ( + acc, + [ + param, + { code, message, additional_errors: additionalErrors = [] }, + ] + ) => { + return [ + ...acc, + { + param, + id: `${ param }_${ code }`, + code, + message: decodeEntities( message ), + }, + ...( Array.isArray( additionalErrors ) + ? additionalErrors.flatMap( ( additionalError ) => { + if ( + ! objectHasProp( additionalError, 'code' ) || + ! objectHasProp( additionalError, 'message' ) + ) { + return []; + } + return [ + { + param, + id: `${ param }_${ additionalError.code }`, + code: additionalError.code, + message: decodeEntities( + additionalError.message + ), + }, + ]; + } ) + : [] ), + ]; + }, + [] as ApiParamError[] + ); +}; + +/** + * Processes the response for an invalid param error, with response code rest_invalid_param. + */ +const processInvalidParamResponse = ( response: ApiErrorResponse ) => { + const errorDetails = getErrorDetails( response ); + + errorDetails.forEach( ( { code, message, id, param } ) => { + switch ( code ) { + case 'invalid_email': + createNotice( 'error', message, { + id, + context: noticeContexts.CONTACT_INFORMATION, + } ); + return; + } + switch ( param ) { + case 'billing_address': + createNoticeIfVisible( 'error', message, { + id, + context: noticeContexts.BILLING_ADDRESS, + } ); + break; + case 'shipping_address': + createNoticeIfVisible( 'error', message, { + id, + context: noticeContexts.SHIPPING_ADDRESS, + } ); + break; + } + } ); +}; + +/** + * Takes an API response object and creates error notices to display to the customer. + */ +const processErrorResponse = ( response: ApiErrorResponse ) => { + if ( ! isApiResponse( response ) ) { + return; + } + switch ( response.code ) { + case 'rest_invalid_param': + processInvalidParamResponse( response ); + break; + default: + createNotice( 'error', response.message || DEFAULT_ERROR_MESSAGE, { + id: response.code, + context: noticeContexts.CHECKOUT, + } ); + } +}; + +export default processErrorResponse; diff --git a/assets/js/types/type-defs/api-error-response.ts b/assets/js/types/type-defs/api-error-response.ts new file mode 100644 index 00000000000..e7b64dc5afa --- /dev/null +++ b/assets/js/types/type-defs/api-error-response.ts @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import type { CartResponse } from './cart-response'; + +// This is the standard API response data when an error is returned. +export type ApiErrorResponse = { + code: string; + message: string; + data: ApiErrorResponseData; +}; + +// API errors contain data with the status, and more in-depth error details. This may be null. +export type ApiErrorResponseData = { + status: number; + params: Record< string, string >; + details: Record< string, ApiErrorResponseDataDetails >; + // Some endpoints return cart data to update the client. + cart?: CartResponse | undefined; +} | null; + +// The details object lists individual errors for each field. +export type ApiErrorResponseDataDetails = { + code: string; + message: string; + data: ApiErrorResponseData; + additional_errors: ApiErrorResponse[]; +}; diff --git a/assets/js/types/type-defs/api-response.ts b/assets/js/types/type-defs/api-response.ts new file mode 100644 index 00000000000..7a8e5d9e543 --- /dev/null +++ b/assets/js/types/type-defs/api-response.ts @@ -0,0 +1,37 @@ +export interface ApiResponse< T > { + body: Record< string, unknown >; + headers: Headers; + status: number; + ok: boolean; + json: () => Promise< T >; +} + +export function assertBatchResponseIsValid( + response: unknown +): asserts response is { + responses: ApiResponse< unknown >[]; + headers: Headers; +} { + if ( + typeof response === 'object' && + response !== null && + response.hasOwnProperty( 'responses' ) + ) { + return; + } + throw new Error( 'Response not valid' ); +} + +export function assertResponseIsValid< T >( + response: unknown +): asserts response is ApiResponse< T > { + if ( + typeof response === 'object' && + response !== null && + response.hasOwnProperty( 'body' ) && + response.hasOwnProperty( 'headers' ) + ) { + return; + } + throw new Error( 'Response not valid' ); +} diff --git a/assets/js/types/type-defs/checkout.ts b/assets/js/types/type-defs/checkout.ts index 45c038dba8d..41a49eef0c3 100644 --- a/assets/js/types/type-defs/checkout.ts +++ b/assets/js/types/type-defs/checkout.ts @@ -3,6 +3,11 @@ */ import { ShippingAddress, BillingAddress } from '@woocommerce/settings'; +/** + * Internal dependencies + */ +import type { ApiErrorResponse } from './api-error-response'; + export interface CheckoutResponseSuccess { billing_address: BillingAddress; customer_id: number; @@ -20,12 +25,6 @@ export interface CheckoutResponseSuccess { status: string; } -export interface CheckoutResponseError { - code: string; - message: string; - data: { - status: number; - }; -} +export type CheckoutResponseError = ApiErrorResponse; export type CheckoutResponse = CheckoutResponseSuccess | CheckoutResponseError; diff --git a/assets/js/types/type-defs/hooks.ts b/assets/js/types/type-defs/hooks.ts index 09cead7e6c8..9d1b51c3f7c 100644 --- a/assets/js/types/type-defs/hooks.ts +++ b/assets/js/types/type-defs/hooks.ts @@ -18,17 +18,17 @@ import type { CartResponse, CartResponseCoupons, } from './cart-response'; -import type { ResponseError } from '../../data/types'; +import type { ApiErrorResponse } from './api-error-response'; export interface StoreCartItemQuantity { isPendingDelete: boolean; quantity: number; setItemQuantity: React.Dispatch< React.SetStateAction< number > >; removeItem: () => Promise< boolean >; - cartItemQuantityErrors: Array< CartResponseErrorItem >; + cartItemQuantityErrors: CartResponseErrorItem[]; } export interface StoreCartCoupon { - appliedCoupons: Array< CartResponseCouponItem >; + appliedCoupons: CartResponseCouponItem[]; isLoading: boolean; applyCoupon: ( coupon: string ) => void; removeCoupon: ( coupon: string ) => void; @@ -38,24 +38,24 @@ export interface StoreCartCoupon { export interface StoreCart { cartCoupons: CartResponseCoupons; - cartItems: Array< CartResponseItem >; - crossSellsProducts: Array< ProductResponseItem >; - cartFees: Array< CartResponseFeeItem >; + cartItems: CartResponseItem[]; + crossSellsProducts: ProductResponseItem[]; + cartFees: CartResponseFeeItem[]; cartItemsCount: number; cartItemsWeight: number; cartNeedsPayment: boolean; cartNeedsShipping: boolean; - cartItemErrors: Array< CartResponseErrorItem >; + cartItemErrors: CartResponseErrorItem[]; cartTotals: CartResponseTotals; cartIsLoading: boolean; - cartErrors: Array< ResponseError >; + cartErrors: ApiErrorResponse[]; billingAddress: CartResponseBillingAddress; shippingAddress: CartResponseShippingAddress; - shippingRates: Array< CartResponseShippingRate >; + shippingRates: CartResponseShippingRate[]; extensions: Record< string, unknown >; isLoadingRates: boolean; cartHasCalculatedShipping: boolean; - paymentRequirements: Array< string >; + paymentRequirements: string[]; receiveCart: ( cart: CartResponse ) => void; } diff --git a/assets/js/types/type-defs/index.ts b/assets/js/types/type-defs/index.ts index 4b2fe4bc849..3f8d79018bf 100644 --- a/assets/js/types/type-defs/index.ts +++ b/assets/js/types/type-defs/index.ts @@ -1,3 +1,5 @@ +export * from './api-response'; +export * from './api-error-response'; export * from './blocks'; export * from './cart'; export * from './cart-response'; diff --git a/packages/checkout/components/store-notices-container/store-notices.tsx b/packages/checkout/components/store-notices-container/store-notices.tsx index 07e99d70b5c..b8dddd70c09 100644 --- a/packages/checkout/components/store-notices-container/store-notices.tsx +++ b/packages/checkout/components/store-notices-container/store-notices.tsx @@ -35,13 +35,30 @@ const StoreNotices = ( { useEffect( () => { // Scroll to container when an error is added here. + const containerRef = ref.current; + + if ( ! containerRef ) { + return; + } + + // Do not scroll if input has focus. + const activeElement = containerRef.ownerDocument.activeElement; + const inputs = [ 'input', 'select', 'button', 'textarea' ]; + + if ( + activeElement && + inputs.indexOf( activeElement.tagName.toLowerCase() ) !== -1 + ) { + return; + } + const newNoticeIds = noticeIds.filter( ( value ) => ! previousNoticeIds || ! previousNoticeIds.includes( value ) ); if ( newNoticeIds.length ) { - ref.current?.scrollIntoView( { + containerRef.scrollIntoView( { behavior: 'smooth', } ); } diff --git a/packages/checkout/filter-registry/index.ts b/packages/checkout/filter-registry/index.ts index 058b7d70cc8..91efcbb66a8 100644 --- a/packages/checkout/filter-registry/index.ts +++ b/packages/checkout/filter-registry/index.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import { useMemo } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import deprecated from '@wordpress/deprecated'; diff --git a/packages/checkout/utils/create-notice.ts b/packages/checkout/utils/create-notice.ts deleted file mode 100644 index 4237d6bded0..00000000000 --- a/packages/checkout/utils/create-notice.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * External dependencies - */ -import type { Options as NoticeOptions } from '@wordpress/notices'; -import { - STORE_NOTICES_STORE_KEY, - PAYMENT_STORE_KEY, -} from '@woocommerce/block-data'; -import { select, dispatch } from '@wordpress/data'; - -const DEFAULT_CONTEXT = 'wc/global'; - -const hasContainer = ( container: string ): boolean => { - const containers = select( STORE_NOTICES_STORE_KEY ).getContainers(); - return containers.includes( container ); -}; - -const findParentContainer = ( container: string ): string => { - let parentContainer = DEFAULT_CONTEXT; - if ( - container.includes( 'wc/checkout/' ) && - hasContainer( 'wc/checkout' ) - ) { - parentContainer = 'wc/checkout'; - } else if ( - container.includes( 'wc/cart/' ) && - hasContainer( 'wc/cart' ) - ) { - parentContainer = 'wc/cart'; - } - return parentContainer; -}; - -/** - * Wrapper for @wordpress/notices createNotice. - * - * This is used to create the correct type of notice based on the provided context, and to ensure the notice container - * exists first, otherwise it uses the default context instead. - */ -export const createNotice = ( - status: 'error' | 'warning' | 'info' | 'success', - message: string, - options: Partial< NoticeOptions > -) => { - let noticeContext = options?.context || DEFAULT_CONTEXT; - - const suppressNotices = - select( PAYMENT_STORE_KEY ).isExpressPaymentMethodActive(); - - if ( suppressNotices ) { - return; - } - - if ( ! hasContainer( noticeContext ) ) { - // If the container ref was not registered, use the parent context instead. - noticeContext = findParentContainer( noticeContext ); - } - - const { createNotice: dispatchCreateNotice } = dispatch( 'core/notices' ); - - dispatchCreateNotice( status, message, { - isDismissible: true, - ...options, - context: noticeContext, - } ); -}; diff --git a/packages/checkout/utils/index.js b/packages/checkout/utils/index.js index 24c2979de55..c48b5ad34b3 100644 --- a/packages/checkout/utils/index.js +++ b/packages/checkout/utils/index.js @@ -1,3 +1,2 @@ export * from './validation'; -export * from './create-notice'; export { extensionCartUpdate } from './extension-cart-update'; From f1151f2a415267a4a1c7b6b077c13e5c25ccf88e Mon Sep 17 00:00:00 2001 From: Thomas Roberts Date: Mon, 28 Nov 2022 19:19:01 +0000 Subject: [PATCH 27/34] Prevent pressing the checkout button when any notices are visible --- .../cart-checkout/place-order-button/index.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/assets/js/base/components/cart-checkout/place-order-button/index.tsx b/assets/js/base/components/cart-checkout/place-order-button/index.tsx index d4f3cc7fecd..68c4da0a19e 100644 --- a/assets/js/base/components/cart-checkout/place-order-button/index.tsx +++ b/assets/js/base/components/cart-checkout/place-order-button/index.tsx @@ -4,6 +4,8 @@ import { useCheckoutSubmit } from '@woocommerce/base-context/hooks'; import { Icon, check } from '@wordpress/icons'; import Button from '@woocommerce/base-components/button'; +import { STORE_NOTICES_STORE_KEY } from '@woocommerce/block-data'; +import { useSelect } from '@wordpress/data'; const PlaceOrderButton = (): JSX.Element => { const { @@ -15,6 +17,21 @@ const PlaceOrderButton = (): JSX.Element => { waitingForRedirect, } = useCheckoutSubmit(); + const hasNoticesVisible = useSelect( ( select ) => { + const noticeContainers = select( + STORE_NOTICES_STORE_KEY + ).getContainers(); + const noticeStore = select( 'core/notices' ); + return noticeContainers.some( ( container ) => { + return ( + noticeStore + .getNotices( container ) + .filter( ( notice ) => notice.status === 'error' ).length > + 0 + ); + } ); + } ); + return (