Remove `useStoreNotices` and interact directly with data store instead (https://github.com/woocommerce/woocommerce-blocks/pull/6159)

* Make useStoreNotices interact directly with the store

* Get/set error notices directly in store in paymentMethodDataContext

* Add hasNoticesOfType util

* Remove useStoreNotices and interact directly with data store

* Create/remove notices directly in store

* Remove tests for useStoreNotices

* Add tests for notices util

* Use setIsSuppressed from useStoreNoticesContext

* remove useStoreNotices hook

* Update context typedef to define only isSuppressed and setIsSuppressed

* Remove all values from StoreNoticesContext besides setIsSuppressed

* Wrap Cart and Checkout blocks in StoreNoticesProvider (for isSuppressed)

* Make StoreNoticesContainer a named export

This is required so we can import it from @wooommerce/base-context

* Change addErrorNotice to createErrorNotice to match store action

* Remove unnecessary StoreNoticeProviders and pass only context to container

* Accept a context in StoreNoticesContainer

* Pass relevant context to StoreNoticesContainer

* Add function to remove notices by status

* Prevent checkout from breaking when removing notices during processing

* Prevent TS error about not included path

* Add StoreNoticesContainer to single product block

* Add StoreNoticesContainer to All Products Block

* Ensure errors are shown when using All Products & Single Product Blocks

* Add a context arg to removeNoticesByStatus

* Use correct contexts for all products and single product block

* Update tests to reflect new context argument

* Re-add missing block file for order-summary

* Remove block file for order-summary

* Send context to useStoreCartCoupons to show errors correctly
This commit is contained in:
Thomas Roberts 2022-04-08 13:11:50 +01:00 committed by GitHub
parent d5503b6e46
commit cab947bc2b
34 changed files with 386 additions and 432 deletions

View File

@ -76,7 +76,9 @@ const AddToCartButton = () => {
isProcessing={ isProcessing }
isDone={ addedToCart }
onClick={ () => {
dispatchActions.submitForm();
dispatchActions.submitForm(
`woocommerce/single-product/${ product?.id || 0 }`
);
dispatchStoreEvent( 'cart-add-item', {
product,
listName: parentName,

View File

@ -104,7 +104,10 @@ const AddToCartButton = ( {
is_in_stock: isInStock,
} = product;
const { dispatchStoreEvent } = useStoreEvents();
const { cartQuantity, addingToCart, addToCart } = useStoreAddToCart( id );
const { cartQuantity, addingToCart, addToCart } = useStoreAddToCart(
id,
`woocommerce/single-product/${ id || 0 }`
);
const addedToCart = Number.isFinite( cartQuantity ) && cartQuantity > 0;
const allowAddToCart = ! hasOptions && isPurchasable && isInStock;

View File

@ -4,7 +4,7 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { useSelect } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
import { decodeEntities } from '@wordpress/html-entities';
import type { StoreCartCoupon } from '@woocommerce/types';
@ -15,7 +15,6 @@ import type { StoreCartCoupon } from '@woocommerce/types';
import { useStoreCart } from './use-store-cart';
import { useStoreSnackbarNotices } from '../use-store-snackbar-notices';
import { useValidationContext } from '../../providers/validation';
import { useStoreNotices } from '../use-store-notices';
/**
* This is a custom hook for loading the Store API /cart/coupons endpoint and an
@ -25,9 +24,9 @@ import { useStoreNotices } from '../use-store-notices';
* @return {StoreCartCoupon} An object exposing data and actions from/for the
* store api /cart/coupons endpoint.
*/
export const useStoreCartCoupons = (): StoreCartCoupon => {
export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
const { cartCoupons, cartIsLoading } = useStoreCart();
const { addErrorNotice } = useStoreNotices();
const { createErrorNotice } = useDispatch( 'core/notices' );
const { addSnackbarNotice } = useStoreSnackbarNotices();
const { setValidationErrors } = useValidationContext();
@ -100,8 +99,9 @@ export const useStoreCartCoupons = (): StoreCartCoupon => {
}
} )
.catch( ( error ) => {
addErrorNotice( error.message, {
createErrorNotice( error.message, {
id: 'coupon-form',
context,
} );
// Finished handling the coupon.
receiveApplyingCoupon( '' );
@ -115,7 +115,7 @@ export const useStoreCartCoupons = (): StoreCartCoupon => {
isRemovingCoupon,
};
},
[ addErrorNotice, addSnackbarNotice ]
[ createErrorNotice, addSnackbarNotice ]
);
return {

View File

@ -2,7 +2,6 @@ export * from './cart';
export * from './collections';
export * from './shipping';
export * from './payment-methods';
export * from './use-store-notices';
export * from './use-store-events';
export * from './use-query-state';
export * from './use-store-products';

View File

@ -1,65 +0,0 @@
/**
* External dependencies
*/
import { render, act } from '@testing-library/react';
import { StoreNoticesProvider } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import { useStoreNotices } from '../use-store-notices';
describe( 'useStoreNotices', () => {
function setup() {
const returnVal = {};
function TestComponent() {
Object.assign( returnVal, useStoreNotices() );
return null;
}
render(
<StoreNoticesProvider>
<TestComponent />
</StoreNoticesProvider>
);
return returnVal;
}
test( 'allows adding and removing notices and checking if there are notices of a specific type', () => {
const storeNoticesData = setup();
// Assert initial state.
expect( storeNoticesData.notices ).toEqual( [] );
expect( storeNoticesData.hasNoticesOfType( 'default' ) ).toBe( false );
// Add error notice.
act( () => {
storeNoticesData.addErrorNotice( 'Error notice' );
} );
expect( storeNoticesData.notices.length ).toBe( 1 );
expect( storeNoticesData.hasNoticesOfType( 'default' ) ).toBe( true );
expect( storeNoticesData.notices.length ).toBe( 1 );
expect( storeNoticesData.hasNoticesOfType( 'default' ) ).toBe( true );
// Remove error notice.
act( () => {
storeNoticesData.removeNotices( 'error' );
} );
expect( storeNoticesData.notices.length ).toBe( 0 );
expect( storeNoticesData.hasNoticesOfType( 'default' ) ).toBe( false );
// Remove all remaining notices.
act( () => {
storeNoticesData.removeNotices();
} );
expect( storeNoticesData.notices.length ).toBe( 0 );
expect( storeNoticesData.hasNoticesOfType( 'default' ) ).toBe( false );
} );
} );

View File

@ -11,7 +11,6 @@ import type { CartItem } from '@woocommerce/types';
* Internal dependencies
*/
import { useStoreCart } from './cart/use-store-cart';
import { useStoreNotices } from './use-store-notices';
/**
* @typedef {import('@woocommerce/type-defs/hooks').StoreCartItemAddToCart} StoreCartItemAddToCart
@ -52,7 +51,7 @@ const getQuantityFromCartItems = (
export const useStoreAddToCart = ( productId: number ): StoreAddToCart => {
const { addItemToCart } = useDispatch( storeKey );
const { cartItems, cartIsLoading } = useStoreCart();
const { addErrorNotice, removeNotice } = useStoreNotices();
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
const [ addingToCart, setAddingToCart ] = useState( false );
const currentCartItemQuantity = useRef(
@ -66,9 +65,9 @@ export const useStoreAddToCart = ( productId: number ): StoreAddToCart => {
removeNotice( 'add-to-cart' );
} )
.catch( ( error ) => {
addErrorNotice( decodeEntities( error.message ), {
context: 'wc/all-products',
createErrorNotice( decodeEntities( error.message ), {
id: 'add-to-cart',
context: 'wc/all-products',
isDismissible: true,
} );
} )

View File

@ -1,116 +0,0 @@
/**
* External dependencies
*/
import { useMemo, useRef, useEffect } from '@wordpress/element';
/**
* Internal dependencies
*/
import { useStoreNoticesContext } from '../providers/store-notices/context';
type WPNoticeAction = {
label: string;
url?: string;
onClick?: () => void;
};
type WPNotice = {
id: string;
status: 'success' | 'info' | 'error' | 'warning';
content: string;
spokenMessage: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
__unstableHTML: string;
isDismissible: boolean;
type: 'default' | 'snackbar';
speak: boolean;
actions: WPNoticeAction[];
};
type NoticeOptions = {
id: string;
type?: string;
isDismissible?: boolean;
};
type NoticeCreator = ( text: string, noticeProps: NoticeOptions ) => void;
export const useStoreNotices = (): {
notices: WPNotice[];
hasNoticesOfType: ( type: string ) => boolean;
removeNotices: ( status: string | null ) => void;
removeNotice: ( id: string, context?: string ) => void;
addDefaultNotice: NoticeCreator;
addErrorNotice: NoticeCreator;
addWarningNotice: NoticeCreator;
addInfoNotice: NoticeCreator;
addSuccessNotice: NoticeCreator;
setIsSuppressed: ( isSuppressed: boolean ) => void;
} => {
const {
notices,
createNotice,
removeNotice,
setIsSuppressed,
} = useStoreNoticesContext();
// Added to a ref so the surface for notices doesn't change frequently
// and thus can be used as dependencies on effects.
const currentNotices = useRef( notices );
// Update notices ref whenever they change
useEffect( () => {
currentNotices.current = notices;
}, [ notices ] );
const noticesApi = useMemo(
() => ( {
hasNoticesOfType: ( type: 'default' | 'snackbar' ): boolean => {
return currentNotices.current.some(
( notice ) => notice.type === type
);
},
removeNotices: ( status = null ) => {
currentNotices.current.forEach( ( notice ) => {
if ( status === null || notice.status === status ) {
removeNotice( notice.id );
}
} );
},
removeNotice,
} ),
[ removeNotice ]
);
const noticeCreators = useMemo(
() => ( {
addDefaultNotice: ( text: string, noticeProps = {} ) =>
void createNotice( 'default', text, {
...noticeProps,
} ),
addErrorNotice: ( text: string, noticeProps = {} ) =>
void createNotice( 'error', text, {
...noticeProps,
} ),
addWarningNotice: ( text: string, noticeProps = {} ) =>
void createNotice( 'warning', text, {
...noticeProps,
} ),
addInfoNotice: ( text: string, noticeProps = {} ) =>
void createNotice( 'info', text, {
...noticeProps,
} ),
addSuccessNotice: ( text: string, noticeProps = {} ) =>
void createNotice( 'success', text, {
...noticeProps,
} ),
} ),
[ createNotice ]
);
return {
notices,
...noticesApi,
...noticeCreators,
setIsSuppressed,
};
};

View File

@ -14,7 +14,7 @@ import {
productIsPurchasable,
productSupportsAddToCartForm,
} from '@woocommerce/base-utils';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
@ -29,8 +29,8 @@ import {
reducer as emitReducer,
} from './event-emit';
import { useValidationContext } from '../../validation';
import { useStoreNotices } from '../../../hooks/use-store-notices';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import { removeNoticesByStatus } from '../../../../../utils/notices';
/**
* @typedef {import('@woocommerce/type-defs/add-to-cart-form').AddToCartFormDispatchActions} AddToCartFormDispatchActions
@ -99,7 +99,7 @@ export const AddToCartFormStateContextProvider = ( {
);
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
const currentObservers = useShallowEqual( observers );
const { addErrorNotice, removeNotices } = useStoreNotices();
const { createErrorNotice } = useDispatch( 'core/notices' );
const { setValidationErrors } = useValidationContext();
const {
isSuccessResponse,
@ -167,7 +167,7 @@ export const AddToCartFormStateContextProvider = ( {
const status = addToCartFormState.status;
if ( status === STATUS.BEFORE_PROCESSING ) {
removeNotices( 'error' );
removeNoticesByStatus( 'error', 'wc/add-to-cart' );
emitEvent(
currentObservers,
EMIT_TYPES.ADD_TO_CART_BEFORE_PROCESSING,
@ -178,7 +178,10 @@ export const AddToCartFormStateContextProvider = ( {
response.forEach(
( { errorMessage, validationErrors } ) => {
if ( errorMessage ) {
addErrorNotice( errorMessage );
createErrorNotice(
errorMessage,
'wc/add-to-cart'
);
}
if ( validationErrors ) {
setValidationErrors( validationErrors );
@ -195,10 +198,10 @@ export const AddToCartFormStateContextProvider = ( {
}, [
addToCartFormState.status,
setValidationErrors,
addErrorNotice,
removeNotices,
createErrorNotice,
dispatch,
currentObservers,
product?.id,
] );
/**
@ -227,7 +230,7 @@ export const AddToCartFormStateContextProvider = ( {
? { context: messageContext }
: undefined;
handled = true;
addErrorNotice( message, errorOptions );
createErrorNotice( message, errorOptions );
}
} );
return handled;
@ -248,8 +251,11 @@ export const AddToCartFormStateContextProvider = ( {
'Something went wrong. Please contact us to get assistance.',
'woo-gutenberg-products-block'
);
addErrorNotice( message, {
createErrorNotice( message, {
id: 'add-to-cart',
context: `woocommerce/single-product/${
product?.id || 0
}`,
} );
}
dispatch( actions.setIdle() );
@ -277,11 +283,12 @@ export const AddToCartFormStateContextProvider = ( {
addToCartFormState.hasError,
addToCartFormState.processingResponse,
dispatchActions,
addErrorNotice,
createErrorNotice,
isErrorResponse,
isFailResponse,
isSuccessResponse,
currentObservers,
product?.id,
] );
const supportsFormElements = productSupportsAddToCartForm( product );

View File

@ -6,6 +6,7 @@ import triggerFetch from '@wordpress/api-fetch';
import { useEffect, useCallback, useState } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { triggerAddedToCartEvent } from '@woocommerce/base-utils';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -13,7 +14,6 @@ import { triggerAddedToCartEvent } from '@woocommerce/base-utils';
import { useAddToCartFormContext } from '../../form-state';
import { useValidationContext } from '../../../validation';
import { useStoreCart } from '../../../../hooks/cart/use-store-cart';
import { useStoreNotices } from '../../../../hooks/use-store-notices';
/**
* FormSubmit.
@ -34,7 +34,7 @@ const FormSubmit = () => {
hasValidationErrors,
showAllValidationErrors,
} = useValidationContext();
const { addErrorNotice, removeNotice } = useStoreNotices();
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
const { receiveCart } = useStoreCart();
const [ isSubmitting, setIsSubmitting ] = useState( false );
const doSubmit = ! hasError && isProcessing;
@ -63,7 +63,10 @@ const FormSubmit = () => {
// Triggers form submission to the API.
const submitFormCallback = useCallback( () => {
setIsSubmitting( true );
removeNotice( 'add-to-cart' );
removeNotice(
'add-to-cart',
`woocommerce/single-product/${ product?.id || 0 }`
);
const fetchData = {
id: product.id || 0,
@ -87,20 +90,23 @@ const FormSubmit = () => {
if ( ! fetchResponse.ok ) {
// We received an error response.
if ( response.body && response.body.message ) {
addErrorNotice(
createErrorNotice(
decodeEntities( response.body.message ),
{
id: 'add-to-cart',
context: `woocommerce/single-product/${
product?.id || 0
}`,
}
);
} else {
addErrorNotice(
createErrorNotice(
__(
'Something went wrong. Please contact us to get assistance.',
'woo-gutenberg-products-block'
),
{
id: 'add-to-cart',
id: 'dadd-to-cart',
}
);
}
@ -126,7 +132,7 @@ const FormSubmit = () => {
} );
}, [
product,
addErrorNotice,
createErrorNotice,
removeNotice,
receiveCart,
dispatchActions,

View File

@ -14,6 +14,7 @@ import {
emptyHiddenAddressFields,
formatStoreApiErrorMessage,
} from '@woocommerce/base-utils';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -25,8 +26,7 @@ import { useCustomerDataContext } from './customer';
import { usePaymentMethodDataContext } from './payment-methods';
import { useValidationContext } from '../validation';
import { useStoreCart } from '../../hooks/cart/use-store-cart';
import { useStoreNotices } from '../../hooks/use-store-notices';
import { useStoreNoticesContext } from '../store-notices';
/**
* CheckoutProcessor component.
*
@ -58,7 +58,8 @@ const CheckoutProcessor = () => {
paymentMethods,
shouldSavePayment,
} = usePaymentMethodDataContext();
const { addErrorNotice, removeNotice, setIsSuppressed } = useStoreNotices();
const { setIsSuppressed } = useStoreNoticesContext();
const { createErrorNotice, removeNotice } = useDispatch( 'core/notices' );
const currentBillingData = useRef( billingData );
const currentShippingAddress = useRef( shippingAddress );
const currentRedirectUrl = useRef( redirectUrl );
@ -232,13 +233,13 @@ const CheckoutProcessor = () => {
if ( response.data?.cart ) {
receiveCart( response.data.cart );
}
addErrorNotice(
createErrorNotice(
formatStoreApiErrorMessage( response ),
{ id: 'checkout' }
);
response?.additional_errors?.forEach?.(
( additionalError ) => {
addErrorNotice( additionalError.message, {
createErrorNotice( additionalError.message, {
id: additionalError.error_code,
} );
}
@ -246,7 +247,7 @@ const CheckoutProcessor = () => {
dispatchActions.setAfterProcessing( response );
} );
} catch {
addErrorNotice(
createErrorNotice(
sprintf(
// Translators: %s Error text.
__(
@ -278,7 +279,7 @@ const CheckoutProcessor = () => {
extensionData,
cartNeedsShipping,
dispatchActions,
addErrorNotice,
createErrorNotice,
receiveCart,
] );

View File

@ -13,7 +13,8 @@ import {
import { __ } from '@wordpress/i18n';
import { usePrevious } from '@woocommerce/base-hooks';
import deprecated from '@wordpress/deprecated';
import { isObject } from '@woocommerce/types';
import { isObject, isString } from '@woocommerce/types';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -38,10 +39,10 @@ import {
reducer as emitReducer,
} from './event-emit';
import { useValidationContext } from '../../validation';
import { useStoreNotices } from '../../../hooks/use-store-notices';
import { useStoreEvents } from '../../../hooks/use-store-events';
import { useCheckoutNotices } from '../../../hooks/use-checkout-notices';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import { removeNoticesByStatus } from '../../../../../utils/notices';
/**
* @typedef {import('@woocommerce/type-defs/contexts').CheckoutDataContext} CheckoutDataContext
@ -76,7 +77,8 @@ export const CheckoutStateProvider = ( {
DEFAULT_STATE.redirectUrl = redirectUrl;
const [ checkoutState, dispatch ] = useReducer( reducer, DEFAULT_STATE );
const { setValidationErrors } = useValidationContext();
const { addErrorNotice, removeNotices } = useStoreNotices();
const { createErrorNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
const isCalculating = checkoutState.calculatingCount > 0;
const {
@ -162,7 +164,7 @@ export const CheckoutStateProvider = ( {
useEffect( () => {
const status = checkoutState.status;
if ( status === STATUS.BEFORE_PROCESSING ) {
removeNotices( 'error' );
removeNoticesByStatus( 'error' );
emitEvent(
currentObservers.current,
EMIT_TYPES.CHECKOUT_VALIDATION_BEFORE_PROCESSING,
@ -172,7 +174,7 @@ export const CheckoutStateProvider = ( {
if ( Array.isArray( response ) ) {
response.forEach(
( { errorMessage, validationErrors } ) => {
addErrorNotice( errorMessage );
createErrorNotice( errorMessage );
setValidationErrors( validationErrors );
}
);
@ -187,8 +189,7 @@ export const CheckoutStateProvider = ( {
}, [
checkoutState.status,
setValidationErrors,
addErrorNotice,
removeNotices,
createErrorNotice,
dispatch,
] );
@ -210,12 +211,15 @@ export const CheckoutStateProvider = ( {
isErrorResponse( response ) ||
isFailResponse( response )
) {
if ( response.message ) {
const errorOptions = response.messageContext
? { context: response.messageContext }
: undefined;
if ( response.message && isString( response.message ) ) {
const errorOptions =
response.messageContext &&
isString( response.messageContent )
? // The `as string` is OK here because of the type guard above.
{ context: response.messageContext as string }
: undefined;
errorResponse = response;
addErrorNotice( response.message, errorOptions );
createErrorNotice( response.message, errorOptions );
}
}
} );
@ -271,7 +275,7 @@ export const CheckoutStateProvider = ( {
'Something went wrong. Please contact us to get assistance.',
'woo-gutenberg-products-block'
);
addErrorNotice( message, {
createErrorNotice( message, {
id: 'checkout',
} );
}
@ -311,11 +315,16 @@ export const CheckoutStateProvider = ( {
if ( successResponse && ! errorResponse ) {
dispatch( actions.setComplete( successResponse ) );
} else if ( isObject( errorResponse ) ) {
if ( errorResponse.message ) {
const errorOptions = errorResponse.messageContext
? { context: errorResponse.messageContext }
: undefined;
addErrorNotice(
if (
errorResponse.message &&
isString( errorResponse.message )
) {
const errorOptions =
errorResponse.messageContext &&
isString( errorResponse.messageContext )
? { context: errorResponse.messageContext }
: undefined;
createErrorNotice(
errorResponse.message,
errorOptions
);
@ -346,7 +355,7 @@ export const CheckoutStateProvider = ( {
previousStatus,
previousHasError,
dispatchActions,
addErrorNotice,
createErrorNotice,
isErrorResponse,
isFailResponse,
isSuccessResponse,

View File

@ -11,6 +11,7 @@ import {
useMemo,
} from '@wordpress/element';
import { objectHasProp } from '@woocommerce/types';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -39,7 +40,6 @@ import {
reducer as emitReducer,
} from './event-emit';
import { useValidationContext } from '../../validation';
import { useStoreNotices } from '../../../hooks/use-store-notices';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import { getCustomerPaymentMethods } from './utils';
@ -70,7 +70,9 @@ export const PaymentMethodDataProvider = ( {
} = useCheckoutContext();
const { isEditor, getPreviewData } = useEditorContext();
const { setValidationErrors } = useValidationContext();
const { addErrorNotice, removeNotice } = useStoreNotices();
const { createErrorNotice: addErrorNotice, removeNotice } = useDispatch(
'core/notices'
);
const {
isSuccessResponse,
isErrorResponse,

View File

@ -16,6 +16,7 @@ import type {
ExpressPaymentMethodConfigInstance,
} from '@woocommerce/type-defs/payments';
import { useDebouncedCallback } from 'use-debounce';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -23,7 +24,6 @@ import { useDebouncedCallback } from 'use-debounce';
import { useEditorContext } from '../../editor-context';
import { useCustomerDataContext } from '../customer';
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
import { useStoreNotices } from '../../../hooks/use-store-notices';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import type { PaymentMethodsDispatcherType } from './types';
import { useShippingData } from '../../../hooks/shipping/use-shipping-data';
@ -68,7 +68,7 @@ const usePaymentMethodRegistration = (
selectedShippingMethods,
paymentRequirements,
} );
const { addErrorNotice } = useStoreNotices();
const { createErrorNotice } = useDispatch( 'core/notices' );
useEffect( () => {
canPayArgument.current = {
@ -142,7 +142,7 @@ const usePaymentMethodRegistration = (
),
paymentMethod.paymentMethodId
);
addErrorNotice( `${ errorText } ${ e }`, {
createErrorNotice( `${ errorText } ${ e }`, {
context: noticeContext,
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
} );
@ -157,7 +157,7 @@ const usePaymentMethodRegistration = (
// That's why we track "is initialized" state here.
setIsInitialized( true );
}, [
addErrorNotice,
createErrorNotice,
dispatcher,
isEditor,
noticeContext,

View File

@ -4,11 +4,13 @@
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Notice } from 'wordpress-components';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import './style.scss';
import { useStoreNoticesContext } from '../context';
const getWooClassName = ( { status = 'default' } ) => {
switch ( status ) {
@ -23,10 +25,23 @@ const getWooClassName = ( { status = 'default' } ) => {
return '';
};
const StoreNoticesContainer = ( { className, notices, removeNotice } ) => {
const regularNotices = notices.filter(
( notice ) => notice.type !== 'snackbar'
);
export const StoreNoticesContainer = ( {
className,
context = 'default',
additionalNotices = [],
} ) => {
const { isSuppressed } = useStoreNoticesContext();
const { notices } = useSelect( ( select ) => {
const store = select( 'core/notices' );
return {
notices: store.getNotices( context ),
};
} );
const { removeNotice } = useDispatch( 'core/notices' );
const regularNotices = notices
.filter( ( notice ) => notice.type !== 'snackbar' )
.concat( additionalNotices );
if ( ! regularNotices.length ) {
return null;
@ -34,7 +49,7 @@ const StoreNoticesContainer = ( { className, notices, removeNotice } ) => {
const wrapperClass = classnames( className, 'wc-block-components-notices' );
return (
return isSuppressed ? null : (
<div className={ wrapperClass }>
{ regularNotices.map( ( props ) => (
<Notice
@ -46,7 +61,7 @@ const StoreNoticesContainer = ( { className, notices, removeNotice } ) => {
) }
onRemove={ () => {
if ( props.isDismissible ) {
removeNotice( props.id );
removeNotice( props.id, context );
}
} }
>
@ -69,5 +84,3 @@ StoreNoticesContainer.propTypes = {
} )
),
};
export default StoreNoticesContainer;

View File

@ -2,20 +2,7 @@
* External dependencies
*/
import PropTypes from 'prop-types';
import {
createContext,
useContext,
useCallback,
useState,
} from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { useStoreEvents } from '../../hooks/use-store-events';
import { useEditorContext } from '../editor-context';
import StoreNoticesContainer from './components/store-notices-container';
import { createContext, useContext, useState } from '@wordpress/element';
/**
* @typedef {import('@woocommerce/type-defs/contexts').NoticeContext} NoticeContext
@ -23,11 +10,8 @@ import StoreNoticesContainer from './components/store-notices-container';
*/
const StoreNoticesContext = createContext( {
notices: [],
createNotice: ( status, text, props ) => void { status, text, props },
removeNotice: ( id, ctxt ) => void { id, ctxt },
setIsSuppressed: ( val ) => void { val },
context: 'wc/core',
isSuppressed: false,
} );
/**
@ -42,89 +26,24 @@ export const useStoreNoticesContext = () => {
/**
* Provides an interface for blocks to add notices to the frontend UI.
*
* Statuses map to https://github.com/WordPress/gutenberg/tree/master/packages/components/src/notice
* - Default (no status)
* - Error
* - Warning
* - Info
* - Success
*
* @param {Object} props Incoming props for the component.
* @param {JSX.Element} props.children The Elements wrapped by this component.
* @param {string} [props.className] CSS class used.
* @param {boolean} [props.createNoticeContainer] Whether to create a notice container or not.
* @param {string} [props.context] The notice context for notices being rendered.
*/
export const StoreNoticesProvider = ( {
children,
className = '',
createNoticeContainer = true,
context = 'wc/core',
} ) => {
const { createNotice, removeNotice } = useDispatch( 'core/notices' );
export const StoreNoticesProvider = ( { children } ) => {
const [ isSuppressed, setIsSuppressed ] = useState( false );
const { dispatchStoreEvent } = useStoreEvents();
const { isEditor } = useEditorContext();
const createNoticeWithContext = useCallback(
( status = 'default', content = '', options = {} ) => {
createNotice( status, content, {
...options,
context: options.context || context,
} );
dispatchStoreEvent( 'store-notice-create', {
status,
content,
options,
} );
},
[ createNotice, dispatchStoreEvent, context ]
);
const removeNoticeWithContext = useCallback(
( id, ctxt = context ) => {
removeNotice( id, ctxt );
},
[ removeNotice, context ]
);
const { notices } = useSelect(
( select ) => {
return {
notices: select( 'core/notices' ).getNotices( context ),
};
},
[ context ]
);
const contextValue = {
notices,
createNotice: createNoticeWithContext,
removeNotice: removeNoticeWithContext,
context,
setIsSuppressed,
isSuppressed,
};
const noticeOutput = isSuppressed ? null : (
<StoreNoticesContainer
className={ className }
notices={ contextValue.notices }
removeNotice={ contextValue.removeNotice }
isEditor={ isEditor }
/>
);
return (
<StoreNoticesContext.Provider value={ contextValue }>
{ createNoticeContainer && noticeOutput }
{ children }
</StoreNoticesContext.Provider>
);
};
StoreNoticesProvider.propTypes = {
className: PropTypes.string,
createNoticeContainer: PropTypes.bool,
children: PropTypes.node,
context: PropTypes.string,
};

View File

@ -8,6 +8,7 @@
"../../settings/blocks/index.ts",
"../../base/hooks/index.js",
"../../base/utils/",
"../../utils",
"../../data/",
"../../types/",
"../components",

View File

@ -7,7 +7,7 @@ import {
useExpressPaymentMethods,
} from '@woocommerce/base-context/hooks';
import {
StoreNoticesProvider,
StoreNoticesContainer,
useCheckoutContext,
usePaymentMethodDataContext,
} from '@woocommerce/base-context';
@ -57,11 +57,10 @@ const CartExpressPayment = () => {
>
<div className="wc-block-components-express-payment wc-block-components-express-payment--cart">
<div className="wc-block-components-express-payment__content">
<StoreNoticesProvider
<StoreNoticesContainer
context={ noticeContexts.EXPRESS_PAYMENTS }
>
<ExpressPaymentMethods />
</StoreNoticesProvider>
/>
<ExpressPaymentMethods />
</div>
</div>
</LoadingMask>

View File

@ -7,7 +7,7 @@ import {
useExpressPaymentMethods,
} from '@woocommerce/base-context/hooks';
import {
StoreNoticesProvider,
StoreNoticesContainer,
useCheckoutContext,
usePaymentMethodDataContext,
useEditorContext,
@ -44,9 +44,9 @@ const CheckoutExpressPayment = () => {
// when a payment method fails to register.
if ( isEditor || CURRENT_USER_IS_ADMIN ) {
return (
<StoreNoticesProvider
<StoreNoticesContainer
context={ noticeContexts.EXPRESS_PAYMENTS }
></StoreNoticesProvider>
/>
);
}
return null;
@ -81,17 +81,16 @@ const CheckoutExpressPayment = () => {
</Title>
</div>
<div className="wc-block-components-express-payment__content">
<StoreNoticesProvider
<StoreNoticesContainer
context={ noticeContexts.EXPRESS_PAYMENTS }
>
<p>
{ __(
'In a hurry? Use one of our express checkout options:',
'woo-gutenberg-products-block'
) }
</p>
<ExpressPaymentMethods />
</StoreNoticesProvider>
/>
<p>
{ __(
'In a hurry? Use one of our express checkout options:',
'woo-gutenberg-products-block'
) }
</p>
<ExpressPaymentMethods />
</div>
</div>
</LoadingMask>

View File

@ -6,6 +6,7 @@ import { Component } from 'react';
import PropTypes from 'prop-types';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import { StoreNoticesContainer } from '@woocommerce/base-context';
import { noticeContexts } from '@woocommerce/base-context/hooks';
class PaymentMethodErrorBoundary extends Component {
state = { errorMessage: '', hasError: false };
@ -44,7 +45,12 @@ class PaymentMethodErrorBoundary extends Component {
status: 'error',
},
];
return <StoreNoticesContainer notices={ notices } />;
return (
<StoreNoticesContainer
additionalNotices={ notices }
context={ noticeContexts.PAYMENTS }
/>
);
}
return this.props.children;

View File

@ -5,7 +5,6 @@ import {
usePaymentMethods,
usePaymentMethodInterface,
useEmitResponse,
useStoreNotices,
useStoreEvents,
} from '@woocommerce/base-context/hooks';
import { cloneElement, useCallback } from '@wordpress/element';
@ -15,6 +14,7 @@ import {
} from '@woocommerce/base-context';
import classNames from 'classnames';
import RadioControlAccordion from '@woocommerce/base-components/radio-control-accordion';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -39,7 +39,7 @@ const PaymentMethodOptions = () => {
...paymentMethodInterface
} = usePaymentMethodInterface();
const { noticeContexts } = useEmitResponse();
const { removeNotice } = useStoreNotices();
const { removeNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
const { isEditor } = useEditorContext();

View File

@ -8,10 +8,10 @@ import RadioControl from '@woocommerce/base-components/radio-control';
import {
usePaymentMethodInterface,
usePaymentMethods,
useStoreNotices,
useStoreEvents,
useEmitResponse,
} from '@woocommerce/base-context/hooks';
import { useDispatch } from '@wordpress/data';
/**
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
@ -61,7 +61,7 @@ const SavedPaymentMethodOptions = () => {
const { paymentMethods } = usePaymentMethods();
const paymentMethodInterface = usePaymentMethodInterface();
const { noticeContexts } = useEmitResponse();
const { removeNotice } = useStoreNotices();
const { removeNotice } = useDispatch( 'core/notices' );
const { dispatchCheckoutEvent } = useStoreEvents();
const options = useMemo( () => {

View File

@ -5,7 +5,10 @@ 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 { ValidationContextProvider } from '@woocommerce/base-context';
import {
ValidationContextProvider,
StoreNoticesContainer,
} 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';
@ -84,7 +87,8 @@ const Block = ( { attributes, children, scrollToTop } ) => (
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreSnackbarNoticesProvider context="wc/cart">
<StoreNoticesProvider context="wc/cart">
<StoreNoticesProvider>
<StoreNoticesContainer context="wc/cart" />
<SlotFillProvider>
<CartProvider>
<Cart attributes={ attributes }>{ children }</Cart>

View File

@ -9,7 +9,7 @@ import { TotalsWrapper } from '@woocommerce/blocks-checkout';
const Block = ( { className }: { className: string } ): JSX.Element | null => {
const couponsEnabled = getSetting( 'couponsEnabled', true );
const { applyCoupon, isApplyingCoupon } = useStoreCartCoupons();
const { applyCoupon, isApplyingCoupon } = useStoreCartCoupons( 'wc/cart' );
if ( ! couponsEnabled ) {
return null;

View File

@ -27,7 +27,7 @@ const DiscountSlotFill = (): JSX.Element => {
const Block = ( { className }: { className: string } ): JSX.Element => {
const { cartTotals, cartCoupons } = useStoreCart();
const { removeCoupon, isRemovingCoupon } = useStoreCartCoupons();
const { removeCoupon, isRemovingCoupon } = useStoreCartCoupons( 'wc/cart' );
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
return (

View File

@ -3,9 +3,10 @@
*/
import classnames from 'classnames';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
import { useStoreCart, useStoreNotices } from '@woocommerce/base-context/hooks';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import { useEffect } from '@wordpress/element';
import { decodeEntities } from '@wordpress/html-entities';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
@ -21,17 +22,17 @@ const FrontendBlock = ( {
} ): JSX.Element | null => {
const { cartItems, cartIsLoading, cartItemErrors } = useStoreCart();
const { hasDarkControls } = useCartBlockContext();
const { addErrorNotice } = useStoreNotices();
const { createErrorNotice } = useDispatch( 'core/notices' );
// Ensures any cart errors listed in the API response get shown.
useEffect( () => {
cartItemErrors.forEach( ( error ) => {
addErrorNotice( decodeEntities( error.message ), {
createErrorNotice( decodeEntities( error.message ), {
isDismissible: true,
id: error.code,
} );
} );
}, [ addErrorNotice, cartItemErrors ] );
}, [ createErrorNotice, cartItemErrors ] );
if ( cartIsLoading || cartItems.length >= 1 ) {
return (

View File

@ -4,15 +4,17 @@
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { createInterpolateElement, useEffect } from '@wordpress/element';
import { useStoreCart, useStoreNotices } from '@woocommerce/base-context/hooks';
import { useStoreCart } from '@woocommerce/base-context/hooks';
import {
useCheckoutContext,
useValidationContext,
ValidationContextProvider,
StoreNoticesProvider,
CheckoutProvider,
} from '@woocommerce/base-context';
import { StoreSnackbarNoticesProvider } from '@woocommerce/base-context/providers';
import {
StoreSnackbarNoticesProvider,
StoreNoticesContainer,
} from '@woocommerce/base-context/providers';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
@ -28,6 +30,8 @@ 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 { StoreNoticesProvider } from '../../base/context/providers';
const LoginPrompt = () => {
return (
@ -102,7 +106,6 @@ const ScrollOnError = ( {
}: {
scrollToTop: ( props: Record< string, unknown > ) => void;
} ): null => {
const { hasNoticesOfType } = useStoreNotices();
const {
hasError: checkoutHasError,
isIdle: checkoutIsIdle,
@ -115,7 +118,7 @@ const ScrollOnError = ( {
const hasErrorsToDisplay =
checkoutIsIdle &&
checkoutHasError &&
( hasValidationErrors || hasNoticesOfType( 'default' ) );
( hasValidationErrors || hasNoticesOfType( 'wc/checkout', 'default' ) );
useEffect( () => {
let scrollToTopTimeout: number;
@ -144,48 +147,59 @@ const Block = ( {
attributes: Attributes;
children: React.ReactChildren;
scrollToTop: ( props: Record< string, unknown > ) => void;
} ): JSX.Element => (
<BlockErrorBoundary
header={ __( 'Something went wrong…', 'woo-gutenberg-products-block' ) }
text={ createInterpolateElement(
__(
'The checkout has encountered an unexpected error. <button>Try reloading the page</button>. If the error persists, please get in touch with us so we can assist.',
} ): JSX.Element => {
return (
<BlockErrorBoundary
header={ __(
'Something went wrong…',
'woo-gutenberg-products-block'
),
{
button: (
<button
className="wc-block-link-button"
onClick={ reloadPage }
/>
) }
text={ createInterpolateElement(
__(
'The checkout has encountered an unexpected error. <button>Try reloading the page</button>. If the error persists, please get in touch with us so we can assist.',
'woo-gutenberg-products-block'
),
}
) }
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreSnackbarNoticesProvider context="wc/checkout">
<StoreNoticesProvider context="wc/checkout">
<ValidationContextProvider>
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
<SlotFillProvider>
<CheckoutProvider>
<SidebarLayout
className={ classnames( 'wc-block-checkout', {
'has-dark-controls':
attributes.hasDarkControls,
} ) }
>
<Checkout attributes={ attributes }>
{ children }
</Checkout>
<ScrollOnError scrollToTop={ scrollToTop } />
</SidebarLayout>
</CheckoutProvider>
</SlotFillProvider>
</ValidationContextProvider>
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
</BlockErrorBoundary>
);
{
button: (
<button
className="wc-block-link-button"
onClick={ reloadPage }
/>
),
}
) }
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreSnackbarNoticesProvider context="wc/checkout">
<StoreNoticesProvider>
<StoreNoticesContainer context="wc/checkout" />
<ValidationContextProvider>
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
<SlotFillProvider>
<CheckoutProvider>
<SidebarLayout
className={ classnames(
'wc-block-checkout',
{
'has-dark-controls':
attributes.hasDarkControls,
}
) }
>
<Checkout attributes={ attributes }>
{ children }
</Checkout>
<ScrollOnError
scrollToTop={ scrollToTop }
/>
</SidebarLayout>
</CheckoutProvider>
</SlotFillProvider>
</ValidationContextProvider>
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
</BlockErrorBoundary>
);
};
export default withScrollToTop( Block );

View File

@ -13,7 +13,9 @@ const Block = ( {
} ): JSX.Element | null => {
const couponsEnabled = getSetting( 'couponsEnabled', true );
const { applyCoupon, isApplyingCoupon } = useStoreCartCoupons();
const { applyCoupon, isApplyingCoupon } = useStoreCartCoupons(
'wc/checkout'
);
if ( ! couponsEnabled ) {
return null;

View File

@ -27,7 +27,9 @@ const DiscountSlotFill = (): JSX.Element => {
const Block = ( { className = '' }: { className?: string } ): JSX.Element => {
const { cartTotals, cartCoupons } = useStoreCart();
const { removeCoupon, isRemovingCoupon } = useStoreCartCoupons();
const { removeCoupon, isRemovingCoupon } = useStoreCartCoupons(
'wc/checkout'
);
const totalsCurrency = getCurrencyFromPriceResponse( cartTotals );
return (

View File

@ -5,16 +5,14 @@ import classnames from 'classnames';
import { useStoreCart, useEmitResponse } from '@woocommerce/base-context/hooks';
import { withFilteredAttributes } from '@woocommerce/shared-hocs';
import { FormStep } from '@woocommerce/base-components/cart-checkout';
import {
useCheckoutContext,
StoreNoticesProvider,
} from '@woocommerce/base-context';
import { useCheckoutContext } from '@woocommerce/base-context';
/**
* Internal dependencies
*/
import Block from './block';
import attributes from './attributes';
import { StoreNoticesContainer } from '../../../../base/context/providers';
const FrontendBlock = ( {
title,
@ -48,9 +46,8 @@ const FrontendBlock = ( {
description={ description }
showStepNumber={ showStepNumber }
>
<StoreNoticesProvider context={ noticeContexts.PAYMENTS }>
<Block />
</StoreNoticesProvider>
<StoreNoticesContainer context={ noticeContexts.PAYMENTS } />
<Block />
{ children }
</FormStep>
);

View File

@ -6,6 +6,10 @@ import PropTypes from 'prop-types';
import { ProductListContainer } from '@woocommerce/base-components/product-list';
import { InnerBlockLayoutContextProvider } from '@woocommerce/shared-context';
import { gridBlockPreview } from '@woocommerce/resource-previews';
import {
StoreNoticesProvider,
StoreNoticesContainer,
} from '@woocommerce/base-context';
/**
* The All Products Block.
@ -36,6 +40,9 @@ class Block extends Component {
parentName="woocommerce/all-products"
parentClassName="wc-block-grid"
>
<StoreNoticesProvider>
<StoreNoticesContainer context={ 'wc/all-products' } />
</StoreNoticesProvider>
<ProductListContainer
attributes={ attributes }
urlParameterSuffix={ urlParameterSuffix }

View File

@ -7,7 +7,10 @@ import {
InnerBlockLayoutContextProvider,
ProductDataContextProvider,
} from '@woocommerce/shared-context';
import { StoreNoticesProvider } from '@woocommerce/base-context';
import {
StoreNoticesProvider,
StoreNoticesContainer,
} from '@woocommerce/base-context';
import { useStoreEvents } from '@woocommerce/base-context/hooks';
/**
@ -46,9 +49,10 @@ const Block = ( { isLoading, product, children } ) => {
product={ product }
isLoading={ isLoading }
>
<StoreNoticesProvider context={ noticeContext }>
<div className={ className }>{ children }</div>
<StoreNoticesProvider>
<StoreNoticesContainer context={ noticeContext } />
</StoreNoticesProvider>
<div className={ className }>{ children }</div>
</ProductDataContextProvider>
</InnerBlockLayoutContextProvider>
);

View File

@ -214,13 +214,8 @@
/**
* @typedef NoticeContext
*
* @property {Array<StoreNoticeObject>} notices An array of notice objects.
* @property {function(string,string,any):undefined} createNotice Creates a notice for the given arguments.
* @property {function(string, any):undefined} createSnackbarNotice Creates a snackbar notice type.
* @property {function(string,string=):undefined} removeNotice Removes a notice with the given id and context
* @property {string} context The current context identifier for the notice
* provider
* @property {function(boolean):void} setIsSuppressed Consumers can use this setter to suppress
* @property {function(boolean):void} setIsSuppressed Consumers can use this setter to suppress notices.
* @property {boolean} isSuppressed Whether notices should be hidden/suppressed.
*/
/**

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { dispatch, select } from '@wordpress/data';
import { Notice } from '@wordpress/notices';
export const hasNoticesOfType = (
context = '',
type: 'default' | 'snackbar'
): boolean => {
const notices: Notice[] = select( 'core/notices' ).getNotices( context );
return notices.some( ( notice: Notice ) => notice.type === type );
};
export const removeNoticesByStatus = ( status: string, context = '' ): void => {
const notices = select( 'core/notices' ).getNotices();
const { removeNotice } = dispatch( 'core/notices' );
const noticesOfType = notices.filter(
( notice ) => notice.status === status
);
noticesOfType.forEach( ( notice ) => removeNotice( notice.id, context ) );
};

View File

@ -0,0 +1,122 @@
/**
* External dependencies
*/
import { select, dispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import { hasNoticesOfType, removeNoticesByStatus } from '../notices';
jest.mock( '@wordpress/data' );
describe( 'Notice utils', () => {
beforeEach( () => {
jest.resetAllMocks();
} );
describe( 'hasNoticesOfType', () => {
it( 'Correctly returns if there are notices of a given type in the core data store', () => {
select.mockReturnValue( {
getNotices: jest.fn().mockReturnValue( [
{
id: 'coupon-form',
status: 'error',
content:
'Coupon cannot be removed because it is not already applied to the cart.',
spokenMessage:
'Coupon cannot be removed because it is not already applied to the cart.',
isDismissible: true,
actions: [],
type: 'default',
icon: null,
explicitDismiss: false,
},
] ),
} );
const hasSnackbarNotices = hasNoticesOfType(
'wc/cart',
'snackbar'
);
const hasDefaultNotices = hasNoticesOfType( 'wc/cart', 'default' );
expect( hasDefaultNotices ).toBe( true );
expect( hasSnackbarNotices ).toBe( false );
} );
it( 'Handles notices being empty', () => {
select.mockReturnValue( {
getNotices: jest.fn().mockReturnValue( [] ),
} );
const hasDefaultNotices = hasNoticesOfType( 'wc/cart', 'default' );
expect( hasDefaultNotices ).toBe( false );
} );
} );
describe( 'removeNoticesByStatus', () => {
it( 'Correctly removes notices of a given status', () => {
select.mockReturnValue( {
getNotices: jest.fn().mockReturnValue( [
{
id: 'coupon-form',
status: 'error',
content:
'Coupon cannot be removed because it is not already applied to the cart.',
spokenMessage:
'Coupon cannot be removed because it is not already applied to the cart.',
isDismissible: true,
actions: [],
type: 'default',
icon: null,
explicitDismiss: false,
},
{
id: 'address-form',
status: 'error',
content: 'Address invalid',
spokenMessage: 'Address invalid',
isDismissible: true,
actions: [],
type: 'default',
icon: null,
explicitDismiss: false,
},
{
id: 'some-warning',
status: 'warning',
content: 'Warning notice.',
spokenMessage: 'Warning notice.',
isDismissible: true,
actions: [],
type: 'default',
icon: null,
explicitDismiss: false,
},
] ),
} );
dispatch.mockReturnValue( {
removeNotice: jest.fn(),
} );
removeNoticesByStatus( 'error' );
expect( dispatch().removeNotice ).toHaveBeenNthCalledWith(
1,
'coupon-form',
''
);
expect( dispatch().removeNotice ).toHaveBeenNthCalledWith(
2,
'address-form',
''
);
} );
it( 'Handles notices being empty', () => {
select.mockReturnValue( {
getNotices: jest.fn().mockReturnValue( [] ),
} );
dispatch.mockReturnValue( {
removeNotice: jest.fn(),
} );
removeNoticesByStatus( 'empty' );
expect( dispatch().removeNotice ).not.toBeCalled();
} );
} );
} );