Merge remote-tracking branch 'origin/PCP-4210-features-refactor-to-use-rest-endpoints' into PCP-4210-features-refactor-to-use-rest-endpoints

# Conflicts:
#	modules/ppcp-settings/resources/js/data/debug.js
This commit is contained in:
carmenmaymo 2025-02-17 15:38:13 +01:00
commit c902f24010
No known key found for this signature in database
GPG key ID: 6023F686B0F3102E
26 changed files with 599 additions and 276 deletions

View file

@ -11,8 +11,8 @@ import { getQuery } from '../utils/navigation';
const SettingsApp = () => { const SettingsApp = () => {
const { isReady: onboardingIsReady, completed: onboardingCompleted } = const { isReady: onboardingIsReady, completed: onboardingCompleted } =
OnboardingHooks.useSteps(); OnboardingHooks.useSteps();
const { isReady: merchantIsReady } = CommonHooks.useStore();
const { const {
isReady: merchantIsReady,
merchant: { isSendOnlyCountry }, merchant: { isSendOnlyCountry },
} = CommonHooks.useMerchantInfo(); } = CommonHooks.useMerchantInfo();

View file

@ -3,7 +3,7 @@ import { __ } from '@wordpress/i18n';
import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; import { useCallback, useEffect, useRef, useState } from '@wordpress/element';
import TopNavigation from '../../../ReusableComponents/TopNavigation'; import TopNavigation from '../../../ReusableComponents/TopNavigation';
import { useSaveSettings } from '../../../../hooks/useSaveSettings'; import { useStoreManager } from '../../../../hooks/useStoreManager';
import { CommonHooks } from '../../../../data'; import { CommonHooks } from '../../../../data';
import TabBar from '../../../ReusableComponents/TabBar'; import TabBar from '../../../ReusableComponents/TabBar';
import classNames from 'classnames'; import classNames from 'classnames';
@ -20,7 +20,7 @@ const SettingsNavigation = ( {
activePanel, activePanel,
setActivePanel, setActivePanel,
} ) => { } ) => {
const { persistAll } = useSaveSettings(); const { persistAll } = useStoreManager();
const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' ); const title = __( 'PayPal Payments', 'woocommerce-paypal-payments' );

View file

@ -12,11 +12,12 @@ import {
import { Content, ContentWrapper } from '../../../ReusableComponents/Elements'; import { Content, ContentWrapper } from '../../../ReusableComponents/Elements';
import SettingsCard from '../../../ReusableComponents/SettingsCard'; import SettingsCard from '../../../ReusableComponents/SettingsCard';
import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge'; import { TITLE_BADGE_POSITIVE } from '../../../ReusableComponents/TitleBadge';
import { useTodos } from '../../../../data/todos/hooks'; import {
import { useMerchantInfo } from '../../../../data/common/hooks'; CommonStoreName,
import { STORE_NAME as COMMON_STORE_NAME } from '../../../../data/common'; TodosStoreName,
import { STORE_NAME as TODOS_STORE_NAME } from '../../../../data/todos'; CommonHooks,
import { CommonHooks, TodosHooks } from '../../../../data'; TodosHooks,
} from '../../../../data';
import { import {
NOTIFICATION_ERROR, NOTIFICATION_ERROR,
@ -28,8 +29,8 @@ import { selectTab, TAB_IDS } from '../../../../utils/tabSelector';
import { setActiveModal } from '../../../../data/common/actions'; import { setActiveModal } from '../../../../data/common/actions';
const TabOverview = () => { const TabOverview = () => {
const { isReady: areTodosReady } = TodosHooks.useTodos(); const { isReady: areTodosReady } = TodosHooks.useStore();
const { isReady: merchantIsReady } = CommonHooks.useMerchantInfo(); const { isReady: merchantIsReady } = CommonHooks.useStore();
if ( ! areTodosReady || ! merchantIsReady ) { if ( ! areTodosReady || ! merchantIsReady ) {
return <SpinnerOverlay asModal={ true } />; return <SpinnerOverlay asModal={ true } />;
@ -48,12 +49,12 @@ export default TabOverview;
const OverviewTodos = () => { const OverviewTodos = () => {
const [ isResetting, setIsResetting ] = useState( false ); const [ isResetting, setIsResetting ] = useState( false );
const { todos, isReady: areTodosReady, dismissTodo } = useTodos(); const { todos, dismissTodo } = TodosHooks.useTodos();
// eslint-disable-next-line no-shadow const { isReady: areTodosReady } = TodosHooks.useStore();
const { setActiveModal, setActiveHighlight } = const { setActiveModal, setActiveHighlight } =
useDispatch( COMMON_STORE_NAME ); useDispatch( CommonStoreName );
const { resetDismissedTodos, setDismissedTodos } = const { resetDismissedTodos, setDismissedTodos } =
useDispatch( TODOS_STORE_NAME ); useDispatch( TodosStoreName );
const { createSuccessNotice } = useDispatch( noticesStore ); const { createSuccessNotice } = useDispatch( noticesStore );
const showTodos = areTodosReady && todos.length > 0; const showTodos = areTodosReady && todos.length > 0;
@ -120,8 +121,8 @@ const OverviewTodos = () => {
const OverviewFeatures = () => { const OverviewFeatures = () => {
const [ isRefreshing, setIsRefreshing ] = useState( false ); const [ isRefreshing, setIsRefreshing ] = useState( false );
const { merchant } = useMerchantInfo(); const { merchant } = CommonHooks.useMerchantInfo();
const { refreshFeatureStatuses } = useDispatch( COMMON_STORE_NAME ); const { refreshFeatureStatuses } = useDispatch( CommonStoreName );
const { createSuccessNotice, createErrorNotice } = const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore ); useDispatch( noticesStore );
const { features, fetchFeatures } = useFeatures(); const { features, fetchFeatures } = useFeatures();

View file

@ -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();
};
}

View file

@ -38,10 +38,16 @@ const useStoreData = () => {
}; };
export const useStore = () => { export const useStore = () => {
const { dispatch, useTransient } = useStoreData(); const { select, dispatch, useTransient } = useStoreData();
const { persist, refresh } = dispatch;
const [ isReady ] = useTransient( 'isReady' ); 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. // TODO: Replace with real hook.

View file

@ -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. * Side effect. Fetches the ISU-login URL for a sandbox account.
* *

View file

@ -13,8 +13,32 @@ import { useCallback, useEffect, useMemo, useState } from '@wordpress/element';
import { createHooksForStore } from '../utils'; import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants'; 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 ); 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 { const {
persist, persist,
sandboxOnboardingUrl, sandboxOnboardingUrl,
@ -23,10 +47,9 @@ const useHooks = () => {
authenticateWithOAuth, authenticateWithOAuth,
startWebhookSimulation, startWebhookSimulation,
checkWebhookSimulationState, checkWebhookSimulationState,
} = useDispatch( STORE_NAME ); } = dispatch;
// Transient accessors. // Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
const [ activeModal, setActiveModal ] = useTransient( 'activeModal' ); const [ activeModal, setActiveModal ] = useTransient( 'activeModal' );
const [ activeHighlight, setActiveHighlight ] = const [ activeHighlight, setActiveHighlight ] =
useTransient( 'activeHighlight' ); useTransient( 'activeHighlight' );
@ -38,18 +61,9 @@ const useHooks = () => {
); );
// Read-only properties. // Read-only properties.
const wooSettings = useSelect( const wooSettings = select.wooSettings();
( select ) => select( STORE_NAME ).wooSettings(), const features = select.features();
[] const webhooks = select.webhooks();
);
const features = useSelect(
( select ) => select( STORE_NAME ).features(),
[]
);
const webhooks = useSelect(
( select ) => select( STORE_NAME ).webhooks(),
[]
);
const savePersistent = async ( setter, value ) => { const savePersistent = async ( setter, value ) => {
setter( value ); setter( value );
@ -57,7 +71,6 @@ const useHooks = () => {
}; };
return { return {
isReady,
activeModal, activeModal,
setActiveModal, setActiveModal,
activeHighlight, 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 = () => { export const useSandbox = () => {
const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks(); const { isSandboxMode, setSandboxMode, sandboxOnboardingUrl } = useHooks();
@ -139,7 +165,7 @@ export const useWebhooks = () => {
}; };
export const useMerchantInfo = () => { export const useMerchantInfo = () => {
const { isReady, features } = useHooks(); const { features } = useHooks();
const merchant = useMerchant(); const merchant = useMerchant();
const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME ); const { refreshMerchantData, setMerchant } = useDispatch( STORE_NAME );
@ -164,7 +190,6 @@ export const useMerchantInfo = () => {
}, [ refreshMerchantData, setMerchant ] ); }, [ refreshMerchantData, setMerchant ] );
return { return {
isReady,
merchant, // Merchant details merchant, // Merchant details
features, // Eligible merchant features features, // Eligible merchant features
verifyLoginStatus, // Callback verifyLoginStatus, // Callback
@ -204,7 +229,9 @@ export const useActiveHighlight = () => {
return { activeHighlight, setActiveHighlight }; return { activeHighlight, setActiveHighlight };
}; };
// -- Not using the `useHooks()` data provider -- /*
* Busy state management hooks
*/
export const useBusyState = () => { export const useBusyState = () => {
const { startActivity, stopActivity } = useDispatch( STORE_NAME ); const { startActivity, stopActivity } = useDispatch( STORE_NAME );

View file

@ -5,6 +5,7 @@ import {
SettingsStoreName, SettingsStoreName,
StylingStoreName, StylingStoreName,
TodosStoreName, TodosStoreName,
FeaturesStoreName,
} from './index'; } from './index';
export const addDebugTools = ( context, modules ) => { export const addDebugTools = ( context, modules ) => {
@ -18,10 +19,15 @@ export const addDebugTools = ( context, modules ) => {
if ( ! context.debug ) { return } 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 || {} ); const debugApi = ( window.ppcpDebugger = window.ppcpDebugger || {} );
// Dump the current state of all our Redux stores. // Dump the current state of all our Redux stores.
debugApi.dumpStore = async () => { debugApi.dumpStore = async ( cbFilter = null ) => {
/* eslint-disable no-console */ /* eslint-disable no-console */
if ( ! console?.groupCollapsed ) { if ( ! console?.groupCollapsed ) {
console.error( 'console.groupCollapsed is not supported.' ); console.error( 'console.groupCollapsed is not supported.' );
@ -34,11 +40,19 @@ export const addDebugTools = ( context, modules ) => {
console.group( `[STORE] ${ storeSelector }` ); console.group( `[STORE] ${ storeSelector }` );
const dumpStore = ( selector ) => { const dumpStore = ( selector ) => {
const contents = wp.data.select( storeName )[ selector ](); let contents = wp.data.select( storeName )[ selector ]();
console.groupCollapsed( `.${ selector }()` ); if ( cbFilter ) {
console.table( contents ); contents = cbFilter( contents, selector, storeName );
console.groupEnd();
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 ); Object.keys( module.selectors ).forEach( dumpStore );
@ -51,45 +65,90 @@ export const addDebugTools = ( context, modules ) => {
// Reset all Redux stores to their initial state. // Reset all Redux stores to their initial state.
debugApi.resetStore = () => { debugApi.resetStore = () => {
const stores = []; const stores = [];
const { isConnected } = wp.data.select( CommonStoreName ).merchant();
if ( isConnected ) { describe(
// Make sure the Onboarding wizard is "completed". 'resetStore',
const onboarding = wp.data.dispatch( OnboardingStoreName ); 'Reset all Redux stores to their DEFAULT state, without changing any server-side data. The default state is defined in the JS code.'
onboarding.setPersistent( 'completed', true ); );
onboarding.persist();
// Reset all stores, except for the onboarding store. const { completed } = wp.data
stores.push( CommonStoreName ); .select( OnboardingStoreName )
stores.push( PaymentStoreName ); .persistentData();
stores.push( SettingsStoreName );
stores.push( StylingStoreName ); // Reset all stores, except for the onboarding store.
stores.push( TodosStoreName ); stores.push( CommonStoreName );
} else { stores.push( PaymentStoreName );
// Only reset the common & onboarding stores to restart the onboarding wizard. stores.push( SettingsStoreName );
stores.push( CommonStoreName ); stores.push( StylingStoreName );
stores.push( TodosStoreName );
// Only reset the onboarding store when the wizard is not completed.
if ( ! completed ) {
stores.push( OnboardingStoreName ); stores.push( OnboardingStoreName );
} }
stores.forEach( ( storeName ) => { stores.forEach( ( storeName ) => {
const store = wp.data.dispatch( storeName ); const store = wp.data.dispatch( storeName );
// eslint-disable-next-line no-console
console.log( `Reset store: ${ storeName }...` );
try { try {
store.reset(); store.reset();
store.persist();
// eslint-disable-next-line no-console
console.log( `Done: Store '${ storeName }' reset` );
} catch ( error ) { } 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. // Disconnect the merchant and display the onboarding wizard.
debugApi.disconnect = () => { debugApi.disconnect = () => {
const common = wp.data.dispatch( CommonStoreName ); const common = wp.data.dispatch( CommonStoreName );
describe();
common.disconnectMerchant(); common.disconnectMerchant();
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -102,6 +161,11 @@ export const addDebugTools = ( context, modules ) => {
debugApi.onboardingMode = ( state ) => { debugApi.onboardingMode = ( state ) => {
const onboarding = wp.data.dispatch( OnboardingStoreName ); const onboarding = wp.data.dispatch( OnboardingStoreName );
describe(
'onboardingMode',
'Toggle between onboarding wizard and the settings screen.'
);
onboarding.setPersistent( 'completed', ! state ); onboarding.setPersistent( 'completed', ! state );
onboarding.persist(); onboarding.persist();
}; };

View file

@ -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();
};
}

View file

@ -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();
};
}

View file

@ -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();
};
}

View file

@ -7,22 +7,58 @@
* @file * @file
*/ */
import { useDispatch } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME } from './constants'; import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils'; 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 { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist, setPersistent, changePaymentSettings } =
useDispatch( STORE_NAME );
// Read-only flags and derived state. return useMemo(
// Nothing here yet. () => ( {
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' ); 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. // PayPal checkout.
const [ paypal ] = usePersistent( 'ppcp-gateway' ); const [ paypal ] = usePersistent( 'ppcp-gateway' );
const [ venmo ] = usePersistent( 'venmo' ); const [ venmo ] = usePersistent( 'venmo' );
@ -47,79 +83,6 @@ const useHooks = () => {
const [ pui ] = usePersistent( 'ppcp-pay-upon-invoice-gateway' ); const [ pui ] = usePersistent( 'ppcp-pay-upon-invoice-gateway' );
const [ oxxo ] = usePersistent( 'ppcp-oxxo-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 payPalCheckout = [ paypal, venmo, payLater, creditCard ];
const onlineCardPayments = [ const onlineCardPayments = [
advancedCreditCard, advancedCreditCard,
@ -169,12 +132,16 @@ export const usePaymentMethods = () => {
}; };
export const usePaymentMethodsModal = () => { export const usePaymentMethodsModal = () => {
const { const { usePersistent } = useStoreData();
paypalShowLogo,
threeDSecure, const [ paypalShowLogo ] = usePersistent( 'paypalShowLogo' );
fastlaneCardholderName, const [ threeDSecure ] = usePersistent( 'threeDSecure' );
fastlaneDisplayWatermark, const [ fastlaneCardholderName ] = usePersistent(
} = useHooks(); 'fastlaneCardholderName'
);
const [ fastlaneDisplayWatermark ] = usePersistent(
'fastlaneDisplayWatermark'
);
return { return {
paypalShowLogo, paypalShowLogo,

View file

@ -19,6 +19,7 @@ const defaultTransient = Object.freeze( {
// Persistent: Values that are loaded from the DB. // Persistent: Values that are loaded from the DB.
const defaultPersistent = Object.freeze( { const defaultPersistent = Object.freeze( {
// Payment methods.
'ppcp-gateway': {}, 'ppcp-gateway': {},
venmo: {}, venmo: {},
'pay-later': {}, 'pay-later': {},
@ -37,6 +38,8 @@ const defaultPersistent = Object.freeze( {
'ppcp-multibanco': {}, 'ppcp-multibanco': {},
'ppcp-pay-upon-invoice-gateway': {}, 'ppcp-pay-upon-invoice-gateway': {},
'ppcp-oxxo-gateway': {}, 'ppcp-oxxo-gateway': {},
// Custom payment method properties.
paypalShowLogo: false, paypalShowLogo: false,
threeDSecure: 'no-3d-secure', threeDSecure: 'no-3d-secure',
fastlaneCardholderName: false, fastlaneCardholderName: false,

View file

@ -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();
};
}

View file

@ -6,17 +6,38 @@
* *
* @file * @file
*/ */
import { useDispatch } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { STORE_NAME } from './constants'; import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils'; 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 useHooks = () => {
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME ); const { usePersistent } = useStoreData();
const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
const [ isReady ] = useTransient( 'isReady' );
// Persistent accessors. // Persistent accessors.
const [ invoicePrefix, setInvoicePrefix ] = const [ invoicePrefix, setInvoicePrefix ] =
@ -47,8 +68,6 @@ const useHooks = () => {
usePersistent( 'disabledCards' ); usePersistent( 'disabledCards' );
return { return {
persist,
isReady,
invoicePrefix, invoicePrefix,
setInvoicePrefix, setInvoicePrefix,
authorizeOnly, authorizeOnly,
@ -79,8 +98,16 @@ const useHooks = () => {
}; };
export const useStore = () => { export const useStore = () => {
const { persist, isReady } = useHooks(); const { select, dispatch, useTransient } = useStoreData();
return { persist, isReady }; 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 = () => { export const useSettings = () => {

View file

@ -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();
};
}

View file

@ -7,7 +7,7 @@
* @file * @file
*/ */
import { useCallback } from '@wordpress/element'; import { useCallback, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data'; import { useDispatch, useSelect } from '@wordpress/data';
import { createHooksForStore } from '../utils'; import { createHooksForStore } from '../utils';
@ -20,13 +20,37 @@ import {
STYLING_PAYMENT_METHODS, STYLING_PAYMENT_METHODS,
STYLING_SHAPES, STYLING_SHAPES,
} from './configuration'; } 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 useHooks = () => {
const { useTransient } = createHooksForStore( STORE_NAME ); const { useTransient, dispatch } = useStoreData();
const { persist, setPersistent } = useDispatch( STORE_NAME ); const { setPersistent } = dispatch;
// Transient accessors. // Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
const [ location, setLocation ] = useTransient( 'location' ); const [ location, setLocation ] = useTransient( 'location' );
// Persistent accessors. // Persistent accessors.
@ -61,8 +85,6 @@ const useHooks = () => {
); );
return { return {
persist,
isReady,
location, location,
setLocation, setLocation,
getLocationProp, getLocationProp,
@ -71,8 +93,16 @@ const useHooks = () => {
}; };
export const useStore = () => { export const useStore = () => {
const { persist, isReady } = useHooks(); const { select, dispatch, useTransient } = useStoreData();
return { persist, isReady }; 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 = () => { export const useStylingLocation = () => {

View file

@ -5,6 +5,12 @@
*/ */
export default { export default {
/**
* Resets the store state to its initial values.
* Used when needing to clear all store data.
*/
RESET: 'ppcp/todos/RESET',
// Transient data // Transient data
SET_TRANSIENT: 'ppcp/todos/SET_TRANSIENT', SET_TRANSIENT: 'ppcp/todos/SET_TRANSIENT',
SET_COMPLETED_TODOS: 'ppcp/todos/SET_COMPLETED_TODOS', SET_COMPLETED_TODOS: 'ppcp/todos/SET_COMPLETED_TODOS',

View file

@ -17,11 +17,47 @@ import {
REST_RESET_DISMISSED_TODOS_PATH, REST_RESET_DISMISSED_TODOS_PATH,
} from './constants'; } from './constants';
export const setIsReady = ( isReady ) => ( { /**
type: ACTION_TYPES.SET_TRANSIENT, * Special. Resets all values in the store to initial defaults.
payload: { isReady }, *
* @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 ) => ( { export const setTodos = ( todos ) => ( {
type: ACTION_TYPES.SET_TODOS, type: ACTION_TYPES.SET_TODOS,
payload: todos, payload: todos,
@ -39,6 +75,7 @@ export const setCompletedTodos = ( completedTodos ) => ( {
// Thunks // Thunks
// TODO: Possibly, this should be a resolver?
export function fetchTodos() { export function fetchTodos() {
return async () => { return async () => {
const response = await apiFetch( { path: REST_PATH } ); 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() { export function persist() {
return async ( { select } ) => { return async ( { select } ) => {
return await apiFetch( { await apiFetch( {
path: REST_PERSIST_PATH, path: REST_PERSIST_PATH,
method: 'POST', method: 'POST',
data: select.persistentData(), 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() { export function resetDismissedTodos() {
return async ( { dispatch } ) => { return async ( { dispatch } ) => {
try { try {

View file

@ -10,31 +10,40 @@
import { useSelect, useDispatch } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
import { STORE_NAME } from './constants'; import { STORE_NAME } from './constants';
import { createHooksForStore } from '../utils'; import { createHooksForStore } from '../utils';
import { useMemo } from '@wordpress/element';
const ensureArray = ( value ) => { /**
if ( ! value ) { * Single source of truth for access Redux details.
return []; *
} * This hook returns a stable API to access actions, selectors and special hooks to generate
return Array.isArray( value ) ? value : Object.values( value ); * 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 useHooks = () => {
const { useTransient } = createHooksForStore( STORE_NAME ); const { dispatch, select } = useStoreData();
const { fetchTodos, setDismissedTodos, setCompletedTodos, persist } = const { fetchTodos, setDismissedTodos, setCompletedTodos } = dispatch;
useDispatch( STORE_NAME );
// Read-only flags and derived state.
const [ isReady ] = useTransient( 'isReady' );
// Get todos data from store // Get todos data from store
const { todos, dismissedTodos, completedTodos } = useSelect( ( select ) => { const todos = select.getTodos();
const store = select( STORE_NAME ); const dismissedTodos = select.getDismissedTodos();
return { const completedTodos = select.getCompletedTodos();
todos: ensureArray( store.getTodos() ),
dismissedTodos: ensureArray( store.getDismissedTodos() ),
completedTodos: ensureArray( store.getCompletedTodos() ),
};
}, [] );
const dismissedSet = new Set( dismissedTodos ); const dismissedSet = new Set( dismissedTodos );
@ -62,8 +71,6 @@ const useHooks = () => {
); );
return { return {
persist,
isReady,
todos: filteredTodos, todos: filteredTodos,
dismissedTodos, dismissedTodos,
completedTodos, completedTodos,
@ -74,14 +81,21 @@ const useHooks = () => {
}; };
export const useStore = () => { export const useStore = () => {
const { persist, isReady } = useHooks(); const { select, dispatch, useTransient } = useStoreData();
return { persist, isReady }; 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 = () => { export const useTodos = () => {
const { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady } = const { todos, fetchTodos, dismissTodo, setTodoCompleted } = useHooks();
useHooks(); return { todos, fetchTodos, dismissTodo, setTodoCompleted };
return { todos, fetchTodos, dismissTodo, setTodoCompleted, isReady };
}; };
export const useDismissedTodos = () => { export const useDismissedTodos = () => {

View file

@ -52,6 +52,21 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) => [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
changeTransient( 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 * 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 * Initializes persistent state with data from the server
* *
* @param {Object} state Current state * @param {Object} state Current state

View file

@ -11,7 +11,17 @@ const EMPTY_OBJ = Object.freeze( {} );
const EMPTY_ARR = Object.freeze( [] ); const EMPTY_ARR = Object.freeze( [] );
const getState = ( state ) => state || EMPTY_OBJ; 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 ) => { export const persistentData = ( state ) => {
return getState( state ).data || EMPTY_OBJ; return getState( state ).data || EMPTY_OBJ;
}; };
@ -23,15 +33,15 @@ export const transientData = ( state ) => {
export const getTodos = ( state ) => { export const getTodos = ( state ) => {
const todos = state?.todos || persistentData( state ).todos; const todos = state?.todos || persistentData( state ).todos;
return todos || EMPTY_ARR; return getArray( todos );
}; };
export const getDismissedTodos = ( state ) => { export const getDismissedTodos = ( state ) => {
const dismissed = const dismissed =
state?.dismissedTodos || persistentData( state ).dismissedTodos; state?.dismissedTodos || persistentData( state ).dismissedTodos;
return dismissed || EMPTY_ARR; return getArray( dismissed );
}; };
export const getCompletedTodos = ( state ) => { 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
}; };

View file

@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices'; import { store as noticesStore } from '@wordpress/notices';
import { CommonHooks, OnboardingHooks } from '../data'; import { CommonHooks, OnboardingHooks } from '../data';
import { useStoreManager } from './useStoreManager';
const PAYPAL_PARTNER_SDK_URL = const PAYPAL_PARTNER_SDK_URL =
'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js'; 'https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js';
@ -30,7 +31,7 @@ export const useHandleOnboardingButton = ( isSandbox ) => {
const { sandboxOnboardingUrl } = CommonHooks.useSandbox(); const { sandboxOnboardingUrl } = CommonHooks.useSandbox();
const { productionOnboardingUrl } = CommonHooks.useProduction(); const { productionOnboardingUrl } = CommonHooks.useProduction();
const products = OnboardingHooks.useDetermineProducts(); const products = OnboardingHooks.useDetermineProducts();
const { withActivity, startActivity } = CommonHooks.useBusyState(); const { startActivity } = CommonHooks.useBusyState();
const { authenticateWithOAuth } = CommonHooks.useAuthentication(); const { authenticateWithOAuth } = CommonHooks.useAuthentication();
const [ onboardingUrl, setOnboardingUrl ] = useState( '' ); const [ onboardingUrl, setOnboardingUrl ] = useState( '' );
const [ scriptLoaded, setScriptLoaded ] = useState( false ); 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. // Ensure the onComplete handler is not removed by a PayPal init script.
timerRef.current = setInterval( addHandler, 250 ); timerRef.current = setInterval( addHandler, 250 );
}, },
[ authenticateWithOAuth, withActivity ] [ authenticateWithOAuth, startActivity ]
); );
const removeCompleteHandler = useCallback( () => { const removeCompleteHandler = useCallback( () => {
@ -161,6 +162,7 @@ const useConnectionBase = () => {
useDispatch( noticesStore ); useDispatch( noticesStore );
const { verifyLoginStatus } = CommonHooks.useMerchantInfo(); const { verifyLoginStatus } = CommonHooks.useMerchantInfo();
const { withActivity } = CommonHooks.useBusyState(); const { withActivity } = CommonHooks.useBusyState();
const { refreshAll } = useStoreManager();
return { return {
handleFailed: ( res, genericMessage ) => { handleFailed: ( res, genericMessage ) => {
@ -178,6 +180,7 @@ const useConnectionBase = () => {
if ( loginSuccessful ) { if ( loginSuccessful ) {
createSuccessNotice( MESSAGES.CONNECTED ); createSuccessNotice( MESSAGES.CONNECTED );
await setCompleted( true ); await setCompleted( true );
refreshAll();
} else { } else {
createErrorNotice( MESSAGES.LOGIN_FAILED ); createErrorNotice( MESSAGES.LOGIN_FAILED );
} }

View file

@ -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 };
};

View file

@ -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,
};
};

View file

@ -211,7 +211,8 @@ class SettingsDataManager {
$this->payment_methods->toggle_method_state( $method['id'], false ); $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( 'venmo', true );
$this->payment_methods->toggle_method_state( 'pay-later', true ); $this->payment_methods->toggle_method_state( 'pay-later', true );