From 39b6c1c32072e1527256d03c40deb09bf2acffee Mon Sep 17 00:00:00 2001 From: Saad Tarhi Date: Fri, 24 Feb 2023 11:57:24 +0100 Subject: [PATCH] Improve the dismissal behavior of the incompatible gateways notice (https://github.com/woocommerce/woocommerce-blocks/pull/8299) * Fix notice persistence after dismissal - This fix applied to the `incompatible payment gateway notice`. - We used the same dismissal logic in the `sidebar compatibility notice` * Get incompatible payments when initialized We initially get the list of `globalPaymentMethods` shared from the back-end as incompatible payments, because the front-end `availablePaymentMethods` is empty before the `paymentMethodsInitialized` state * Introduce advanced notice dismissal handling We want to display a dismissed incompatible gateways notice, when the list of incompatible gateways is updated (e.g., a new incompatible gateway is enabled) * Use the full block name for the `Cart` & `Checkout` * Update variable name for comprehension * Fix TS errors * Remove unused imports --- .../js/base/hooks/use-local-storage-state.ts | 3 +- .../sidebar-notices/index.tsx | 5 + .../assets/js/data/payment/selectors.ts | 15 ++- .../index.tsx | 40 +++---- ...se-incompatible-payment-gateways-notice.ts | 103 ++++++++++++++++++ 5 files changed, 137 insertions(+), 29 deletions(-) create mode 100644 plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/use-incompatible-payment-gateways-notice.ts diff --git a/plugins/woocommerce-blocks/assets/js/base/hooks/use-local-storage-state.ts b/plugins/woocommerce-blocks/assets/js/base/hooks/use-local-storage-state.ts index ab817c268bb..b8b99787361 100644 --- a/plugins/woocommerce-blocks/assets/js/base/hooks/use-local-storage-state.ts +++ b/plugins/woocommerce-blocks/assets/js/base/hooks/use-local-storage-state.ts @@ -2,11 +2,12 @@ * External dependencies */ import { useEffect, useState } from '@wordpress/element'; +import { Dispatch, SetStateAction } from 'react'; export const useLocalStorageState = < T >( key: string, initialValue: T -): [ T, ( arg0: T ) => void ] => { +): [ T, Dispatch< SetStateAction< T > > ] => { const [ state, setState ] = useState< T >( () => { const valueInLocalStorage = window.localStorage.getItem( key ); if ( valueInLocalStorage ) { diff --git a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/sidebar-notices/index.tsx b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/sidebar-notices/index.tsx index 1fb2e516e07..92cd9f5d09e 100644 --- a/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/sidebar-notices/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/blocks/cart-checkout-shared/sidebar-notices/index.tsx @@ -98,6 +98,11 @@ const withSidebarNotices = createHigherOrderComponent( toggleDismissedStatus={ toggleIncompatiblePaymentGatewaysNoticeDismissedStatus } + block={ + isCheckout + ? 'woocommerce/checkout' + : 'woocommerce/cart' + } /> { isIncompatiblePaymentGatewaysNoticeDismissed ? ( diff --git a/plugins/woocommerce-blocks/assets/js/data/payment/selectors.ts b/plugins/woocommerce-blocks/assets/js/data/payment/selectors.ts index afc815d85ce..cf5cf71ff1b 100644 --- a/plugins/woocommerce-blocks/assets/js/data/payment/selectors.ts +++ b/plugins/woocommerce-blocks/assets/js/data/payment/selectors.ts @@ -111,13 +111,24 @@ export const getPaymentMethodData = ( state: PaymentState ) => { }; export const getIncompatiblePaymentMethods = ( state: PaymentState ) => { + const { + availablePaymentMethods, + availableExpressPaymentMethods, + paymentMethodsInitialized, + expressPaymentMethodsInitialized, + } = state; + + if ( ! paymentMethodsInitialized || ! expressPaymentMethodsInitialized ) { + return {}; + } + return Object.fromEntries( Object.entries( globalPaymentMethods ).filter( ( [ k ] ) => { return ! ( k in { - ...state.availablePaymentMethods, - ...state.availableExpressPaymentMethods, + ...availablePaymentMethods, + ...availableExpressPaymentMethods, } ); } ) diff --git a/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx b/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx index 1c33b987a35..103352aa272 100644 --- a/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx +++ b/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/index.tsx @@ -3,48 +3,36 @@ */ import { _n } from '@wordpress/i18n'; import { Notice, ExternalLink } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; -import { - useState, - createInterpolateElement, - useEffect, -} from '@wordpress/element'; +import { createInterpolateElement, useEffect } from '@wordpress/element'; import { Alert } from '@woocommerce/icons'; import { Icon } from '@wordpress/icons'; /** * Internal dependencies */ -import { STORE_KEY as PAYMENT_STORE_KEY } from '../../data/payment/constants'; +import { useIncompatiblePaymentGatewaysNotice } from './use-incompatible-payment-gateways-notice'; import './editor.scss'; interface PaymentGatewaysNoticeProps { toggleDismissedStatus: ( status: boolean ) => void; + block: 'woocommerce/cart' | 'woocommerce/checkout'; } export function IncompatiblePaymentGatewaysNotice( { toggleDismissedStatus, + block, }: PaymentGatewaysNoticeProps ) { - // Everything below works the same for Cart/Checkout - const { incompatiblePaymentMethods } = useSelect( ( select ) => { - const { getIncompatiblePaymentMethods } = select( PAYMENT_STORE_KEY ); - return { - incompatiblePaymentMethods: getIncompatiblePaymentMethods(), - }; - }, [] ); - const [ settingStatus, setStatus ] = useState( 'pristine' ); - - const numberOfIncompatiblePaymentMethods = Object.keys( - incompatiblePaymentMethods - ).length; - const isNoticeDismissed = - numberOfIncompatiblePaymentMethods === 0 || - settingStatus === 'dismissed'; + const [ + isVisible, + dismissNotice, + incompatiblePaymentMethods, + numberOfIncompatiblePaymentMethods, + ] = useIncompatiblePaymentGatewaysNotice( block ); useEffect( () => { - toggleDismissedStatus( isNoticeDismissed ); - }, [ isNoticeDismissed, toggleDismissedStatus ] ); + toggleDismissedStatus( ! isVisible ); + }, [ isVisible, toggleDismissedStatus ] ); - if ( isNoticeDismissed ) { + if ( ! isVisible ) { return null; } @@ -68,7 +56,7 @@ export function IncompatiblePaymentGatewaysNotice( { setStatus( 'dismissed' ) } + onRemove={ dismissNotice } spokenMessage={ noticeContent } >
diff --git a/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/use-incompatible-payment-gateways-notice.ts b/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/use-incompatible-payment-gateways-notice.ts new file mode 100644 index 00000000000..71ac1bc456f --- /dev/null +++ b/plugins/woocommerce-blocks/assets/js/editor-components/incompatible-payment-gateways-notice/use-incompatible-payment-gateways-notice.ts @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import { useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; +import { useLocalStorageState } from '@woocommerce/base-hooks'; + +/** + * Internal dependencies + */ +import { STORE_KEY as PAYMENT_STORE_KEY } from '../../data/payment/constants'; + +type StoredIncompatibleGateway = { [ k: string ]: string[] }; +const initialDismissedNotices: React.SetStateAction< + StoredIncompatibleGateway[] +> = []; + +const areEqual = ( array1: string[], array2: string[] ) => { + if ( array1.length !== array2.length ) { + return false; + } + + const uniqueCollectionValues = new Set( [ ...array1, ...array2 ] ); + + return uniqueCollectionValues.size === array1.length; +}; + +export const useIncompatiblePaymentGatewaysNotice = ( + blockName: string +): [ boolean, () => void, { [ k: string ]: string }, number ] => { + const [ dismissedNotices, setDismissedNotices ] = useLocalStorageState< + StoredIncompatibleGateway[] + >( + `wc-blocks_dismissed_incompatible_payment_gateways_notices`, + initialDismissedNotices + ); + const [ isVisible, setIsVisible ] = useState( false ); + + const { incompatiblePaymentMethods } = useSelect( ( select ) => { + const { getIncompatiblePaymentMethods } = select( PAYMENT_STORE_KEY ); + return { + incompatiblePaymentMethods: getIncompatiblePaymentMethods(), + }; + }, [] ); + const incompatiblePaymentMethodsIDs = Object.keys( + incompatiblePaymentMethods + ); + const numberOfIncompatiblePaymentMethods = + incompatiblePaymentMethodsIDs.length; + + const isDismissedNoticeUpToDate = dismissedNotices.some( + ( notice ) => + Object.keys( notice ).includes( blockName ) && + areEqual( + notice[ blockName as keyof object ], + incompatiblePaymentMethodsIDs + ) + ); + + const shouldBeDismissed = + numberOfIncompatiblePaymentMethods === 0 || isDismissedNoticeUpToDate; + const dismissNotice = () => { + const dismissedNoticesSet = new Set( dismissedNotices ); + dismissedNoticesSet.add( { + [ blockName ]: incompatiblePaymentMethodsIDs, + } ); + setDismissedNotices( [ ...dismissedNoticesSet ] ); + }; + + // This ensures the modal is not loaded on first render. This is required so + // Gutenberg doesn't steal the focus from the Guide and focuses the block. + useEffect( () => { + setIsVisible( ! shouldBeDismissed ); + + if ( ! shouldBeDismissed && ! isDismissedNoticeUpToDate ) { + setDismissedNotices( ( previousDismissedNotices ) => + previousDismissedNotices.reduce( + ( acc: StoredIncompatibleGateway[], curr ) => { + if ( Object.keys( curr ).includes( blockName ) ) { + return acc; + } + acc.push( curr ); + + return acc; + }, + [] + ) + ); + } + }, [ + shouldBeDismissed, + isDismissedNoticeUpToDate, + setDismissedNotices, + blockName, + ] ); + + return [ + isVisible, + dismissNotice, + incompatiblePaymentMethods, + numberOfIncompatiblePaymentMethods, + ]; +};