From 25cb047483eac88584847a6c3bdfa8397d77e872 Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Wed, 25 May 2022 22:00:47 +0100 Subject: [PATCH] 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 --- .../hooks/cart/use-store-cart-coupons.ts | 15 ++- .../hooks/test/use-store-snackbar-notices.js | 51 ------- .../hooks/use-store-snackbar-notices.js | 52 -------- .../components/snackbar-notices-container.js | 42 +++++- .../store-snackbar-notices/context.js | 125 ------------------ .../providers/store-snackbar-notices/index.ts | 2 +- .../assets/js/blocks/cart/block.js | 23 ++-- .../assets/js/blocks/checkout/block.tsx | 56 ++++---- .../assets/js/types/type-defs/contexts.ts | 5 + .../docs/block-client-apis/notices.md | 107 +++------------ 10 files changed, 103 insertions(+), 375 deletions(-) delete mode 100644 plugins/woocommerce-blocks/assets/js/base/context/hooks/test/use-store-snackbar-notices.js delete mode 100644 plugins/woocommerce-blocks/assets/js/base/context/hooks/use-store-snackbar-notices.js delete mode 100644 plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/context.js diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts b/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts index 3c31c3b12b3..9b045d6e6a9 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/hooks/cart/use-store-cart-coupons.ts @@ -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 { diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/test/use-store-snackbar-notices.js b/plugins/woocommerce-blocks/assets/js/base/context/hooks/test/use-store-snackbar-notices.js deleted file mode 100644 index 493bde10f9e..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/test/use-store-snackbar-notices.js +++ /dev/null @@ -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( - - - - ); - - 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 ); - } ); -} ); diff --git a/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-store-snackbar-notices.js b/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-store-snackbar-notices.js deleted file mode 100644 index ff07554b774..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/hooks/use-store-snackbar-notices.js +++ /dev/null @@ -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, - }; -}; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js index b78ac2e39d5..87ee473c504 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/components/snackbar-notices-container.js @@ -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 = ( { { + 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' ] ), + } ) + ), +}; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/context.js b/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/context.js deleted file mode 100644 index 892e229c93a..00000000000 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/context.js +++ /dev/null @@ -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 : ( - - ); - - return ( - - { children } - { snackbarNoticeOutput } - - ); -}; - -StoreSnackbarNoticesProvider.propTypes = { - className: PropTypes.string, - children: PropTypes.node, - context: PropTypes.string, -}; diff --git a/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/index.ts b/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/index.ts index c38e8e82152..05f01b11123 100644 --- a/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/index.ts +++ b/plugins/woocommerce-blocks/assets/js/base/context/providers/store-snackbar-notices/index.ts @@ -1 +1 @@ -export * from './context'; +export * from './components/snackbar-notices-container'; diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js b/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js index cbd191f4c9f..b2f49db7a5d 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart/block.js @@ -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 } > - - - - - - { children } - - - - - + + + + + + { children } + + + + ); export default withScrollToTop( Block ); diff --git a/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx b/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx index 4cf069d553e..3c5112d8da4 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/checkout/block.tsx @@ -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 } > - - - - - { /* 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/plugins/woocommerce-blocks/assets/js/types/type-defs/contexts.ts b/plugins/woocommerce-blocks/assets/js/types/type-defs/contexts.ts index a2968ef6a94..7e1a47cd043 100644 --- a/plugins/woocommerce-blocks/assets/js/types/type-defs/contexts.ts +++ b/plugins/woocommerce-blocks/assets/js/types/type-defs/contexts.ts @@ -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; +}; diff --git a/plugins/woocommerce-blocks/docs/block-client-apis/notices.md b/plugins/woocommerce-blocks/docs/block-client-apis/notices.md index 1851d8520a9..d521f5e3b50 100644 --- a/plugins/woocommerce-blocks/docs/block-client-apis/notices.md +++ b/plugins/woocommerce-blocks/docs/block-client-apis/notices.md @@ -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. - - - -### 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 - - - // ... - - - + + // ... + + ```