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
*/
import { useStoreCart } from './use-store-cart';
import { useStoreSnackbarNotices } from '../use-store-snackbar-notices';
import { useValidationContext } from '../../providers/validation';
/**
@ -27,7 +26,7 @@ import { useValidationContext } from '../../providers/validation';
export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
const { cartCoupons, cartIsLoading } = useStoreCart();
const { createErrorNotice } = useDispatch( 'core/notices' );
const { addSnackbarNotice } = useStoreSnackbarNotices();
const { createNotice } = useDispatch( 'core/notices' );
const { setValidationErrors } = useValidationContext();
const results: Pick<
@ -52,7 +51,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
applyCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
addSnackbarNotice(
createNotice(
'info',
sprintf(
/* translators: %s coupon code. */
__(
@ -63,6 +63,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
),
{
id: 'coupon-form',
type: 'snackbar',
context,
}
);
}
@ -83,7 +85,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
removeCoupon( couponCode )
.then( ( result ) => {
if ( result === true ) {
addSnackbarNotice(
createNotice(
'info',
sprintf(
/* translators: %s coupon code. */
__(
@ -94,6 +97,8 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
),
{
id: 'coupon-form',
type: 'snackbar',
context,
}
);
}
@ -115,7 +120,7 @@ export const useStoreCartCoupons = ( context = '' ): StoreCartCoupon => {
isRemovingCoupon,
};
},
[ createErrorNotice, addSnackbarNotice ]
[ createErrorNotice, createNotice ]
);
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
*/
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 = {};
const SnackbarNoticesContainer = ( {
export const SnackbarNoticesContainer = ( {
className,
notices,
removeNotice,
isEditor,
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;
}
@ -47,9 +62,24 @@ const SnackbarNoticesContainer = ( {
<SnackbarList
notices={ visibleNotices }
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 {
ValidationContextProvider,
StoreNoticesContainer,
SnackbarNoticesContainer,
} from '@woocommerce/base-context';
import { CURRENT_USER_IS_ADMIN } from '@woocommerce/settings';
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 {
StoreNoticesProvider,
StoreSnackbarNoticesProvider,
CartProvider,
} from '@woocommerce/base-context/providers';
import { SlotFillProvider } from '@woocommerce/blocks-checkout';
@ -86,17 +86,16 @@ const Block = ( { attributes, children, scrollToTop } ) => (
}
showErrorMessage={ CURRENT_USER_IS_ADMIN }
>
<StoreSnackbarNoticesProvider context="wc/cart">
<StoreNoticesProvider>
<StoreNoticesContainer context="wc/cart" />
<SlotFillProvider>
<CartProvider>
<Cart attributes={ attributes }>{ children }</Cart>
<ScrollOnError scrollToTop={ scrollToTop } />
</CartProvider>
</SlotFillProvider>
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
<SnackbarNoticesContainer context="wc/cart" />
<StoreNoticesProvider>
<StoreNoticesContainer context="wc/cart" />
<SlotFillProvider>
<CartProvider>
<Cart attributes={ attributes }>{ children }</Cart>
<ScrollOnError scrollToTop={ scrollToTop } />
</CartProvider>
</SlotFillProvider>
</StoreNoticesProvider>
</BlockErrorBoundary>
);
export default withScrollToTop( Block );

View File

@ -10,11 +10,9 @@ import {
useValidationContext,
ValidationContextProvider,
CheckoutProvider,
SnackbarNoticesContainer,
} from '@woocommerce/base-context';
import {
StoreSnackbarNoticesProvider,
StoreNoticesContainer,
} from '@woocommerce/base-context/providers';
import { 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';
@ -170,34 +168,28 @@ const Block = ( {
) }
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>
<SnackbarNoticesContainer 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>
</BlockErrorBoundary>
);
};

View File

@ -18,3 +18,8 @@ export enum SHIPPING_ERROR_TYPES {
INVALID_ADDRESS = 'invalid_address',
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.
Please refer to the `useStoreSnackbarNotices()` section for information on handling snackbar notices.
### API
> _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
{
id: 'checkout',
type: string,
isDismissible: false,
id: 'checkout',
type: string,
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.
## 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
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
const CheckoutProcessor = () => {
const { addErrorNotice, removeNotice } = useStoreNotices();
const { addSnackbarNotice } = useStoreSnackbarNotices();
// ...
const paymentFail = () => {
addErrorNotice( 'Something went wrong.', { id: 'checkout' } );
};
const paymentStart = () => {
removeNotice( 'checkout' );
};
const paymentSuccess = () => {
addSnackbarNotice( 'Payment successfully completed.' );
};
// ...
const { addErrorNotice, removeNotice } = useStoreNotices();
// ...
const paymentFail = () => {
addErrorNotice( 'Something went wrong.', { id: 'checkout' } );
};
const paymentStart = () => {
removeNotice( 'checkout' );
};
// ...
};
```
```JSX
<StoreNoticesSnackbarProvider context="wc/checkout">
<StoreNoticesProvider context="wc/checkout">
// ...
<CheckoutProcessor />
</StoreNoticesProvider>
</StoreSnackbarNoticesProvider>
<StoreNoticesProvider context="wc/checkout">
// ...
<CheckoutProcessor />
</StoreNoticesProvider>
```
<!-- FEEDBACK -->