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

* Use wp store directly instead of React Context

We are using now actions directly from wp store in 'useStoreCartCoupons'
hook to apply and remove coupon.

* Remove unused "useStoreSnackbarNotices" related
files

* Add NoticeContext TS definition

* Remove the Provider references and refactor code

* Fix snackbar notice creation bug

* Fix "clear out snackbar coupon notice" bug

* Update "notices" API documentation

Remove snackbar hooks mentions since it's not used anymore
This commit is contained in:
Saad Tarhi 2022-05-25 22:00:47 +01:00 committed by GitHub
parent d3c2f638fa
commit 25cb047483
10 changed files with 103 additions and 375 deletions

View File

@ -13,7 +13,6 @@ import type { StoreCartCoupon } from '@woocommerce/types';
* Internal dependencies * Internal dependencies
*/ */
import { useStoreCart } from './use-store-cart'; import { useStoreCart } from './use-store-cart';
import { useStoreSnackbarNotices } from '../use-store-snackbar-notices';
import { useValidationContext } from '../../providers/validation'; import { useValidationContext } from '../../providers/validation';
/** /**
@ -27,7 +26,7 @@ import { useValidationContext } from '../../providers/validation';
export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => { export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
const { cartCoupons, cartIsLoading } = useStoreCart(); const { cartCoupons, cartIsLoading } = useStoreCart();
const { createErrorNotice } = useDispatch( 'core/notices' ); const { createErrorNotice } = useDispatch( 'core/notices' );
const { addSnackbarNotice } = useStoreSnackbarNotices(); const { createNotice } = useDispatch( 'core/notices' );
const { setValidationErrors } = useValidationContext(); const { setValidationErrors } = useValidationContext();
const results: Pick< const results: Pick<
@ -52,7 +51,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
applyCoupon( couponCode ) applyCoupon( couponCode )
.then( ( result ) => { .then( ( result ) => {
if ( result === true ) { if ( result === true ) {
addSnackbarNotice( createNotice(
'info',
sprintf( sprintf(
/* translators: %s coupon code. */ /* translators: %s coupon code. */
__( __(
@ -63,6 +63,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
), ),
{ {
id: 'coupon-form', id: 'coupon-form',
type: 'snackbar',
context,
} }
); );
} }
@ -83,7 +85,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
removeCoupon( couponCode ) removeCoupon( couponCode )
.then( ( result ) => { .then( ( result ) => {
if ( result === true ) { if ( result === true ) {
addSnackbarNotice( createNotice(
'info',
sprintf( sprintf(
/* translators: %s coupon code. */ /* translators: %s coupon code. */
__( __(
@ -94,6 +97,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
), ),
{ {
id: 'coupon-form', id: 'coupon-form',
type: 'snackbar',
context,
} }
); );
} }
@ -115,7 +120,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
isRemovingCoupon, isRemovingCoupon,
}; };
}, },
[ createErrorNotice, addSnackbarNotice ] [ createErrorNotice, createNotice ]
); );
return { return {

View File

@ -1,51 +0,0 @@
/**
* External dependencies
*/
import { render, act } from '@testing-library/react';
import { StoreSnackbarNoticesProvider } from '@woocommerce/base-context/providers';
/**
* Internal dependencies
*/
import { useStoreSnackbarNotices } from '../use-store-snackbar-notices';
describe( 'useStoreNoticesWithSnackbar', () => {
function setup() {
const returnVal = {};
function TestComponent() {
Object.assign( returnVal, useStoreSnackbarNotices() );
return null;
}
render(
<StoreSnackbarNoticesProvider>
<TestComponent />
</StoreSnackbarNoticesProvider>
);
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( [] );
// Add snackbar notice.
act( () => {
storeNoticesData.addSnackbarNotice( 'Snackbar notice' );
} );
expect( storeNoticesData.notices.length ).toBe( 1 );
// Remove all remaining notices.
act( () => {
storeNoticesData.removeNotices();
} );
expect( storeNoticesData.notices.length ).toBe( 0 );
} );
} );

View File

@ -1,52 +0,0 @@
/**
* External dependencies
*/
import { useMemo, useRef, useEffect } from '@wordpress/element';
import { useStoreSnackbarNoticesContext } from '@woocommerce/base-context/providers';
export const useStoreSnackbarNotices = () => {
const {
notices,
createSnackbarNotice,
removeSnackbarNotice,
setIsSuppressed,
} = useStoreSnackbarNoticesContext();
// 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(
() => ( {
removeNotices: ( status = null ) => {
currentNotices.current.forEach( ( notice ) => {
if ( status === null || notice.status === status ) {
removeSnackbarNotice( notice.id );
}
} );
},
removeSnackbarNotice,
} ),
[ removeSnackbarNotice ]
);
const noticeCreators = useMemo(
() => ( {
addSnackbarNotice: ( text, noticeProps = {} ) => {
createSnackbarNotice( text, noticeProps );
},
} ),
[ createSnackbarNotice ]
);
return {
notices,
...noticesApi,
...noticeCreators,
setIsSuppressed,
};
};

View File

@ -1,18 +1,33 @@
/** /**
* External dependencies * External dependencies
*/ */
import PropTypes from 'prop-types';
import { SnackbarList } from 'wordpress-components'; import { SnackbarList } from 'wordpress-components';
import classnames from 'classnames'; import classnames from 'classnames';
import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout'; import { __experimentalApplyCheckoutFilter } from '@woocommerce/blocks-checkout';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { useEditorContext } from '../../editor-context';
const EMPTY_SNACKBAR_NOTICES = {}; const EMPTY_SNACKBAR_NOTICES = {};
const SnackbarNoticesContainer = ( { export const SnackbarNoticesContainer = ( {
className, className,
notices, context = 'default',
removeNotice,
isEditor,
} ) => { } ) => {
const { isEditor } = useEditorContext();
const { notices } = useSelect( ( select ) => {
const store = select( 'core/notices' );
return {
notices: store.getNotices( context ),
};
} );
const { removeNotice } = useDispatch( 'core/notices' );
if ( isEditor ) { if ( isEditor ) {
return null; return null;
} }
@ -47,9 +62,24 @@ const SnackbarNoticesContainer = ( {
<SnackbarList <SnackbarList
notices={ visibleNotices } notices={ visibleNotices }
className={ wrapperClass } className={ wrapperClass }
onRemove={ removeNotice } onRemove={ () => {
visibleNotices.forEach( ( notice ) =>
removeNotice( notice.id, context )
);
} }
/> />
); );
}; };
export default SnackbarNoticesContainer; 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' ] ),
} )
),
};

View File

@ -1,125 +0,0 @@
/**
* External dependencies
*/
import PropTypes from 'prop-types';
import {
createContext,
useContext,
useCallback,
useState,
} from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import SnackbarNoticesContainer from '@woocommerce/base-context/providers/store-snackbar-notices/components/snackbar-notices-container';
/**
* Internal dependencies
*/
import { useStoreEvents } from '../../hooks/use-store-events';
import { useEditorContext } from '../editor-context';
/**
* @typedef {import('@woocommerce/type-defs/contexts').NoticeContext} NoticeContext
* @typedef {import('react')} React
*/
const StoreSnackbarNoticesContext = createContext( {
notices: [],
createSnackbarNotice: ( content, options ) => void { content, options },
removeSnackbarNotice: ( id, ctxt ) => void { id, ctxt },
setIsSuppressed: ( val ) => void { val },
context: 'wc/core',
} );
/**
* Returns the notices context values.
*
* @return {NoticeContext} The notice context value from the notice context.
*/
export const useStoreSnackbarNoticesContext = () => {
return useContext( StoreSnackbarNoticesContext );
};
/**
* 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 {React.ReactChildren} props.children The Elements wrapped by this component.
* @param {string} props.context The notice context for notices being rendered.
*/
export const StoreSnackbarNoticesProvider = ( {
children,
context = 'wc/core',
} ) => {
const { createNotice, removeNotice } = useDispatch( 'core/notices' );
const [ isSuppressed, setIsSuppressed ] = useState( false );
const { dispatchStoreEvent } = useStoreEvents();
const { isEditor } = useEditorContext();
const createSnackbarNotice = useCallback(
( content = '', options = {} ) => {
createNotice( 'default', content, {
...options,
type: 'snackbar',
context: options.context || context,
} );
dispatchStoreEvent( 'store-notice-create', {
status: 'default',
content,
options,
} );
},
[ createNotice, dispatchStoreEvent, context ]
);
const removeSnackbarNotice = useCallback(
( id, ctxt = context ) => {
removeNotice( id, ctxt );
},
[ removeNotice, context ]
);
const { notices } = useSelect(
( select ) => {
return {
notices: select( 'core/notices' ).getNotices( context ),
};
},
[ context ]
);
const contextValue = {
notices,
createSnackbarNotice,
removeSnackbarNotice,
context,
setIsSuppressed,
};
const snackbarNoticeOutput = isSuppressed ? null : (
<SnackbarNoticesContainer
notices={ contextValue.notices }
removeNotice={ contextValue.removeSnackbarNotice }
isEditor={ isEditor }
/>
);
return (
<StoreSnackbarNoticesContext.Provider value={ contextValue }>
{ children }
{ snackbarNoticeOutput }
</StoreSnackbarNoticesContext.Provider>
);
};
StoreSnackbarNoticesProvider.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
context: PropTypes.string,
};

View File

@ -1 +1 @@
export * from './context'; export * from './components/snackbar-notices-container';

View File

@ -8,6 +8,7 @@ import LoadingMask from '@woocommerce/base-components/loading-mask';
import { import {
ValidationContextProvider, ValidationContextProvider,
StoreNoticesContainer, StoreNoticesContainer,
SnackbarNoticesContainer,
} from '@woocommerce/base-context'; } from '@woocommerce/base-context';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings'; import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
@ -15,7 +16,6 @@ import { translateJQueryEventToNative } from '@woocommerce/base-utils';
import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top'; import withScrollToTop from '@woocommerce/base-hocs/with-scroll-to-top';
import { import {
StoreNoticesProvider, StoreNoticesProvider,
StoreSnackbarNoticesProvider,
CartProvider, CartProvider,
} from '@woocommerce/base-context/providers'; } from '@woocommerce/base-context/providers';
import { SlotFillProvider } from '@woocommerce/blocks-checkout'; import { SlotFillProvider } from '@woocommerce/blocks-checkout';
@ -86,17 +86,16 @@ const Block = ( { attributes, children, scrollToTop } ) => (
} }
showErrorMessage={ CURRENT_USER_IS_ADMIN } showErrorMessage={ CURRENT_USER_IS_ADMIN }
> >
<StoreSnackbarNoticesProvider context="wc/cart"> <SnackbarNoticesContainer context="wc/cart" />
<StoreNoticesProvider> <StoreNoticesProvider>
<StoreNoticesContainer context="wc/cart" /> <StoreNoticesContainer context="wc/cart" />
<SlotFillProvider> <SlotFillProvider>
<CartProvider> <CartProvider>
<Cart attributes={ attributes }>{ children }</Cart> <Cart attributes={ attributes }>{ children }</Cart>
<ScrollOnError scrollToTop={ scrollToTop } /> <ScrollOnError scrollToTop={ scrollToTop } />
</CartProvider> </CartProvider>
</SlotFillProvider> </SlotFillProvider>
</StoreNoticesProvider> </StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
</BlockErrorBoundary> </BlockErrorBoundary>
); );
export default withScrollToTop( Block ); export default withScrollToTop( Block );

View File

@ -10,11 +10,9 @@ import {
useValidationContext, useValidationContext,
ValidationContextProvider, ValidationContextProvider,
CheckoutProvider, CheckoutProvider,
SnackbarNoticesContainer,
} from '@woocommerce/base-context'; } from '@woocommerce/base-context';
import { import { StoreNoticesContainer } from '@woocommerce/base-context/providers';
StoreSnackbarNoticesProvider,
StoreNoticesContainer,
} from '@woocommerce/base-context/providers';
import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary'; import BlockErrorBoundary from '@woocommerce/base-components/block-error-boundary';
import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout'; import { SidebarLayout } from '@woocommerce/base-components/sidebar-layout';
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings'; import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
@ -170,34 +168,28 @@ const Block = ( {
) } ) }
showErrorMessage={ CURRENT_USER_IS_ADMIN } showErrorMessage={ CURRENT_USER_IS_ADMIN }
> >
<StoreSnackbarNoticesProvider context="wc/checkout"> <SnackbarNoticesContainer context="wc/checkout" />
<StoreNoticesProvider> <StoreNoticesProvider>
<StoreNoticesContainer context="wc/checkout" /> <StoreNoticesContainer context="wc/checkout" />
<ValidationContextProvider> <ValidationContextProvider>
{ /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ } { /* SlotFillProvider need to be defined before CheckoutProvider so fills have the SlotFill context ready when they mount. */ }
<SlotFillProvider> <SlotFillProvider>
<CheckoutProvider> <CheckoutProvider>
<SidebarLayout <SidebarLayout
className={ classnames( className={ classnames( 'wc-block-checkout', {
'wc-block-checkout', 'has-dark-controls':
{ attributes.hasDarkControls,
'has-dark-controls': } ) }
attributes.hasDarkControls, >
} <Checkout attributes={ attributes }>
) } { children }
> </Checkout>
<Checkout attributes={ attributes }> <ScrollOnError scrollToTop={ scrollToTop } />
{ children } </SidebarLayout>
</Checkout> </CheckoutProvider>
<ScrollOnError </SlotFillProvider>
scrollToTop={ scrollToTop } </ValidationContextProvider>
/> </StoreNoticesProvider>
</SidebarLayout>
</CheckoutProvider>
</SlotFillProvider>
</ValidationContextProvider>
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
</BlockErrorBoundary> </BlockErrorBoundary>
); );
}; };

View File

@ -18,3 +18,8 @@ export enum SHIPPING_ERROR_TYPES {
INVALID_ADDRESS = 'invalid_address', INVALID_ADDRESS = 'invalid_address',
UNKNOWN = 'unknown_error', UNKNOWN = 'unknown_error',
} }
export type NoticeContext = {
setIsSuppressed: ( val: boolean ) => undefined;
isSuppressed: boolean;
};

View File

@ -4,8 +4,6 @@
The `useStoreNotices()` hook allows reading and manipulating notices in the frontend. The `useStoreNotices()` hook allows reading and manipulating notices in the frontend.
Please refer to the `useStoreSnackbarNotices()` section for information on handling snackbar notices.
### API ### API
> _Note: if the context is not specified in `noticeProps` or `ctxt` params (depending on the method), the current context is used._ > _Note: if the context is not specified in `noticeProps` or `ctxt` params (depending on the method), the current context is used._
@ -146,110 +144,37 @@ Object of the form:
```JS ```JS
{ {
id: 'checkout', id: 'checkout',
type: string, type: string,
isDismissible: false, isDismissible: false,
} }
``` ```
Refer to the [Gutenberg docs](https://github.com/WordPress/gutenberg/blob/master/packages/notices/src/store/actions.js#L46) to know the available options. Refer to the [Gutenberg docs](https://github.com/WordPress/gutenberg/blob/master/packages/notices/src/store/actions.js#L46) to know the available options.
## useStoreSnackbarNotices()
The `useStoreNotices()` hook allows reading and manipulating snackbar notices in the frontend.
The snackbar is a small toast-like notification that appears at the bottom of a user's screen.
<img width="300" src="https://user-images.githubusercontent.com/5656702/124294673-dd803b80-db4f-11eb-81ec-02fb962d04ed.png" />
### API
#### `addSnackbarNotice( text = '', noticeProps = {} )`
Create a new snackbar notice.
| Argument | Type | Description |
| ------------- | ------ | -------------------------------------------------- |
| `text` | string | Text to be displayed in the notice. |
| `noticeProps` | Object | Object with the [notice options](#notice-options). |
#### `notices`
An array of the notices in the current context.
#### `removeNotices( status = null )`
Remove all notices from the current context. If a `status` is provided, only the notices with that status are removed.
| Argument | Type | Description |
| -------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `status` | string | Status that notices must match to be removed. If not provided, all notices of any status are removed. |
## StoreSnackbarNoticesProvider
The `StoreSnackbarNoticesProvider` allows managing snackbar notices in the frontend. Snackbar notices are displayed in the bottom left corner and disappear after a certain time.
Internally, it uses the `StoreNoticesContext` which relies on the [`notices` package](https://github.com/WordPress/gutenberg/tree/master/packages/notices) from Gutenberg.
### Actions
#### `createSnackbarNotice( content = '', options = {} )`
This action creates a new snackbar notice. If the context is not specified in the `options` object, the current context is used.
| Argument | Type | Description |
| --------- | ------ | -------------------------------------------------- |
| `content` | string | Text to be displayed in the notice. |
| `options` | Object | Object with the [notice options](#notice-options). |
#### `removeSnackbarNotice( id, ctx )`
This action removes an existing notice. If the context is not specified, the current context is used.
| Argument | Type | Description |
| -------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| `id` | string | Id of the notice to remove. |
| `ctx` | string | Context where the notice to remove is stored. If the context is not specified, the current context is used. |
#### `setIsSuppressed( val )`
Whether notices are suppressed. If true, it will hide the notices from the frontend.
| Argument | Type | Description |
| -------- | ------- | --------------------------- |
| `val` | boolean | Id of the notice to remove. |
## Example usage ## Example usage
The following example shows a `CheckoutProcessor` component that displays an error notice when the payment process fails and it removes it every time the payment is started. When the payment is completed correctly, it shows a snackbar notice. The following example shows a `CheckoutProcessor` component that displays an error notice when the payment process fails and it removes it every time the payment is started. When the payment is completed correctly, it shows a snackbar notice.
```JSX ```JSX
const CheckoutProcessor = () => { const CheckoutProcessor = () => {
const { addErrorNotice, removeNotice } = useStoreNotices(); const { addErrorNotice, removeNotice } = useStoreNotices();
const { addSnackbarNotice } = useStoreSnackbarNotices(); // ...
// ... const paymentFail = () => {
const paymentFail = () => { addErrorNotice( 'Something went wrong.', { id: 'checkout' } );
addErrorNotice( 'Something went wrong.', { id: 'checkout' } ); };
}; const paymentStart = () => {
const paymentStart = () => { removeNotice( 'checkout' );
removeNotice( 'checkout' ); };
}; // ...
const paymentSuccess = () => {
addSnackbarNotice( 'Payment successfully completed.' );
};
// ...
}; };
``` ```
```JSX ```JSX
<StoreNoticesSnackbarProvider context="wc/checkout"> <StoreNoticesProvider context="wc/checkout">
<StoreNoticesProvider context="wc/checkout"> // ...
// ... <CheckoutProcessor />
<CheckoutProcessor /> </StoreNoticesProvider>
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
``` ```
<!-- FEEDBACK --> <!-- FEEDBACK -->