diff --git a/modules/ppcp-settings/resources/js/Components/App.js b/modules/ppcp-settings/resources/js/Components/App.js index 1febe8674..6441d0a51 100644 --- a/modules/ppcp-settings/resources/js/Components/App.js +++ b/modules/ppcp-settings/resources/js/Components/App.js @@ -11,8 +11,8 @@ import { getQuery } from '../utils/navigation'; const SettingsApp = () => { const { isReady: onboardingIsReady, completed: onboardingCompleted } = OnboardingHooks.useSteps(); + const { isReady: merchantIsReady } = CommonHooks.useStore(); const { - isReady: merchantIsReady, merchant: { isSendOnlyCountry }, } = CommonHooks.useMerchantInfo(); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js index 6ea843c67..1b06eb222 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Navigation.js @@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import TopNavigation from '../../../ReusableComponents/TopNavigation'; -import { useSaveSettings } from '../../../../hooks/useSaveSettings'; +import { useStoreManager } from '../../../../hooks/useStoreManager'; import { CommonHooks } from '../../../../data'; import TabBar from '../../../ReusableComponents/TabBar'; import classNames from 'classnames'; @@ -20,7 +20,7 @@ const SettingsNavigation = ( { activePanel, setActivePanel, } ) => { - const { persistAll } = useSaveSettings(); + const { persistAll } = useStoreManager(); const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js index 089ad7150..d22b15461 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Tabs/TabOverview.js @@ -12,11 +12,12 @@ import { import { Content, ContentWrapper } from '../../../ReusableComponents/Elements'; import SettingsCard from '../../../ReusableComponents/SettingsCard'; import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge'; -import { useTodos } from '../../../../data/todos/hooks'; -import { useMerchantInfo } from '../../../../data/common/hooks'; -import { STORE_NAME as COMMON_STORE_NAME } from '../../../../data/common'; -import { STORE_NAME as TODOS_STORE_NAME } from '../../../../data/todos'; -import { CommonHooks, TodosHooks } from '../../../../data'; +import { + CommonStoreName, + TodosStoreName, + CommonHooks, + TodosHooks, +} from '../../../../data'; import { NOTIFICATION_ERROR, @@ -28,8 +29,8 @@ import { selectTab, TAB_IDS } from '../../../../utils/tabSelector'; import { setActiveModal } from '../../../../data/common/actions'; const TabOverview = () => { - const { isReady: areTodosReady } = TodosHooks.useTodos(); - const { isReady: merchantIsReady } = CommonHooks.useMerchantInfo(); + const { isReady: areTodosReady } = TodosHooks.useStore(); + const { isReady: merchantIsReady } = CommonHooks.useStore(); if ( ! areTodosReady || ! merchantIsReady ) { return ; @@ -48,12 +49,12 @@ export default TabOverview; const OverviewTodos = () => { const [ isResetting, setIsResetting ] = useState( false ); - const { todos, isReady: areTodosReady, dismissTodo } = useTodos(); - // eslint-disable-next-line no-shadow + const { todos, dismissTodo } = TodosHooks.useTodos(); + const { isReady: areTodosReady } = TodosHooks.useStore(); const { setActiveModal, setActiveHighlight } = - useDispatch( COMMON_STORE_NAME ); + useDispatch( CommonStoreName ); const { resetDismissedTodos, setDismissedTodos } = - useDispatch( TODOS_STORE_NAME ); + useDispatch( TodosStoreName ); const { createSuccessNotice } = useDispatch( noticesStore ); const showTodos = areTodosReady && todos.length > 0; @@ -120,8 +121,8 @@ const OverviewTodos = () => { const OverviewFeatures = () => { const [ isRefreshing, setIsRefreshing ] = useState( false ); - const { merchant } = useMerchantInfo(); - const { refreshFeatureStatuses } = useDispatch( COMMON_STORE_NAME ); + const { merchant } = CommonHooks.useMerchantInfo(); + const { refreshFeatureStatuses } = useDispatch( CommonStoreName ); const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); const { features, fetchFeatures } = useFeatures(); diff --git a/modules/ppcp-settings/resources/js/data/_example/actions.js b/modules/ppcp-settings/resources/js/data/_example/actions.js index f9cd9a556..51b516bb1 100644 --- a/modules/ppcp-settings/resources/js/data/_example/actions.js +++ b/modules/ppcp-settings/resources/js/data/_example/actions.js @@ -82,3 +82,16 @@ export function persist() { } ); }; } + +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} diff --git a/modules/ppcp-settings/resources/js/data/_example/hooks.js b/modules/ppcp-settings/resources/js/data/_example/hooks.js index 21253d137..4c88523b5 100644 --- a/modules/ppcp-settings/resources/js/data/_example/hooks.js +++ b/modules/ppcp-settings/resources/js/data/_example/hooks.js @@ -38,10 +38,16 @@ const useStoreData = () => { }; export const useStore = () => { - const { dispatch, useTransient } = useStoreData(); + const { select, dispatch, useTransient } = useStoreData(); + const { persist, refresh } = dispatch; const [ isReady ] = useTransient( 'isReady' ); - return { persist: dispatch.persist, isReady }; + // Load persistent data from REST if not done yet. + if ( ! isReady ) { + select.persistentData(); + } + + return { persist, refresh, isReady }; }; // TODO: Replace with real hook. diff --git a/modules/ppcp-settings/resources/js/data/common/actions-thunk.js b/modules/ppcp-settings/resources/js/data/common/actions-thunk.js index 8e7448af8..d49aa914a 100644 --- a/modules/ppcp-settings/resources/js/data/common/actions-thunk.js +++ b/modules/ppcp-settings/resources/js/data/common/actions-thunk.js @@ -27,6 +27,19 @@ export function persist() { }; } +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} + /** * Side effect. Fetches the ISU-login URL for a sandbox account. * diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js index 76a12cd2b..fd6ba3157 100644 --- a/modules/ppcp-settings/resources/js/data/common/hooks.js +++ b/modules/ppcp-settings/resources/js/data/common/hooks.js @@ -13,8 +13,32 @@ import { useCallback, useEffect, useMemo, useState } from '@wordpress/element'; import { createHooksForStore } from '../utils'; import { STORE_NAME } from './constants'; -const useHooks = () => { +/** + * Single source of truth for access Redux details. + * + * This hook returns a stable API to access actions, selectors and special hooks to generate + * getter- and setters for transient or persistent properties. + * + * @return {{select, dispatch, useTransient, usePersistent}} Store data API. + */ +const useStoreData = () => { + const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] ); + const dispatch = useDispatch( STORE_NAME ); const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + + return useMemo( + () => ( { + select, + dispatch, + useTransient, + usePersistent, + } ), + [ select, dispatch, useTransient, usePersistent ] + ); +}; + +const useHooks = () => { + const { useTransient, usePersistent, dispatch, select } = useStoreData(); const { persist, sandboxOnboardingUrl, @@ -23,10 +47,9 @@ const useHooks = () => { authenticateWithOAuth, startWebhookSimulation, checkWebhookSimulationState, - } = useDispatch( STORE_NAME ); + } = dispatch; // Transient accessors. - const [ isReady ] = useTransient( 'isReady' ); const [ activeModal, setActiveModal ] = useTransient( 'activeModal' ); const [ activeHighlight, setActiveHighlight ] = useTransient( 'activeHighlight' ); @@ -38,18 +61,9 @@ const useHooks = () => { ); // Read-only properties. - const wooSettings = useSelect( - ( select ) => select( STORE_NAME ).wooSettings(), - [] - ); - const features = useSelect( - ( select ) => select( STORE_NAME ).features(), - [] - ); - const webhooks = useSelect( - ( select ) => select( STORE_NAME ).webhooks(), - [] - ); + const wooSettings = select.wooSettings(); + const features = select.features(); + const webhooks = select.webhooks(); const savePersistent = async ( setter, value ) => { setter( value ); @@ -57,7 +71,6 @@ const useHooks = () => { }; return { - isReady, activeModal, setActiveModal, activeHighlight, @@ -82,6 +95,19 @@ const useHooks = () => { }; }; +export const useStore = () => { + const { select, dispatch, useTransient } = useStoreData(); + const { persist, refresh } = dispatch; + const [ isReady ] = useTransient( 'isReady' ); + + // Load persistent data from REST if not done yet. + if ( ! isReady ) { + select.persistentData(); + } + + return { persist, refresh, isReady }; +}; + export const useSandbox = () => { const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks(); @@ -139,7 +165,7 @@ export const useWebhooks = () => { }; export const useMerchantInfo = () => { - const { isReady, features } = useHooks(); + const { features } = useHooks(); const merchant = useMerchant(); const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME ); @@ -164,7 +190,6 @@ export const useMerchantInfo = () => { }, [ refreshMerchantData, setMerchant ] ); return { - isReady, merchant, // Merchant details features, // Eligible merchant features verifyLoginStatus, // Callback @@ -204,7 +229,9 @@ export const useActiveHighlight = () => { return { activeHighlight, setActiveHighlight }; }; -// -- Not using the `useHooks()` data provider -- +/* + * Busy state management hooks + */ export const useBusyState = () => { const { startActivity, stopActivity } = useDispatch( STORE_NAME ); diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js index f31a647d3..5bcd76ab2 100644 --- a/modules/ppcp-settings/resources/js/data/debug.js +++ b/modules/ppcp-settings/resources/js/data/debug.js @@ -5,6 +5,7 @@ import { SettingsStoreName, StylingStoreName, TodosStoreName, + FeaturesStoreName, } from './index'; export const addDebugTools = ( context, modules ) => { @@ -18,10 +19,15 @@ export const addDebugTools = ( context, modules ) => { if ( ! context.debug ) { return } */ + const describe = ( fnName, fnInfo ) => { + // eslint-disable-next-line no-console + console.log( `\n%c${ fnName }:`, 'font-weight:bold', fnInfo, '\n\n' ); + }; + const debugApi = ( window.ppcpDebugger = window.ppcpDebugger || {} ); // Dump the current state of all our Redux stores. - debugApi.dumpStore = async () => { + debugApi.dumpStore = async ( cbFilter = null ) => { /* eslint-disable no-console */ if ( ! console?.groupCollapsed ) { console.error( 'console.groupCollapsed is not supported.' ); @@ -34,11 +40,19 @@ export const addDebugTools = ( context, modules ) => { console.group( `[STORE] ${ storeSelector }` ); const dumpStore = ( selector ) => { - const contents = wp.data.select( storeName )[ selector ](); + let contents = wp.data.select( storeName )[ selector ](); - console.groupCollapsed( `.${ selector }()` ); - console.table( contents ); - console.groupEnd(); + if ( cbFilter ) { + contents = cbFilter( contents, selector, storeName ); + + if ( undefined !== contents && null !== contents ) { + console.log( `.${ selector }() [filtered]`, contents ); + } + } else { + console.groupCollapsed( `.${ selector }()` ); + console.table( contents ); + console.groupEnd(); + } }; Object.keys( module.selectors ).forEach( dumpStore ); @@ -51,46 +65,91 @@ export const addDebugTools = ( context, modules ) => { // Reset all Redux stores to their initial state. debugApi.resetStore = () => { const stores = []; - const { isConnected } = wp.data.select( CommonStoreName ).merchant(); - if ( isConnected ) { - // Make sure the Onboarding wizard is "completed". - const onboarding = wp.data.dispatch( OnboardingStoreName ); - onboarding.setPersistent( 'completed', true ); - onboarding.persist(); + describe( + 'resetStore', + 'Reset all Redux stores to their DEFAULT state, without changing any server-side data. The default state is defined in the JS code.' + ); - // Reset all stores, except for the onboarding store. - stores.push( CommonStoreName ); - stores.push( PaymentStoreName ); - stores.push( SettingsStoreName ); - stores.push( StylingStoreName ); - stores.push( TodosStoreName ); - stores.push( FeaturesStoreName ); - } else { - // Only reset the common & onboarding stores to restart the onboarding wizard. - stores.push( CommonStoreName ); + const { completed } = wp.data + .select( OnboardingStoreName ) + .persistentData(); + + // Reset all stores, except for the onboarding store. + stores.push( CommonStoreName ); + stores.push( PaymentStoreName ); + stores.push( SettingsStoreName ); + stores.push( StylingStoreName ); + stores.push( TodosStoreName ); + stores.push( FeaturesStoreName ); + + // Only reset the onboarding store when the wizard is not completed. + if ( ! completed ) { stores.push( OnboardingStoreName ); } stores.forEach( ( storeName ) => { const store = wp.data.dispatch( storeName ); - // eslint-disable-next-line no-console - console.log( `Reset store: ${ storeName }...` ); - try { store.reset(); - store.persist(); + + // eslint-disable-next-line no-console + console.log( `Done: Store '${ storeName }' reset` ); } catch ( error ) { - console.error( ' ... Reset failed, skipping this store' ); + console.error( + `Failed: Could not reset store '${ storeName }'` + ); } } ); + + // eslint-disable-next-line no-console + console.log( '---- Complete ----\n\n' ); + }; + + debugApi.refreshStore = () => { + const stores = []; + + describe( + 'refreshStore', + 'Refreshes all Redux details with details provided by the server. This has a similar effect as reloading the page without saving' + ); + + stores.push( CommonStoreName ); + stores.push( PaymentStoreName ); + stores.push( SettingsStoreName ); + stores.push( StylingStoreName ); + stores.push( TodosStoreName ); + stores.push( FeaturesStoreName ); + stores.push( OnboardingStoreName ); + + stores.forEach( ( storeName ) => { + const store = wp.data.dispatch( storeName ); + + try { + store.refresh(); + + // eslint-disable-next-line no-console + console.log( + `Done: Store '${ storeName }' refreshed from REST` + ); + } catch ( error ) { + console.error( + `Failed: Could not refresh store '${ storeName }' from REST` + ); + } + } ); + + // eslint-disable-next-line no-console + console.log( '---- Complete ----\n\n' ); }; // Disconnect the merchant and display the onboarding wizard. debugApi.disconnect = () => { const common = wp.data.dispatch( CommonStoreName ); + describe(); + common.disconnectMerchant(); // eslint-disable-next-line no-console @@ -103,6 +162,11 @@ export const addDebugTools = ( context, modules ) => { debugApi.onboardingMode = ( state ) => { const onboarding = wp.data.dispatch( OnboardingStoreName ); + describe( + 'onboardingMode', + 'Toggle between onboarding wizard and the settings screen.' + ); + onboarding.setPersistent( 'completed', ! state ); onboarding.persist(); }; diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js index 0d2617095..70967168d 100644 --- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js +++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js @@ -87,3 +87,16 @@ export function persist() { } }; } + +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} diff --git a/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js b/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js index f9cd9a556..51b516bb1 100644 --- a/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js +++ b/modules/ppcp-settings/resources/js/data/pay-later-messaging/actions.js @@ -82,3 +82,16 @@ export function persist() { } ); }; } + +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} diff --git a/modules/ppcp-settings/resources/js/data/payment/actions.js b/modules/ppcp-settings/resources/js/data/payment/actions.js index 1107d5bfb..dfa100f76 100644 --- a/modules/ppcp-settings/resources/js/data/payment/actions.js +++ b/modules/ppcp-settings/resources/js/data/payment/actions.js @@ -94,3 +94,16 @@ export function persist() { } ); }; } + +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} diff --git a/modules/ppcp-settings/resources/js/data/payment/hooks.js b/modules/ppcp-settings/resources/js/data/payment/hooks.js index 253710580..6060041f4 100644 --- a/modules/ppcp-settings/resources/js/data/payment/hooks.js +++ b/modules/ppcp-settings/resources/js/data/payment/hooks.js @@ -7,22 +7,58 @@ * @file */ -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { STORE_NAME } from './constants'; import { createHooksForStore } from '../utils'; +import { useMemo } from '@wordpress/element'; -const useHooks = () => { +/** + * Single source of truth for access Redux details. + * + * This hook returns a stable API to access actions, selectors and special hooks to generate + * getter- and setters for transient or persistent properties. + * + * @return {{select, dispatch, useTransient, usePersistent}} Store data API. + */ +const useStoreData = () => { + const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] ); + const dispatch = useDispatch( STORE_NAME ); const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); - const { persist, setPersistent, changePaymentSettings } = - useDispatch( STORE_NAME ); - // Read-only flags and derived state. - // Nothing here yet. + return useMemo( + () => ( { + select, + dispatch, + useTransient, + usePersistent, + } ), + [ select, dispatch, useTransient, usePersistent ] + ); +}; - // Transient accessors. +export const useStore = () => { + const { select, useTransient, dispatch } = useStoreData(); + const { persist, refresh, setPersistent, changePaymentSettings } = dispatch; const [ isReady ] = useTransient( 'isReady' ); + // Load persistent data from REST if not done yet. + if ( ! isReady ) { + select.persistentData(); + } + + return { + persist, + refresh, + setPersistent, + changePaymentSettings, + isReady, + }; +}; + +export const usePaymentMethods = () => { + const { usePersistent } = useStoreData(); + // PayPal checkout. const [ paypal ] = usePersistent( 'ppcp-gateway' ); const [ venmo ] = usePersistent( 'venmo' ); @@ -47,79 +83,6 @@ const useHooks = () => { const [ pui ] = usePersistent( 'ppcp-pay-upon-invoice-gateway' ); const [ oxxo ] = usePersistent( 'ppcp-oxxo-gateway' ); - // Custom modal data. - const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' ); - const [ threeDSecure ] = usePersistent( 'threeDSecure' ); - const [ fastlaneCardholderName ] = usePersistent( - 'fastlaneCardholderName' - ); - const [ fastlaneDisplayWatermark ] = usePersistent( - 'fastlaneDisplayWatermark' - ); - - return { - persist, - isReady, - setPersistent, - changePaymentSettings, - paypal, - venmo, - payLater, - creditCard, - advancedCreditCard, - fastlane, - applePay, - googlePay, - bancontact, - blik, - eps, - ideal, - mybank, - p24, - trustly, - multibanco, - pui, - oxxo, - paypalShowLogo, - threeDSecure, - fastlaneCardholderName, - fastlaneDisplayWatermark, - }; -}; - -export const useStore = () => { - const { persist, isReady, setPersistent, changePaymentSettings } = - useHooks(); - return { persist, isReady, setPersistent, changePaymentSettings }; -}; - -export const usePaymentMethods = () => { - const { - // PayPal Checkout. - paypal, - venmo, - payLater, - creditCard, - - // Online card payments. - advancedCreditCard, - fastlane, - applePay, - googlePay, - - // Local APMs. - bancontact, - blik, - eps, - ideal, - mybank, - p24, - trustly, - multibanco, - pui, - oxxo, - } = useHooks(); - const payPalCheckout = [ paypal, venmo, payLater, creditCard ]; const onlineCardPayments = [ advancedCreditCard, @@ -169,12 +132,16 @@ export const usePaymentMethods = () => { }; export const usePaymentMethodsModal = () => { - const { - paypalShowLogo, - threeDSecure, - fastlaneCardholderName, - fastlaneDisplayWatermark, - } = useHooks(); + const { usePersistent } = useStoreData(); + + const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' ); + const [ threeDSecure ] = usePersistent( 'threeDSecure' ); + const [ fastlaneCardholderName ] = usePersistent( + 'fastlaneCardholderName' + ); + const [ fastlaneDisplayWatermark ] = usePersistent( + 'fastlaneDisplayWatermark' + ); return { paypalShowLogo, diff --git a/modules/ppcp-settings/resources/js/data/payment/reducer.js b/modules/ppcp-settings/resources/js/data/payment/reducer.js index d46106f6b..4da85fa4e 100644 --- a/modules/ppcp-settings/resources/js/data/payment/reducer.js +++ b/modules/ppcp-settings/resources/js/data/payment/reducer.js @@ -19,6 +19,7 @@ const defaultTransient = Object.freeze( { // Persistent: Values that are loaded from the DB. const defaultPersistent = Object.freeze( { + // Payment methods. 'ppcp-gateway': {}, venmo: {}, 'pay-later': {}, @@ -37,6 +38,8 @@ const defaultPersistent = Object.freeze( { 'ppcp-multibanco': {}, 'ppcp-pay-upon-invoice-gateway': {}, 'ppcp-oxxo-gateway': {}, + + // Custom payment method properties. paypalShowLogo: false, threeDSecure: 'no-3d-secure', fastlaneCardholderName: false, diff --git a/modules/ppcp-settings/resources/js/data/settings/actions.js b/modules/ppcp-settings/resources/js/data/settings/actions.js index 6633516bb..f99adae5a 100644 --- a/modules/ppcp-settings/resources/js/data/settings/actions.js +++ b/modules/ppcp-settings/resources/js/data/settings/actions.js @@ -84,3 +84,16 @@ export function persist() { } ); }; } + +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} diff --git a/modules/ppcp-settings/resources/js/data/settings/hooks.js b/modules/ppcp-settings/resources/js/data/settings/hooks.js index f161ee48e..4a97afa9e 100644 --- a/modules/ppcp-settings/resources/js/data/settings/hooks.js +++ b/modules/ppcp-settings/resources/js/data/settings/hooks.js @@ -6,17 +6,38 @@ * * @file */ -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { STORE_NAME } from './constants'; import { createHooksForStore } from '../utils'; +import { useMemo } from '@wordpress/element'; + +/** + * Single source of truth for access Redux details. + * + * This hook returns a stable API to access actions, selectors and special hooks to generate + * getter- and setters for transient or persistent properties. + * + * @return {{select, dispatch, useTransient, usePersistent}} Store data API. + */ +const useStoreData = () => { + const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] ); + const dispatch = useDispatch( STORE_NAME ); + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + + return useMemo( + () => ( { + select, + dispatch, + useTransient, + usePersistent, + } ), + [ select, dispatch, useTransient, usePersistent ] + ); +}; const useHooks = () => { - const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); - const { persist } = useDispatch( STORE_NAME ); - - // Read-only flags and derived state. - const [ isReady ] = useTransient( 'isReady' ); + const { usePersistent } = useStoreData(); // Persistent accessors. const [ invoicePrefix, setInvoicePrefix ] = @@ -47,8 +68,6 @@ const useHooks = () => { usePersistent( 'disabledCards' ); return { - persist, - isReady, invoicePrefix, setInvoicePrefix, authorizeOnly, @@ -79,8 +98,16 @@ const useHooks = () => { }; export const useStore = () => { - const { persist, isReady } = useHooks(); - return { persist, isReady }; + const { select, dispatch, useTransient } = useStoreData(); + const { persist, refresh } = dispatch; + const [ isReady ] = useTransient( 'isReady' ); + + // Load persistent data from REST if not done yet. + if ( ! isReady ) { + select.persistentData(); + } + + return { persist, refresh, isReady }; }; export const useSettings = () => { diff --git a/modules/ppcp-settings/resources/js/data/styling/actions.js b/modules/ppcp-settings/resources/js/data/styling/actions.js index 9e1639c7f..72d4d9ea7 100644 --- a/modules/ppcp-settings/resources/js/data/styling/actions.js +++ b/modules/ppcp-settings/resources/js/data/styling/actions.js @@ -82,3 +82,16 @@ export function persist() { } ); }; } + +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} diff --git a/modules/ppcp-settings/resources/js/data/styling/hooks.js b/modules/ppcp-settings/resources/js/data/styling/hooks.js index 8227d0124..ecf999eaf 100644 --- a/modules/ppcp-settings/resources/js/data/styling/hooks.js +++ b/modules/ppcp-settings/resources/js/data/styling/hooks.js @@ -7,7 +7,7 @@ * @file */ -import { useCallback } from '@wordpress/element'; +import { useCallback, useMemo } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { createHooksForStore } from '../utils'; @@ -20,13 +20,37 @@ import { STYLING_PAYMENT_METHODS, STYLING_SHAPES, } from './configuration'; +import { persistentData } from './selectors'; + +/** + * Single source of truth for access Redux details. + * + * This hook returns a stable API to access actions, selectors and special hooks to generate + * getter- and setters for transient or persistent properties. + * + * @return {{select, dispatch, useTransient, usePersistent}} Store data API. + */ +const useStoreData = () => { + const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] ); + const dispatch = useDispatch( STORE_NAME ); + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + + return useMemo( + () => ( { + select, + dispatch, + useTransient, + usePersistent, + } ), + [ select, dispatch, useTransient, usePersistent ] + ); +}; const useHooks = () => { - const { useTransient } = createHooksForStore( STORE_NAME ); - const { persist, setPersistent } = useDispatch( STORE_NAME ); + const { useTransient, dispatch } = useStoreData(); + const { setPersistent } = dispatch; // Transient accessors. - const [ isReady ] = useTransient( 'isReady' ); const [ location, setLocation ] = useTransient( 'location' ); // Persistent accessors. @@ -61,8 +85,6 @@ const useHooks = () => { ); return { - persist, - isReady, location, setLocation, getLocationProp, @@ -71,8 +93,16 @@ const useHooks = () => { }; export const useStore = () => { - const { persist, isReady } = useHooks(); - return { persist, isReady }; + const { select, dispatch, useTransient } = useStoreData(); + const { persist, refresh } = dispatch; + const [ isReady ] = useTransient( 'isReady' ); + + // Load persistent data from REST if not done yet. + if ( ! isReady ) { + select.persistentData(); + } + + return { persist, refresh, isReady }; }; export const useStylingLocation = () => { diff --git a/modules/ppcp-settings/resources/js/data/todos/action-types.js b/modules/ppcp-settings/resources/js/data/todos/action-types.js index 66d92218a..17bbe8a0f 100644 --- a/modules/ppcp-settings/resources/js/data/todos/action-types.js +++ b/modules/ppcp-settings/resources/js/data/todos/action-types.js @@ -5,6 +5,12 @@ */ export default { + /** + * Resets the store state to its initial values. + * Used when needing to clear all store data. + */ + RESET: 'ppcp/todos/RESET', + // Transient data SET_TRANSIENT: 'ppcp/todos/SET_TRANSIENT', SET_COMPLETED_TODOS: 'ppcp/todos/SET_COMPLETED_TODOS', diff --git a/modules/ppcp-settings/resources/js/data/todos/actions.js b/modules/ppcp-settings/resources/js/data/todos/actions.js index 41ac372cd..25b6d7647 100644 --- a/modules/ppcp-settings/resources/js/data/todos/actions.js +++ b/modules/ppcp-settings/resources/js/data/todos/actions.js @@ -17,11 +17,47 @@ import { REST_RESET_DISMISSED_TODOS_PATH, } from './constants'; -export const setIsReady = ( isReady ) => ( { - type: ACTION_TYPES.SET_TRANSIENT, - payload: { isReady }, +/** + * Special. Resets all values in the store to initial defaults. + * + * @return {Object} The action. + */ +export const reset = () => ( { + type: ACTION_TYPES.RESET, } ); +/** + * Generic transient-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Object} The action. + */ +export const setTransient = ( prop, value ) => ( { + type: ACTION_TYPES.SET_TRANSIENT, + payload: { [ prop ]: value }, +} ); + +/** + * Generic persistent-data updater. + * + * @param {string} prop Name of the property to update. + * @param {any} value The new value of the property. + * @return {Object} The action. + */ +export const setPersistent = ( prop, value ) => ( { + type: ACTION_TYPES.SET_PERSISTENT, + payload: { [ prop ]: value }, +} ); + +/** + * Transient. Marks the store as "ready", i.e., fully initialized. + * + * @param {boolean} isReady Whether the store is ready + * @return {Object} The action. + */ +export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady ); + export const setTodos = ( todos ) => ( { type: ACTION_TYPES.SET_TODOS, payload: todos, @@ -39,6 +75,7 @@ export const setCompletedTodos = ( completedTodos ) => ( { // Thunks +// TODO: Possibly, this should be a resolver? export function fetchTodos() { return async () => { const response = await apiFetch( { path: REST_PATH } ); @@ -46,9 +83,14 @@ export function fetchTodos() { }; } +/** + * Thunk action creator. Triggers the persistence of store data to the server. + * + * @return {Function} The thunk function. + */ export function persist() { return async ( { select } ) => { - return await apiFetch( { + await apiFetch( { path: REST_PERSIST_PATH, method: 'POST', data: select.persistentData(), @@ -56,6 +98,19 @@ export function persist() { }; } +/** + * Thunk action creator. Forces a data refresh from the REST API, replacing the current Redux values. + * + * @return {Function} The thunk function. + */ +export function refresh() { + return ( { dispatch, select } ) => { + dispatch.invalidateResolutionForStore(); + + select.persistentData(); + }; +} + export function resetDismissedTodos() { return async ( { dispatch } ) => { try { diff --git a/modules/ppcp-settings/resources/js/data/todos/hooks.js b/modules/ppcp-settings/resources/js/data/todos/hooks.js index 7fc5da5c1..5ce97a636 100644 --- a/modules/ppcp-settings/resources/js/data/todos/hooks.js +++ b/modules/ppcp-settings/resources/js/data/todos/hooks.js @@ -10,31 +10,40 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { STORE_NAME } from './constants'; import { createHooksForStore } from '../utils'; +import { useMemo } from '@wordpress/element'; -const ensureArray = ( value ) => { - if ( ! value ) { - return []; - } - return Array.isArray( value ) ? value : Object.values( value ); +/** + * Single source of truth for access Redux details. + * + * This hook returns a stable API to access actions, selectors and special hooks to generate + * getter- and setters for transient or persistent properties. + * + * @return {{select, dispatch, useTransient, usePersistent}} Store data API. + */ +const useStoreData = () => { + const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] ); + const dispatch = useDispatch( STORE_NAME ); + const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); + + return useMemo( + () => ( { + select, + dispatch, + useTransient, + usePersistent, + } ), + [ select, dispatch, useTransient, usePersistent ] + ); }; const useHooks = () => { - const { useTransient } = createHooksForStore( STORE_NAME ); - const { fetchTodos, setDismissedTodos, setCompletedTodos, persist } = - useDispatch( STORE_NAME ); - - // Read-only flags and derived state. - const [ isReady ] = useTransient( 'isReady' ); + const { dispatch, select } = useStoreData(); + const { fetchTodos, setDismissedTodos, setCompletedTodos } = dispatch; // Get todos data from store - const { todos, dismissedTodos, completedTodos } = useSelect( ( select ) => { - const store = select( STORE_NAME ); - return { - todos: ensureArray( store.getTodos() ), - dismissedTodos: ensureArray( store.getDismissedTodos() ), - completedTodos: ensureArray( store.getCompletedTodos() ), - }; - }, [] ); + const todos = select.getTodos(); + const dismissedTodos = select.getDismissedTodos(); + const completedTodos = select.getCompletedTodos(); const dismissedSet = new Set( dismissedTodos ); @@ -62,8 +71,6 @@ const useHooks = () => { ); return { - persist, - isReady, todos: filteredTodos, dismissedTodos, completedTodos, @@ -74,14 +81,21 @@ const useHooks = () => { }; export const useStore = () => { - const { persist, isReady } = useHooks(); - return { persist, isReady }; + const { select, dispatch, useTransient } = useStoreData(); + const { persist, refresh } = dispatch; + const [ isReady ] = useTransient( 'isReady' ); + + // Load persistent data from REST if not done yet. + if ( ! isReady ) { + select.getTodos(); + } + + return { persist, refresh, isReady }; }; export const useTodos = () => { - const { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady } = - useHooks(); - return { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady }; + const { todos, fetchTodos, dismissTodo, setTodoCompleted } = useHooks(); + return { todos, fetchTodos, dismissTodo, setTodoCompleted }; }; export const useDismissedTodos = () => { diff --git a/modules/ppcp-settings/resources/js/data/todos/reducer.js b/modules/ppcp-settings/resources/js/data/todos/reducer.js index 6f4c74d46..f3cc412ac 100644 --- a/modules/ppcp-settings/resources/js/data/todos/reducer.js +++ b/modules/ppcp-settings/resources/js/data/todos/reducer.js @@ -52,6 +52,21 @@ const reducer = createReducer( defaultTransient, defaultPersistent, { [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => changeTransient( state, payload ), + /** + * Resets state to defaults while maintaining initialization status + * + * @param {Object} state Current state + * @return {Object} Reset state + */ + [ ACTION_TYPES.RESET ]: ( state ) => { + const cleanState = changeTransient( + changePersistent( state, defaultPersistent ), + defaultTransient + ); + cleanState.isReady = true; // Keep initialization flag + return cleanState; + }, + /** * Updates todos list * @@ -99,6 +114,7 @@ const reducer = createReducer( defaultTransient, defaultPersistent, { }, /** + * TODO: This is not used anywhere. Remove "SET_TODOS" and use this resolver instead. * Initializes persistent state with data from the server * * @param {Object} state Current state diff --git a/modules/ppcp-settings/resources/js/data/todos/selectors.js b/modules/ppcp-settings/resources/js/data/todos/selectors.js index 2f42c6ffc..1066ba3a8 100644 --- a/modules/ppcp-settings/resources/js/data/todos/selectors.js +++ b/modules/ppcp-settings/resources/js/data/todos/selectors.js @@ -11,7 +11,17 @@ const EMPTY_OBJ = Object.freeze( {} ); const EMPTY_ARR = Object.freeze( [] ); const getState = ( state ) => state || EMPTY_OBJ; +const getArray = ( value ) => { + if ( Array.isArray( value ) ) { + return value; + } + if ( value ) { + return Object.values( value ); + } + return EMPTY_ARR; +}; +// TODO: Implement a persistentData resolver! export const persistentData = ( state ) => { return getState( state ).data || EMPTY_OBJ; }; @@ -23,15 +33,15 @@ export const transientData = ( state ) => { export const getTodos = ( state ) => { const todos = state?.todos || persistentData( state ).todos; - return todos || EMPTY_ARR; + return getArray( todos ); }; export const getDismissedTodos = ( state ) => { const dismissed = state?.dismissedTodos || persistentData( state ).dismissedTodos; - return dismissed || EMPTY_ARR; + return getArray( dismissed ); }; export const getCompletedTodos = ( state ) => { - return state?.completedTodos || EMPTY_ARR; // Only look at root state, not persistent data + return getArray( state?.completedTodos ); // Only look at root state, not persistent data }; diff --git a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js index e605b61ef..e98345149 100644 --- a/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js +++ b/modules/ppcp-settings/resources/js/hooks/useHandleConnections.js @@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { CommonHooks, OnboardingHooks } from '../data'; +import { useStoreManager } from './useStoreManager'; const PAYPAL_PARTNER_SDK_URL = 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js'; @@ -30,7 +31,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { const { sandboxOnboardingUrl } = CommonHooks.useSandbox(); const { productionOnboardingUrl } = CommonHooks.useProduction(); const products = OnboardingHooks.useDetermineProducts(); - const { withActivity, startActivity } = CommonHooks.useBusyState(); + const { startActivity } = CommonHooks.useBusyState(); const { authenticateWithOAuth } = CommonHooks.useAuthentication(); const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ scriptLoaded, setScriptLoaded ] = useState( false ); @@ -134,7 +135,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => { // Ensure the onComplete handler is not removed by a PayPal init script. timerRef.current = setInterval( addHandler, 250 ); }, - [ authenticateWithOAuth, withActivity ] + [ authenticateWithOAuth, startActivity ] ); const removeCompleteHandler = useCallback( () => { @@ -161,6 +162,7 @@ const useConnectionBase = () => { useDispatch( noticesStore ); const { verifyLoginStatus } = CommonHooks.useMerchantInfo(); const { withActivity } = CommonHooks.useBusyState(); + const { refreshAll } = useStoreManager(); return { handleFailed: ( res, genericMessage ) => { @@ -178,6 +180,7 @@ const useConnectionBase = () => { if ( loginSuccessful ) { createSuccessNotice( MESSAGES.CONNECTED ); await setCompleted( true ); + refreshAll(); } else { createErrorNotice( MESSAGES.LOGIN_FAILED ); } diff --git a/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js b/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js deleted file mode 100644 index 7237aa4df..000000000 --- a/modules/ppcp-settings/resources/js/hooks/useSaveSettings.js +++ /dev/null @@ -1,74 +0,0 @@ -import { useCallback, useMemo } from '@wordpress/element'; - -import { - CommonHooks, - PayLaterMessagingHooks, - PaymentHooks, - SettingsHooks, - StylingHooks, - TodosHooks, -} from '../data'; - -export const useSaveSettings = () => { - const { withActivity } = CommonHooks.useBusyState(); - - const { persist: persistPayment } = PaymentHooks.useStore(); - const { persist: persistSettings } = SettingsHooks.useStore(); - const { persist: persistStyling } = StylingHooks.useStore(); - const { persist: persistTodos } = TodosHooks.useStore(); - const { persist: persistPayLaterMessaging } = - PayLaterMessagingHooks.useStore(); - - const persistActions = useMemo( - () => [ - { - key: 'persist-methods', - message: 'Save payment methods', - action: persistPayment, - }, - { - key: 'persist-settings', - message: 'Save the settings', - action: persistSettings, - }, - { - key: 'persist-styling', - message: 'Save styling details', - action: persistStyling, - }, - { - key: 'persist-todos', - message: 'Save todos state', - action: persistTodos, - }, - { - key: 'persist-pay-later-messaging', - message: 'Save pay later messaging details', - action: persistPayLaterMessaging, - }, - ], - [ - persistPayLaterMessaging, - persistPayment, - persistSettings, - persistStyling, - persistTodos, - ] - ); - - const persistAll = useCallback( () => { - /** - * Executes onSave on TabPayLaterMessaging component. - * - * Todo: find a better way for this, because it's highly unreliable - * (it only works when the user is still on the "Pay Later Messaging" tab) - */ - document.getElementById( 'configurator-publishButton' )?.click(); - - persistActions.forEach( ( { key, message, action } ) => { - withActivity( key, message, action ); - } ); - }, [ persistActions, withActivity ] ); - - return { persistAll }; -}; diff --git a/modules/ppcp-settings/resources/js/hooks/useStoreManager.js b/modules/ppcp-settings/resources/js/hooks/useStoreManager.js new file mode 100644 index 000000000..190b2ac88 --- /dev/null +++ b/modules/ppcp-settings/resources/js/hooks/useStoreManager.js @@ -0,0 +1,76 @@ +import { useCallback, useMemo } from '@wordpress/element'; + +import { + CommonHooks, + PayLaterMessagingHooks, + PaymentHooks, + SettingsHooks, + StylingHooks, + TodosHooks, +} from '../data'; + +export const useStoreManager = () => { + const { withActivity } = CommonHooks.useBusyState(); + + const paymentStore = PaymentHooks.useStore(); + const settingsStore = SettingsHooks.useStore(); + const stylingStore = StylingHooks.useStore(); + const todosStore = TodosHooks.useStore(); + const payLaterStore = PayLaterMessagingHooks.useStore(); + + const storeActions = useMemo( + () => [ + { + key: 'methods', + message: 'Process payment methods', + store: paymentStore, + }, + { + key: 'settings', + message: 'Process the settings', + store: settingsStore, + }, + { + key: 'styling', + message: 'Process styling details', + store: stylingStore, + }, + { + key: 'todos', + message: 'Process todos state', + store: todosStore, + }, + { + key: 'pay-later-messaging', + message: 'Process pay later messaging details', + store: payLaterStore, + }, + ], + [ payLaterStore, paymentStore, settingsStore, stylingStore, todosStore ] + ); + + const persistAll = useCallback( () => { + /** + * Executes onSave on TabPayLaterMessaging component. + * + * Todo: find a better way for this, because it's highly unreliable + * (it only works when the user is still on the "Pay Later Messaging" tab) + */ + document.getElementById( 'configurator-publishButton' )?.click(); + + storeActions.forEach( ( { key, message, store } ) => { + withActivity( `persist-${ key }`, message, store.persist ); + } ); + }, [ storeActions, withActivity ] ); + + const refreshAll = useCallback( () => { + storeActions.forEach( ( { key, message, store } ) => { + withActivity( `refresh-${ key }`, message, store.refresh ); + } ); + }, [ storeActions, withActivity ] ); + + return { + persistAll, + refreshAll, + }; +}; diff --git a/modules/ppcp-settings/src/Service/SettingsDataManager.php b/modules/ppcp-settings/src/Service/SettingsDataManager.php index 2feefc4ee..4af72b7bd 100644 --- a/modules/ppcp-settings/src/Service/SettingsDataManager.php +++ b/modules/ppcp-settings/src/Service/SettingsDataManager.php @@ -211,7 +211,8 @@ class SettingsDataManager { $this->payment_methods->toggle_method_state( $method['id'], false ); } - // Always enable Venmo and Pay Later. + // Always enable PayPal, Venmo and Pay Later. + $this->payment_methods->toggle_method_state( PayPalGateway::ID, true ); $this->payment_methods->toggle_method_state( 'venmo', true ); $this->payment_methods->toggle_method_state( 'pay-later', true );