@@ -33,7 +28,7 @@ const StepProducts = ( {
) }
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PRODUCT_TYPES.VIRTUAL }
- changeCallback={ toggleProduct }
+ changeCallback={ setProducts }
currentValue={ products }
type="checkbox"
>
@@ -75,7 +70,7 @@ const StepProducts = ( {
) }
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PRODUCT_TYPES.PHYSICAL }
- changeCallback={ toggleProduct }
+ changeCallback={ setProducts }
currentValue={ products }
type="checkbox"
>
@@ -102,7 +97,7 @@ const StepProducts = ( {
) }
name={ PRODUCTS_CHECKBOX_GROUP_NAME }
value={ PRODUCT_TYPES.SUBSCRIPTIONS }
- changeCallback={ toggleProduct }
+ changeCallback={ setProducts }
currentValue={ products }
type="checkbox"
>
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
index 1d0c07b32..c94c84935 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/StepWelcome.js
@@ -1,13 +1,13 @@
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import OnboardingHeader from '../../ReusableComponents/OnboardingHeader';
import PaymentMethodIcons from '../../ReusableComponents/PaymentMethodIcons';
import Separator from '../../ReusableComponents/Separator';
import WelcomeDocs from '../../ReusableComponents/WelcomeDocs/WelcomeDocs';
+import AccordionSection from '../../ReusableComponents/AccordionSection';
import AdvancedOptionsForm from './Components/AdvancedOptionsForm';
-import AccordionSection from '../../ReusableComponents/AccordionSection';
const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
return (
@@ -57,7 +57,7 @@ const StepWelcome = ( { setStep, currentStep, setCompleted } ) => {
'woocommerce-paypal-payments'
) }
className="onboarding-advanced-options"
- initiallyOpen={ false }
+ id="advanced-options"
>
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js
index a5555180d..7e8ea1556 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/availableSteps.js
@@ -1,6 +1,7 @@
import StepWelcome from './StepWelcome';
import StepBusiness from './StepBusiness';
import StepProducts from './StepProducts';
+import StepPaymentMethods from './StepPaymentMethods';
import StepCompleteSetup from './StepCompleteSetup';
export const getSteps = ( flags ) => {
@@ -8,6 +9,7 @@ export const getSteps = ( flags ) => {
StepWelcome,
StepBusiness,
StepProducts,
+ StepPaymentMethods,
StepCompleteSetup,
];
diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js
index 2f2357951..e0634343c 100644
--- a/modules/ppcp-settings/resources/js/Components/Screens/Settings.js
+++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings.js
@@ -1,9 +1,9 @@
-import { useOnboardingStep } from '../../data';
+import { OnboardingHooks } from '../../data';
import Onboarding from './Onboarding/Onboarding';
import SettingsScreen from './SettingsScreen';
const Settings = () => {
- const onboardingProgress = useOnboardingStep();
+ const onboardingProgress = OnboardingHooks.useSteps();
if ( ! onboardingProgress.isReady ) {
// TODO: Use better loading state indicator.
diff --git a/modules/ppcp-settings/resources/js/data/common/action-types.js b/modules/ppcp-settings/resources/js/data/common/action-types.js
new file mode 100644
index 000000000..47de76afe
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/action-types.js
@@ -0,0 +1,19 @@
+/**
+ * Action Types: Define unique identifiers for actions across all store modules.
+ *
+ * @file
+ */
+
+export default {
+ // Transient data.
+ SET_TRANSIENT: 'COMMON:SET_TRANSIENT',
+
+ // Persistent data.
+ SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
+ HYDRATE: 'COMMON:HYDRATE',
+
+ // Controls - always start with "DO_".
+ DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
+ DO_MANUAL_CONNECTION: 'COMMON:DO_MANUAL_CONNECTION',
+ DO_SANDBOX_LOGIN: 'COMMON:DO_SANDBOX_LOGIN',
+};
diff --git a/modules/ppcp-settings/resources/js/data/common/actions.js b/modules/ppcp-settings/resources/js/data/common/actions.js
new file mode 100644
index 000000000..619aaca5f
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/actions.js
@@ -0,0 +1,154 @@
+/**
+ * Action Creators: Define functions to create action objects.
+ *
+ * These functions update state or trigger side effects (e.g., async operations).
+ * Actions are categorized as Transient, Persistent, or Side effect.
+ *
+ * @file
+ */
+
+import { select } from '@wordpress/data';
+
+import ACTION_TYPES from './action-types';
+import { STORE_NAME } from './constants';
+
+/**
+ * @typedef {Object} Action An action object that is handled by a reducer or control.
+ * @property {string} type - The action type.
+ * @property {Object?} payload - Optional payload for the action.
+ */
+
+/**
+ * Persistent. Set the full onboarding details, usually during app initialization.
+ *
+ * @param {{data: {}, flags?: {}}} payload
+ * @return {Action} The action.
+ */
+export const hydrate = ( payload ) => ( {
+ type: ACTION_TYPES.HYDRATE,
+ payload,
+} );
+
+/**
+ * Transient. Marks the onboarding details as "ready", i.e., fully initialized.
+ *
+ * @param {boolean} isReady
+ * @return {Action} The action.
+ */
+export const setIsReady = ( isReady ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { isReady },
+} );
+
+/**
+ * Transient. Changes the "saving" flag.
+ *
+ * @param {boolean} isSaving
+ * @return {Action} The action.
+ */
+export const setIsSaving = ( isSaving ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { isSaving },
+} );
+
+/**
+ * Transient. Changes the "manual connection is busy" flag.
+ *
+ * @param {boolean} isBusy
+ * @return {Action} The action.
+ */
+export const setIsBusy = ( isBusy ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { isBusy },
+} );
+
+/**
+ * Persistent. Sets the sandbox mode on or off.
+ *
+ * @param {boolean} useSandbox
+ * @return {Action} The action.
+ */
+export const setSandboxMode = ( useSandbox ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { useSandbox },
+} );
+
+/**
+ * Persistent. Toggles the "Manual Connection" mode on or off.
+ *
+ * @param {boolean} useManualConnection
+ * @return {Action} The action.
+ */
+export const setManualConnectionMode = ( useManualConnection ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { useManualConnection },
+} );
+
+/**
+ * Persistent. Changes the "client ID" value.
+ *
+ * @param {string} clientId
+ * @return {Action} The action.
+ */
+export const setClientId = ( clientId ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { clientId },
+} );
+
+/**
+ * Persistent. Changes the "client secret" value.
+ *
+ * @param {string} clientSecret
+ * @return {Action} The action.
+ */
+export const setClientSecret = ( clientSecret ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { clientSecret },
+} );
+
+/**
+ * Side effect. Saves the persistent details to the WP database.
+ *
+ * @return {Action} The action.
+ */
+export const persist = function* () {
+ const data = yield select( STORE_NAME ).persistentData();
+
+ yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
+};
+
+/**
+ * Side effect. Initiates the sandbox login ISU.
+ *
+ * @return {Action} The action.
+ */
+export const connectViaSandbox = function* () {
+ yield setIsBusy( true );
+
+ const result = yield { type: ACTION_TYPES.DO_SANDBOX_LOGIN };
+ yield setIsBusy( false );
+
+ return result;
+};
+
+/**
+ * Side effect. Initiates a manual connection attempt using the provided client ID and secret.
+ *
+ * @return {Action} The action.
+ */
+export const connectViaIdAndSecret = function* () {
+ const { clientId, clientSecret, useSandbox } =
+ yield select( STORE_NAME ).persistentData();
+
+ yield setIsBusy( true );
+
+ const result = yield {
+ type: ACTION_TYPES.DO_MANUAL_CONNECTION,
+ clientId,
+ clientSecret,
+ useSandbox,
+ };
+ yield setIsBusy( false );
+
+ return result;
+};
diff --git a/modules/ppcp-settings/resources/js/data/common/constants.js b/modules/ppcp-settings/resources/js/data/common/constants.js
new file mode 100644
index 000000000..c7ea9b4c1
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/constants.js
@@ -0,0 +1,46 @@
+/**
+ * Name of the module-store in the main Redux store.
+ *
+ * Helps to isolate data, used by reducer and selectors.
+ *
+ * @type {string}
+ */
+export const STORE_NAME = 'wc/paypal/common';
+
+/**
+ * REST path to hydrate data of this module by loading data from the WP DB..
+ *
+ * Used by resolvers.
+ *
+ * @type {string}
+ */
+export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/common';
+
+/**
+ * REST path to persist data of this module to the WP DB.
+ *
+ * Used by controls.
+ *
+ * @type {string}
+ */
+export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/common';
+
+/**
+ * REST path to perform the manual connection check, using client ID and secret,
+ *
+ * Used by: Controls
+ * See: ConnectManualRestEndpoint.php
+ *
+ * @type {string}
+ */
+export const REST_MANUAL_CONNECTION_PATH = '/wc/v3/wc_paypal/connect_manual';
+
+/**
+ * REST path to generate an ISU URL for the sandbox-login.
+ *
+ * Used by: Controls
+ * See: LoginLinkRestEndpoint.php
+ *
+ * @type {string}
+ */
+export const REST_SANDBOX_CONNECTION_PATH = '/wc/v3/wc_paypal/login_link';
diff --git a/modules/ppcp-settings/resources/js/data/common/controls.js b/modules/ppcp-settings/resources/js/data/common/controls.js
new file mode 100644
index 000000000..6de513e0b
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/controls.js
@@ -0,0 +1,80 @@
+/**
+ * Controls: Implement side effects, typically asynchronous operations.
+ *
+ * Controls use ACTION_TYPES keys as identifiers.
+ * They are triggered by corresponding actions and handle external interactions.
+ *
+ * @file
+ */
+
+import apiFetch from '@wordpress/api-fetch';
+
+import {
+ REST_PERSIST_PATH,
+ REST_MANUAL_CONNECTION_PATH,
+ REST_SANDBOX_CONNECTION_PATH,
+} from './constants';
+import ACTION_TYPES from './action-types';
+
+export const controls = {
+ async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
+ try {
+ return await apiFetch( {
+ path: REST_PERSIST_PATH,
+ method: 'POST',
+ data,
+ } );
+ } catch ( error ) {
+ console.error( 'Error saving data.', error );
+ }
+ },
+
+ async [ ACTION_TYPES.DO_SANDBOX_LOGIN ]() {
+ let result = null;
+
+ try {
+ result = await apiFetch( {
+ path: REST_SANDBOX_CONNECTION_PATH,
+ method: 'POST',
+ data: {
+ environment: 'sandbox',
+ products: [ 'EXPRESS_CHECKOUT' ],
+ },
+ } );
+ } catch ( e ) {
+ result = {
+ success: false,
+ error: e,
+ };
+ }
+
+ return result;
+ },
+
+ async [ ACTION_TYPES.DO_MANUAL_CONNECTION ]( {
+ clientId,
+ clientSecret,
+ useSandbox,
+ } ) {
+ let result = null;
+
+ try {
+ result = await apiFetch( {
+ path: REST_MANUAL_CONNECTION_PATH,
+ method: 'POST',
+ data: {
+ clientId,
+ clientSecret,
+ useSandbox,
+ },
+ } );
+ } catch ( e ) {
+ result = {
+ success: false,
+ error: e,
+ };
+ }
+
+ return result;
+ },
+};
diff --git a/modules/ppcp-settings/resources/js/data/common/hooks.js b/modules/ppcp-settings/resources/js/data/common/hooks.js
new file mode 100644
index 000000000..8be3857b0
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/hooks.js
@@ -0,0 +1,111 @@
+/**
+ * Hooks: Provide the main API for components to interact with the store.
+ *
+ * These encapsulate store interactions, offering a consistent interface.
+ * Hooks simplify data access and manipulation for components.
+ *
+ * @file
+ */
+
+import { useDispatch, useSelect } from '@wordpress/data';
+import { useCallback } from '@wordpress/element';
+
+import { STORE_NAME } from './constants';
+
+const useTransient = ( key ) =>
+ useSelect(
+ ( select ) => select( STORE_NAME ).transientData()?.[ key ],
+ [ key ]
+ );
+
+const usePersistent = ( key ) =>
+ useSelect(
+ ( select ) => select( STORE_NAME ).persistentData()?.[ key ],
+ [ key ]
+ );
+
+const useHooks = () => {
+ const {
+ persist,
+ setSandboxMode,
+ setManualConnectionMode,
+ setClientId,
+ setClientSecret,
+ connectViaSandbox,
+ connectViaIdAndSecret,
+ } = useDispatch( STORE_NAME );
+
+ // Transient accessors.
+ const isReady = useTransient( 'isReady' );
+
+ // Persistent accessors.
+ const clientId = usePersistent( 'clientId' );
+ const clientSecret = usePersistent( 'clientSecret' );
+ const isSandboxMode = usePersistent( 'useSandbox' );
+ const isManualConnectionMode = usePersistent( 'useManualConnection' );
+
+ const savePersistent = async ( setter, value ) => {
+ setter( value );
+ await persist();
+ };
+
+ return {
+ isReady,
+ isSandboxMode,
+ setSandboxMode: ( state ) => {
+ return savePersistent( setSandboxMode, state );
+ },
+ isManualConnectionMode,
+ setManualConnectionMode: ( state ) => {
+ return savePersistent( setManualConnectionMode, state );
+ },
+ clientId,
+ setClientId: ( value ) => {
+ return savePersistent( setClientId, value );
+ },
+ clientSecret,
+ setClientSecret: ( value ) => {
+ return savePersistent( setClientSecret, value );
+ },
+ connectViaSandbox,
+ connectViaIdAndSecret,
+ };
+};
+
+export const useBusyState = () => {
+ const { setIsBusy } = useDispatch( STORE_NAME );
+ const isBusy = useTransient( 'isBusy' );
+
+ return {
+ isBusy,
+ setIsBusy: useCallback( ( busy ) => setIsBusy( busy ), [ setIsBusy ] ),
+ };
+};
+
+export const useSandbox = () => {
+ const { isSandboxMode, setSandboxMode, connectViaSandbox } = useHooks();
+
+ return { isSandboxMode, setSandboxMode, connectViaSandbox };
+};
+
+export const useManualConnection = () => {
+ const {
+ isManualConnectionMode,
+ setManualConnectionMode,
+ clientId,
+ setClientId,
+ clientSecret,
+ setClientSecret,
+ connectViaIdAndSecret,
+ } = useHooks();
+
+ return {
+ isManualConnectionMode,
+ setManualConnectionMode,
+ clientId,
+ setClientId,
+ clientSecret,
+ setClientSecret,
+ connectViaIdAndSecret,
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/data/common/index.js b/modules/ppcp-settings/resources/js/data/common/index.js
new file mode 100644
index 000000000..28c162f98
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/index.js
@@ -0,0 +1,24 @@
+import { createReduxStore, register } from '@wordpress/data';
+import { controls as wpControls } from '@wordpress/data-controls';
+
+import { STORE_NAME } from './constants';
+import reducer from './reducer';
+import * as selectors from './selectors';
+import * as actions from './actions';
+import * as hooks from './hooks';
+import { resolvers } from './resolvers';
+import { controls } from './controls';
+
+export const initStore = () => {
+ const store = createReduxStore( STORE_NAME, {
+ reducer,
+ controls: { ...wpControls, ...controls },
+ actions,
+ selectors,
+ resolvers,
+ } );
+
+ register( store );
+};
+
+export { hooks, selectors, STORE_NAME };
diff --git a/modules/ppcp-settings/resources/js/data/common/reducer.js b/modules/ppcp-settings/resources/js/data/common/reducer.js
new file mode 100644
index 000000000..3f822468b
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/reducer.js
@@ -0,0 +1,45 @@
+/**
+ * Reducer: Defines store structure and state updates for this module.
+ *
+ * Manages both transient (temporary) and persistent (saved) state.
+ * The initial state must define all properties, as dynamic additions are not supported.
+ *
+ * @file
+ */
+
+import { createReducer, createSetters } from '../utils';
+import ACTION_TYPES from './action-types';
+
+// Store structure.
+
+const defaultTransient = {
+ isReady: false,
+ isBusy: false,
+};
+
+const defaultPersistent = {
+ useSandbox: false,
+ useManualConnection: false,
+ clientId: '',
+ clientSecret: '',
+};
+
+// Reducer logic.
+
+const [ setTransient, setPersistent ] = createSetters(
+ defaultTransient,
+ defaultPersistent
+);
+
+const commonReducer = createReducer( defaultTransient, defaultPersistent, {
+ [ ACTION_TYPES.SET_TRANSIENT ]: ( state, action ) =>
+ setTransient( state, action ),
+
+ [ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) =>
+ setPersistent( state, action ),
+
+ [ ACTION_TYPES.HYDRATE ]: ( state, payload ) =>
+ setPersistent( state, payload.data ),
+} );
+
+export default commonReducer;
diff --git a/modules/ppcp-settings/resources/js/data/common/resolvers.js b/modules/ppcp-settings/resources/js/data/common/resolvers.js
new file mode 100644
index 000000000..ceebca53f
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/resolvers.js
@@ -0,0 +1,36 @@
+/**
+ * Resolvers: Handle asynchronous data fetching for the store.
+ *
+ * These functions update store state with data from external sources.
+ * Each resolver corresponds to a specific selector (selector with same name must exist).
+ * Resolvers are called automatically when selectors request unavailable data.
+ *
+ * @file
+ */
+
+import { dispatch } from '@wordpress/data';
+import { __ } from '@wordpress/i18n';
+import { apiFetch } from '@wordpress/data-controls';
+
+import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
+
+export const resolvers = {
+ /**
+ * Retrieve settings from the site's REST API.
+ */
+ *persistentData() {
+ try {
+ const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
+
+ yield dispatch( STORE_NAME ).hydrate( result );
+ yield dispatch( STORE_NAME ).setIsReady( true );
+ } catch ( e ) {
+ yield dispatch( 'core/notices' ).createErrorNotice(
+ __(
+ 'Error retrieving plugin details.',
+ 'woocommerce-paypal-payments'
+ )
+ );
+ }
+ },
+};
diff --git a/modules/ppcp-settings/resources/js/data/common/selectors.js b/modules/ppcp-settings/resources/js/data/common/selectors.js
new file mode 100644
index 000000000..14334fcf3
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/common/selectors.js
@@ -0,0 +1,21 @@
+/**
+ * Selectors: Extract specific pieces of state from the store.
+ *
+ * These functions provide a consistent interface for accessing store data.
+ * They allow components to retrieve data without knowing the store structure.
+ *
+ * @file
+ */
+
+const EMPTY_OBJ = Object.freeze( {} );
+
+const getState = ( state ) => state || EMPTY_OBJ;
+
+export const persistentData = ( state ) => {
+ return getState( state ).data || EMPTY_OBJ;
+};
+
+export const transientData = ( state ) => {
+ const { data, ...transientState } = getState( state );
+ return transientState || EMPTY_OBJ;
+};
diff --git a/modules/ppcp-settings/resources/js/data/constants.js b/modules/ppcp-settings/resources/js/data/constants.js
index e6f8f9de5..5654ad476 100644
--- a/modules/ppcp-settings/resources/js/data/constants.js
+++ b/modules/ppcp-settings/resources/js/data/constants.js
@@ -1,6 +1,3 @@
-export const NAMESPACE = '/wc/v3/wc_paypal';
-export const STORE_NAME = 'wc/paypal';
-
export const BUSINESS_TYPES = {
CASUAL_SELLER: 'casual_seller',
BUSINESS: 'business',
diff --git a/modules/ppcp-settings/resources/js/data/debug.js b/modules/ppcp-settings/resources/js/data/debug.js
new file mode 100644
index 000000000..b292d1920
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/debug.js
@@ -0,0 +1,47 @@
+import { OnboardingStoreName } from './index';
+
+export const addDebugTools = ( context, modules ) => {
+ if ( ! context || ! context?.debug ) {
+ return;
+ }
+
+ context.dumpStore = async () => {
+ /* eslint-disable no-console */
+ if ( ! console?.groupCollapsed ) {
+ console.error( 'console.groupCollapsed is not supported.' );
+ return;
+ }
+
+ modules.forEach( ( module ) => {
+ const storeName = module.STORE_NAME;
+ const storeSelector = `wp.data.select( '${ storeName }' )`;
+ console.group( `[STORE] ${ storeSelector }` );
+
+ const dumpStore = ( selector ) => {
+ const contents = wp.data.select( storeName )[ selector ]();
+
+ console.groupCollapsed( `.${ selector }()` );
+ console.table( contents );
+ console.groupEnd();
+ };
+
+ Object.keys( module.selectors ).forEach( dumpStore );
+
+ console.groupEnd();
+ } );
+ /* eslint-enable no-console */
+ };
+
+ context.resetStore = () => {
+ const onboarding = wp.data.dispatch( OnboardingStoreName );
+ onboarding.reset();
+ onboarding.persist();
+ };
+
+ context.startOnboarding = () => {
+ const onboarding = wp.data.dispatch( OnboardingStoreName );
+ onboarding.setCompleted( false );
+ onboarding.setStep( 0 );
+ onboarding.persist();
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/data/index.js b/modules/ppcp-settings/resources/js/data/index.js
index 1c95c4261..274aac790 100644
--- a/modules/ppcp-settings/resources/js/data/index.js
+++ b/modules/ppcp-settings/resources/js/data/index.js
@@ -1,7 +1,16 @@
-import { STORE_NAME } from './constants';
-import { initStore } from './store';
+import { addDebugTools } from './debug';
+import * as Onboarding from './onboarding';
+import * as Common from './common';
-initStore();
+Onboarding.initStore();
+Common.initStore();
-export const WC_PAYPAL_STORE_NAME = STORE_NAME;
-export * from './onboarding/hooks';
+export const OnboardingHooks = Onboarding.hooks;
+export const CommonHooks = Common.hooks;
+
+export const OnboardingStoreName = Onboarding.STORE_NAME;
+export const CommonStoreName = Common.STORE_NAME;
+
+export * from './constants';
+
+addDebugTools( window.ppcpSettings, [ Onboarding, Common ] );
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js
index 39472e2ff..2e16f8468 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/action-types.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/action-types.js
@@ -1,19 +1,18 @@
-export default {
- RESET_ONBOARDING: 'RESET_ONBOARDING',
+/**
+ * Action Types: Define unique identifiers for actions across all store modules.
+ *
+ * @file
+ */
+export default {
// Transient data.
- SET_ONBOARDING_IS_READY: 'SET_ONBOARDING_IS_READY',
- SET_IS_SAVING_ONBOARDING: 'SET_IS_SAVING_ONBOARDING',
- SET_MANUAL_CONNECTION_BUSY: 'SET_MANUAL_CONNECTION_BUSY',
+ SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT',
// Persistent data.
- SET_ONBOARDING_COMPLETED: 'SET_ONBOARDING_COMPLETED',
- SET_ONBOARDING_DETAILS: 'SET_ONBOARDING_DETAILS',
- SET_ONBOARDING_STEP: 'SET_ONBOARDING_STEP',
- SET_SANDBOX_MODE: 'SET_SANDBOX_MODE',
- SET_MANUAL_CONNECTION_MODE: 'SET_MANUAL_CONNECTION_MODE',
- SET_CLIENT_ID: 'SET_CLIENT_ID',
- SET_CLIENT_SECRET: 'SET_CLIENT_SECRET',
- SET_IS_CASUAL_SELLER: 'SET_IS_CASUAL_SELLER',
- SET_PRODUCTS: 'SET_PRODUCTS',
+ SET_PERSISTENT: 'ONBOARDING:SET_PERSISTENT',
+ RESET: 'ONBOARDING:RESET',
+ HYDRATE: 'ONBOARDING:HYDRATE',
+
+ // Controls - always start with "DO_".
+ DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA',
};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/actions.js b/modules/ppcp-settings/resources/js/data/onboarding/actions.js
index 09229e63e..dcf401995 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/actions.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/actions.js
@@ -1,235 +1,116 @@
+/**
+ * Action Creators: Define functions to create action objects.
+ *
+ * These functions update state or trigger side effects (e.g., async operations).
+ * Actions are categorized as Transient, Persistent, or Side effect.
+ *
+ * @file
+ */
+
import { select } from '@wordpress/data';
-import { apiFetch } from '@wordpress/data-controls';
+
import ACTION_TYPES from './action-types';
-import { NAMESPACE, STORE_NAME } from '../constants';
+import { STORE_NAME } from './constants';
+
+/**
+ * @typedef {Object} Action An action object that is handled by a reducer or control.
+ * @property {string} type - The action type.
+ * @property {Object?} payload - Optional payload for the action.
+ */
/**
* Special. Resets all values in the onboarding store to initial defaults.
*
- * @return {{type: string}} The action.
+ * @return {Action} The action.
*/
-export const resetOnboarding = () => {
- return { type: ACTION_TYPES.RESET_ONBOARDING };
-};
-
-/**
- * Non-persistent. Marks the onboarding details as "ready", i.e., fully initialized.
- *
- * @param {boolean} isReady
- * @return {{type: string, isReady}} The action.
- */
-export const setIsReady = ( isReady ) => {
- return {
- type: ACTION_TYPES.SET_ONBOARDING_IS_READY,
- isReady,
- };
-};
-
-/**
- * Non-persistent. Changes the "saving" flag.
- *
- * @param {boolean} isSaving
- * @return {{type: string, isSaving}} The action.
- */
-export const setIsSaving = ( isSaving ) => {
- return {
- type: ACTION_TYPES.SET_IS_SAVING_ONBOARDING,
- isSaving,
- };
-};
-
-/**
- * Non-persistent. Changes the "manual connection is busy" flag.
- *
- * @param {boolean} isBusy
- * @return {{type: string, isBusy}} The action.
- */
-export const setManualConnectionIsBusy = ( isBusy ) => {
- return {
- type: ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY,
- isBusy,
- };
-};
+export const reset = () => ( { type: ACTION_TYPES.RESET } );
/**
* Persistent. Set the full onboarding details, usually during app initialization.
*
* @param {{data: {}, flags?: {}}} payload
- * @return {{type: string, payload}} The action.
+ * @return {Action} The action.
*/
-export const setOnboardingDetails = ( payload ) => {
- return {
- type: ACTION_TYPES.SET_ONBOARDING_DETAILS,
- payload,
- };
-};
+export const hydrate = ( payload ) => ( {
+ type: ACTION_TYPES.HYDRATE,
+ payload,
+} );
+
+/**
+ * Transient. Marks the onboarding details as "ready", i.e., fully initialized.
+ *
+ * @param {boolean} isReady
+ * @return {Action} The action.
+ */
+export const setIsReady = ( isReady ) => ( {
+ type: ACTION_TYPES.SET_TRANSIENT,
+ payload: { isReady },
+} );
/**
* Persistent.Set the "onboarding completed" flag which shows or hides the wizard.
*
* @param {boolean} completed
- * @return {{type: string, payload}} The action.
+ * @return {Action} The action.
*/
-export const setCompleted = ( completed ) => {
- return {
- type: ACTION_TYPES.SET_ONBOARDING_COMPLETED,
- completed,
- };
-};
+export const setCompleted = ( completed ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { completed },
+} );
/**
* Persistent. Sets the onboarding wizard to a new step.
*
* @param {number} step
- * @return {{type: string, step}} An action.
+ * @return {Action} The action.
*/
-export const setOnboardingStep = ( step ) => {
- return {
- type: ACTION_TYPES.SET_ONBOARDING_STEP,
- step,
- };
-};
-
-/**
- * Persistent. Sets the sandbox mode on or off.
- *
- * @param {boolean} sandboxMode
- * @return {{type: string, useSandbox}} An action.
- */
-export const setSandboxMode = ( sandboxMode ) => {
- return {
- type: ACTION_TYPES.SET_SANDBOX_MODE,
- useSandbox: sandboxMode,
- };
-};
-
-/**
- * Persistent. Toggles the "Manual Connection" mode on or off.
- *
- * @param {boolean} manualConnectionMode
- * @return {{type: string, useManualConnection}} An action.
- */
-export const setManualConnectionMode = ( manualConnectionMode ) => {
- return {
- type: ACTION_TYPES.SET_MANUAL_CONNECTION_MODE,
- useManualConnection: manualConnectionMode,
- };
-};
-
-/**
- * Persistent. Changes the "client ID" value.
- *
- * @param {string} clientId
- * @return {{type: string, clientId}} The action.
- */
-export const setClientId = ( clientId ) => {
- return {
- type: ACTION_TYPES.SET_CLIENT_ID,
- clientId,
- };
-};
-
-/**
- * Persistent. Changes the "client secret" value.
- *
- * @param {string} clientSecret
- * @return {{type: string, clientSecret}} The action.
- */
-export const setClientSecret = ( clientSecret ) => {
- return {
- type: ACTION_TYPES.SET_CLIENT_SECRET,
- clientSecret,
- };
-};
+export const setStep = ( step ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { step },
+} );
/**
* Persistent. Sets the "isCasualSeller" value.
*
* @param {boolean} isCasualSeller
- * @return {{type: string, isCasualSeller}} The action.
+ * @return {Action} The action.
*/
-export const setIsCasualSeller = ( isCasualSeller ) => {
- return {
- type: ACTION_TYPES.SET_IS_CASUAL_SELLER,
- isCasualSeller,
- };
-};
+export const setIsCasualSeller = ( isCasualSeller ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { isCasualSeller },
+} );
+
+/**
+ * Persistent. Sets the "areOptionalPaymentMethodsEnabled" value.
+ *
+ * @param {boolean} areOptionalPaymentMethodsEnabled
+ * @return {Action} The action.
+ */
+export const setAreOptionalPaymentMethodsEnabled = (
+ areOptionalPaymentMethodsEnabled
+) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { areOptionalPaymentMethodsEnabled },
+} );
/**
* Persistent. Sets the "products" array.
*
* @param {string[]} products
- * @return {{type: string, products}} The action.
+ * @return {Action} The action.
*/
-export const setProducts = ( products ) => {
- return {
- type: ACTION_TYPES.SET_PRODUCTS,
- products,
- };
+export const setProducts = ( products ) => ( {
+ type: ACTION_TYPES.SET_PERSISTENT,
+ payload: { products },
+} );
+
+/**
+ * Side effect. Triggers the persistence of onboarding data to the server.
+ *
+ * @return {Action} The action.
+ */
+export const persist = function* () {
+ const data = yield select( STORE_NAME ).persistentData();
+
+ yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
-
-/**
- * Attempts to establish a connection using client ID and secret via the server-side
- * connection endpoint.
- *
- * @return {Object} The server response object
- */
-export function* connectViaIdAndSecret() {
- let result = null;
-
- try {
- const path = `${ NAMESPACE }/connect_manual`;
- const { clientId, clientSecret, useSandbox } =
- yield select( STORE_NAME ).getPersistentData();
-
- yield setManualConnectionIsBusy( true );
-
- result = yield apiFetch( {
- path,
- method: 'POST',
- data: {
- clientId,
- clientSecret,
- useSandbox,
- },
- } );
- } catch ( e ) {
- result = {
- success: false,
- error: e,
- };
- } finally {
- yield setManualConnectionIsBusy( false );
- }
-
- return result;
-}
-
-/**
- * Saves the persistent details to the WP database.
- *
- * @return {boolean} True, if the values were successfully saved.
- */
-export function* persist() {
- let error = null;
-
- try {
- const path = `${ NAMESPACE }/onboarding`;
- const data = select( STORE_NAME ).getPersistentData();
-
- yield setIsSaving( true );
-
- yield apiFetch( {
- path,
- method: 'post',
- data,
- } );
- } catch ( e ) {
- error = e;
- console.error( 'Error saving progress.', e );
- } finally {
- yield setIsSaving( false );
- }
-
- return error === null;
-}
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/constants.js b/modules/ppcp-settings/resources/js/data/onboarding/constants.js
new file mode 100644
index 000000000..4b33c6701
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/onboarding/constants.js
@@ -0,0 +1,28 @@
+/**
+ * Name of the Redux store module.
+ *
+ * Used by: Reducer, Selector, Index
+ *
+ * @type {string}
+ */
+export const STORE_NAME = 'wc/paypal/onboarding';
+
+/**
+ * REST path to hydrate data of this module by loading data from the WP DB..
+ *
+ * Used by: Resolvers
+ * See: OnboardingRestEndpoint.php
+ *
+ * @type {string}
+ */
+export const REST_HYDRATE_PATH = '/wc/v3/wc_paypal/onboarding';
+
+/**
+ * REST path to persist data of this module to the WP DB.
+ *
+ * Used by: Controls
+ * See: OnboardingRestEndpoint.php
+ *
+ * @type {string}
+ */
+export const REST_PERSIST_PATH = '/wc/v3/wc_paypal/onboarding';
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/controls.js b/modules/ppcp-settings/resources/js/data/onboarding/controls.js
new file mode 100644
index 000000000..30f1cce48
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/onboarding/controls.js
@@ -0,0 +1,27 @@
+/**
+ * Controls: Implement side effects, typically asynchronous operations.
+ *
+ * Controls use ACTION_TYPES keys as identifiers.
+ * They are triggered by corresponding actions and handle external interactions.
+ *
+ * @file
+ */
+
+import apiFetch from '@wordpress/api-fetch';
+
+import { REST_PERSIST_PATH } from './constants';
+import ACTION_TYPES from './action-types';
+
+export const controls = {
+ async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
+ try {
+ await apiFetch( {
+ path: REST_PERSIST_PATH,
+ method: 'POST',
+ data,
+ } );
+ } catch ( e ) {
+ console.error( 'Error saving progress.', e );
+ }
+ },
+};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
index ff9052d69..4ae5bd947 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/hooks.js
@@ -1,163 +1,115 @@
-import { useSelect, useDispatch } from '@wordpress/data';
-import apiFetch from '@wordpress/api-fetch';
-import { NAMESPACE, PRODUCT_TYPES, STORE_NAME } from '../constants';
-import { getFlags } from './selectors';
+/**
+ * Hooks: Provide the main API for components to interact with the store.
+ *
+ * These encapsulate store interactions, offering a consistent interface.
+ * Hooks simplify data access and manipulation for components.
+ *
+ * @file
+ */
-const useOnboardingDetails = () => {
+import { useSelect, useDispatch } from '@wordpress/data';
+
+import { PRODUCT_TYPES } from '../constants';
+import { STORE_NAME } from './constants';
+
+const useTransient = ( key ) =>
+ useSelect(
+ ( select ) => select( STORE_NAME ).transientData()?.[ key ],
+ [ key ]
+ );
+
+const usePersistent = ( key ) =>
+ useSelect(
+ ( select ) => select( STORE_NAME ).persistentData()?.[ key ],
+ [ key ]
+ );
+
+const useHooks = () => {
const {
persist,
- setOnboardingStep,
+ setStep,
setCompleted,
- setSandboxMode,
- setManualConnectionMode,
- setClientId,
- setClientSecret,
setIsCasualSeller,
+ setAreOptionalPaymentMethodsEnabled,
setProducts,
} = useDispatch( STORE_NAME );
- // Transient accessors.
- const isSaving = useSelect( ( select ) => {
- return select( STORE_NAME ).getTransientData().isSaving;
- }, [] );
-
- const isReady = useSelect( ( select ) => {
- return select( STORE_NAME ).getTransientData().isReady;
- } );
-
- const isManualConnectionBusy = useSelect( ( select ) => {
- return select( STORE_NAME ).getTransientData().isManualConnectionBusy;
- }, [] );
-
// Read-only flags.
- const flags = useSelect( ( select ) => {
- return select( STORE_NAME ).getFlags();
- } );
+ const flags = useSelect( ( select ) => select( STORE_NAME ).flags(), [] );
+
+ // Transient accessors.
+ const isReady = useTransient( 'isReady' );
// Persistent accessors.
- const step = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().step || 0;
- } );
+ const step = usePersistent( 'step' );
+ const completed = usePersistent( 'completed' );
+ const isCasualSeller = usePersistent( 'isCasualSeller' );
+ const areOptionalPaymentMethodsEnabled = usePersistent(
+ 'areOptionalPaymentMethodsEnabled'
+ );
+ const products = usePersistent( 'products' );
- const completed = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().completed;
- } );
-
- const clientId = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().clientId;
- }, [] );
-
- const clientSecret = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().clientSecret;
- }, [] );
-
- const isSandboxMode = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().useSandbox;
- }, [] );
-
- const isManualConnectionMode = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().useManualConnection;
- }, [] );
-
- const isCasualSeller = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().isCasualSeller;
- }, [] );
-
- const products = useSelect( ( select ) => {
- return select( STORE_NAME ).getPersistentData().products || [];
- }, [] );
-
- const toggleProduct = ( list ) => {
- const validProducts = list.filter( ( item ) =>
- Object.values( PRODUCT_TYPES ).includes( item )
- );
- return setDetailAndPersist( setProducts, validProducts );
- };
-
- const setDetailAndPersist = async ( setter, value ) => {
+ const savePersistent = async ( setter, value ) => {
setter( value );
await persist();
};
return {
- isSaving,
- isReady,
- isManualConnectionBusy,
- step,
- setStep: ( value ) => setDetailAndPersist( setOnboardingStep, value ),
- completed,
- setCompleted: ( state ) => setDetailAndPersist( setCompleted, state ),
- isSandboxMode,
- setSandboxMode: ( state ) =>
- setDetailAndPersist( setSandboxMode, state ),
- isManualConnectionMode,
- setManualConnectionMode: ( state ) =>
- setDetailAndPersist( setManualConnectionMode, state ),
- clientId,
- setClientId: ( value ) => setDetailAndPersist( setClientId, value ),
- clientSecret,
- setClientSecret: ( value ) =>
- setDetailAndPersist( setClientSecret, value ),
- isCasualSeller,
- setIsCasualSeller: ( value ) =>
- setDetailAndPersist( setIsCasualSeller, value ),
- products,
- toggleProduct,
flags,
+ isReady,
+ step,
+ setStep: ( value ) => {
+ return savePersistent( setStep, value );
+ },
+ completed,
+ setCompleted: ( state ) => {
+ return savePersistent( setCompleted, state );
+ },
+ isCasualSeller,
+ setIsCasualSeller: ( value ) => {
+ return savePersistent( setIsCasualSeller, value );
+ },
+ areOptionalPaymentMethodsEnabled,
+ setAreOptionalPaymentMethodsEnabled: ( value ) => {
+ return savePersistent( setAreOptionalPaymentMethodsEnabled, value );
+ },
+ products,
+ setProducts: ( activeProducts ) => {
+ const validProducts = activeProducts.filter( ( item ) =>
+ Object.values( PRODUCT_TYPES ).includes( item )
+ );
+ return savePersistent( setProducts, validProducts );
+ },
};
};
-export const useOnboardingStepWelcome = () => {
- const {
- isSaving,
- isManualConnectionBusy,
- isSandboxMode,
- setSandboxMode,
- isManualConnectionMode,
- setManualConnectionMode,
- clientId,
- setClientId,
- clientSecret,
- setClientSecret,
- } = useOnboardingDetails();
-
- return {
- isSaving,
- isManualConnectionBusy,
- isSandboxMode,
- setSandboxMode,
- isManualConnectionMode,
- setManualConnectionMode,
- clientId,
- setClientId,
- clientSecret,
- setClientSecret,
- };
-};
-
-export const useOnboardingStepBusiness = () => {
- const { isCasualSeller, setIsCasualSeller } = useOnboardingDetails();
+export const useBusiness = () => {
+ const { isCasualSeller, setIsCasualSeller } = useHooks();
return { isCasualSeller, setIsCasualSeller };
};
-export const useOnboardingStepProducts = () => {
- const { products, toggleProduct } = useOnboardingDetails();
+export const useProducts = () => {
+ const { products, setProducts } = useHooks();
- return { products, toggleProduct };
+ return { products, setProducts };
};
-export const useOnboardingStep = () => {
- const { isReady, step, setStep, completed, setCompleted, flags } =
- useOnboardingDetails();
-
- return { isReady, step, setStep, completed, setCompleted, flags };
-};
-
-export const useManualConnect = () => {
- const { connectViaIdAndSecret } = useDispatch( STORE_NAME );
+export const useOptionalPaymentMethods = () => {
+ const {
+ areOptionalPaymentMethodsEnabled,
+ setAreOptionalPaymentMethodsEnabled,
+ } = useHooks();
return {
- connectManual: connectViaIdAndSecret,
+ areOptionalPaymentMethodsEnabled,
+ setAreOptionalPaymentMethodsEnabled,
};
};
+
+export const useSteps = () => {
+ const { flags, isReady, step, setStep, completed, setCompleted } =
+ useHooks();
+
+ return { flags, isReady, step, setStep, completed, setCompleted };
+};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/index.js b/modules/ppcp-settings/resources/js/data/onboarding/index.js
index 0b07abf46..28c162f98 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/index.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/index.js
@@ -1,6 +1,24 @@
+import { createReduxStore, register } from '@wordpress/data';
+import { controls as wpControls } from '@wordpress/data-controls';
+
+import { STORE_NAME } from './constants';
import reducer from './reducer';
import * as selectors from './selectors';
import * as actions from './actions';
-import * as resolvers from './resolvers';
+import * as hooks from './hooks';
+import { resolvers } from './resolvers';
+import { controls } from './controls';
-export { reducer, selectors, actions, resolvers };
+export const initStore = () => {
+ const store = createReduxStore( STORE_NAME, {
+ reducer,
+ controls: { ...wpControls, ...controls },
+ actions,
+ selectors,
+ resolvers,
+ } );
+
+ register( store );
+};
+
+export { hooks, selectors, STORE_NAME };
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
index 5c1f59263..176d4875d 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/reducer.js
@@ -1,21 +1,19 @@
+/**
+ * Reducer: Defines store structure and state updates for this module.
+ *
+ * Manages both transient (temporary) and persistent (saved) state.
+ * The initial state must define all properties, as dynamic additions are not supported.
+ *
+ * @file
+ */
+
+import { createReducer, createSetters } from '../utils';
import ACTION_TYPES from './action-types';
-const defaultState = {
- isReady: false,
- isSaving: false,
- isManualConnectionBusy: false,
+// Store structure.
- // Data persisted to the server.
- data: {
- completed: false,
- step: 0,
- useSandbox: false,
- useManualConnection: false,
- clientId: '',
- clientSecret: '',
- isCasualSeller: null, // null value will uncheck both options in the UI.
- products: [],
- },
+const defaultTransient = {
+ isReady: false,
// Read only values, provided by the server.
flags: {
@@ -25,83 +23,41 @@ const defaultState = {
},
};
-export const onboardingReducer = (
- state = defaultState,
- { type, ...action }
-) => {
- const setTransient = ( changes ) => {
- const { data, ...transientChanges } = changes;
- return { ...state, ...transientChanges };
- };
-
- const setPersistent = ( changes ) => {
- const validChanges = Object.keys( changes ).reduce( ( acc, key ) => {
- if ( key in defaultState.data ) {
- acc[ key ] = changes[ key ];
- }
- return acc;
- }, {} );
-
- return {
- ...state,
- data: { ...state.data, ...validChanges },
- };
- };
-
- switch ( type ) {
- // Reset store to initial state.
- case ACTION_TYPES.RESET_ONBOARDING:
- return setPersistent( defaultState.data );
-
- // Transient data.
- case ACTION_TYPES.SET_ONBOARDING_IS_READY:
- return setTransient( { isReady: action.isReady } );
-
- case ACTION_TYPES.SET_IS_SAVING_ONBOARDING:
- return setTransient( { isSaving: action.isSaving } );
-
- case ACTION_TYPES.SET_MANUAL_CONNECTION_BUSY:
- return setTransient( { isManualConnectionBusy: action.isBusy } );
-
- // Persistent data.
- case ACTION_TYPES.SET_ONBOARDING_DETAILS:
- const newState = setPersistent( action.payload.data );
-
- if ( action.payload.flags ) {
- newState.flags = { ...newState.flags, ...action.payload.flags };
- }
-
- return newState;
-
- case ACTION_TYPES.SET_ONBOARDING_COMPLETED:
- return setPersistent( { completed: action.completed } );
-
- case ACTION_TYPES.SET_CLIENT_ID:
- return setPersistent( { clientId: action.clientId } );
-
- case ACTION_TYPES.SET_CLIENT_SECRET:
- return setPersistent( { clientSecret: action.clientSecret } );
-
- case ACTION_TYPES.SET_ONBOARDING_STEP:
- return setPersistent( { step: action.step } );
-
- case ACTION_TYPES.SET_SANDBOX_MODE:
- return setPersistent( { useSandbox: action.useSandbox } );
-
- case ACTION_TYPES.SET_MANUAL_CONNECTION_MODE:
- return setPersistent( {
- useManualConnection: action.useManualConnection,
- } );
-
- case ACTION_TYPES.SET_IS_CASUAL_SELLER:
- return setPersistent( { isCasualSeller: action.isCasualSeller } );
-
- case ACTION_TYPES.SET_PRODUCTS:
- return setPersistent( { products: action.products } );
-
- default:
- return state;
- }
+const defaultPersistent = {
+ completed: false,
+ step: 0,
+ isCasualSeller: null, // null value will uncheck both options in the UI.
+ areOptionalPaymentMethodsEnabled: true,
+ products: [],
};
+// Reducer logic.
+
+const [ setTransient, setPersistent ] = createSetters(
+ defaultTransient,
+ defaultPersistent
+);
+
+const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
+ [ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
+ setTransient( state, payload ),
+
+ [ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
+ setPersistent( state, payload ),
+
+ [ ACTION_TYPES.RESET ]: ( state ) =>
+ setPersistent( state, defaultPersistent ),
+
+ [ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
+ const newState = setPersistent( state, payload.data );
+
+ // Flags are not updated by `setPersistent()`.
+ if ( payload.flags ) {
+ newState.flags = { ...newState.flags, ...payload.flags };
+ }
+
+ return newState;
+ },
+} );
+
export default onboardingReducer;
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js
index 18f2a7528..bf7828dd3 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/resolvers.js
@@ -1,25 +1,36 @@
+/**
+ * Resolvers: Handle asynchronous data fetching for the store.
+ *
+ * These functions update store state with data from external sources.
+ * Each resolver corresponds to a specific selector (selector with same name must exist).
+ * Resolvers are called automatically when selectors request unavailable data.
+ *
+ * @file
+ */
+
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
-import { NAMESPACE } from '../constants';
-import { setIsReady, setOnboardingDetails } from './actions';
-/**
- * Retrieve settings from the site's REST API.
- */
-export function* getPersistentData() {
- const path = `${ NAMESPACE }/onboarding`;
+import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
- try {
- const result = yield apiFetch( { path } );
- yield setOnboardingDetails( result );
- yield setIsReady( true );
- } catch ( e ) {
- yield dispatch( 'core/notices' ).createErrorNotice(
- __(
- 'Error retrieving onboarding details.',
- 'woocommerce-paypal-payments'
- )
- );
- }
-}
+export const resolvers = {
+ /**
+ * Retrieve settings from the site's REST API.
+ */
+ *persistentData() {
+ try {
+ const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
+
+ yield dispatch( STORE_NAME ).hydrate( result );
+ yield dispatch( STORE_NAME ).setIsReady( true );
+ } catch ( e ) {
+ yield dispatch( 'core/notices' ).createErrorNotice(
+ __(
+ 'Error retrieving onboarding details.',
+ 'woocommerce-paypal-payments'
+ )
+ );
+ }
+ },
+};
diff --git a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
index b7721b992..d4d57ef4d 100644
--- a/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
+++ b/modules/ppcp-settings/resources/js/data/onboarding/selectors.js
@@ -1,22 +1,25 @@
+/**
+ * Selectors: Extract specific pieces of state from the store.
+ *
+ * These functions provide a consistent interface for accessing store data.
+ * They allow components to retrieve data without knowing the store structure.
+ *
+ * @file
+ */
+
const EMPTY_OBJ = Object.freeze( {} );
-const getOnboardingState = ( state ) => {
- if ( ! state ) {
- return EMPTY_OBJ;
- }
+const getState = ( state ) => state || EMPTY_OBJ;
- return state.onboarding || EMPTY_OBJ;
+export const persistentData = ( state ) => {
+ return getState( state ).data || EMPTY_OBJ;
};
-export const getPersistentData = ( state ) => {
- return getOnboardingState( state ).data || EMPTY_OBJ;
-};
-
-export const getTransientData = ( state ) => {
- const { data, flags, ...transientState } = getOnboardingState( state );
+export const transientData = ( state ) => {
+ const { data, flags, ...transientState } = getState( state );
return transientState || EMPTY_OBJ;
};
-export const getFlags = ( state ) => {
- return getOnboardingState( state ).flags || EMPTY_OBJ;
+export const flags = ( state ) => {
+ return getState( state ).flags || EMPTY_OBJ;
};
diff --git a/modules/ppcp-settings/resources/js/data/store.js b/modules/ppcp-settings/resources/js/data/store.js
deleted file mode 100644
index a4acaf548..000000000
--- a/modules/ppcp-settings/resources/js/data/store.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { createReduxStore, register, combineReducers } from '@wordpress/data';
-import { controls } from '@wordpress/data-controls';
-import { STORE_NAME } from './constants';
-import * as onboarding from './onboarding';
-
-const actions = {};
-const selectors = {};
-const resolvers = {};
-
-[ onboarding ].forEach( ( item ) => {
- Object.assign( actions, { ...item.actions } );
- Object.assign( selectors, { ...item.selectors } );
- Object.assign( resolvers, { ...item.resolvers } );
-} );
-
-const reducer = combineReducers( {
- onboarding: onboarding.reducer,
-} );
-
-export const initStore = () => {
- const store = createReduxStore( STORE_NAME, {
- reducer,
- controls,
- actions,
- selectors,
- resolvers,
- } );
-
- register( store );
-
- /* eslint-disable no-console */
- // Provide a debug tool to inspect the Redux store via the JS console.
- if ( window.ppcpSettings?.debug && console?.groupCollapsed ) {
- window.ppcpSettings.dumpStore = () => {
- const storeSelector = `wp.data.select('${ STORE_NAME }')`;
- console.group( `[STORE] ${ storeSelector }` );
-
- const storeState = wp.data.select( STORE_NAME );
- Object.keys( selectors ).forEach( ( selector ) => {
- console.groupCollapsed( `[SELECTOR] .${ selector }()` );
- console.table( storeState[ selector ]() );
- console.groupEnd();
- } );
-
- console.groupEnd();
- };
- window.ppcpSettings.resetStore = () => {
- wp.data.dispatch( STORE_NAME ).resetOnboarding();
- wp.data.dispatch( STORE_NAME ).persist();
- };
- window.ppcpSettings.startOnboarding = () => {
- wp.data.dispatch( STORE_NAME ).setCompleted( false );
- wp.data.dispatch( STORE_NAME ).setOnboardingStep( 0 );
- wp.data.dispatch( STORE_NAME ).persist();
- };
- }
- /* eslint-enable no-console */
-};
diff --git a/modules/ppcp-settings/resources/js/data/utils.js b/modules/ppcp-settings/resources/js/data/utils.js
new file mode 100644
index 000000000..45c652862
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/data/utils.js
@@ -0,0 +1,75 @@
+/**
+ * Updates an object with new values, filtering based on allowed keys.
+ *
+ * Helper method used by createSetters.
+ *
+ * @param {Object} oldObject The original object to update.
+ * @param {Object} newValues The new values to apply.
+ * @param {Object} allowedKeys An object whose keys define the allowed keys to update.
+ * @return {Object} A new object with the allowed updates applied.
+ */
+const updateObject = ( oldObject, newValues, allowedKeys = {} ) => ( {
+ ...oldObject,
+ ...Object.keys( newValues ).reduce( ( acc, key ) => {
+ if ( key in allowedKeys ) {
+ acc[ key ] = newValues[ key ];
+ }
+ return acc;
+ }, {} ),
+} );
+
+/**
+ * Creates setter functions for updating state.
+ *
+ * Only properties that are present in the "defaultTransient" or "defaultPersistent"
+ * arguments can be updated by the setters. Make sure that the default state defines
+ * ALL possible properties.
+ *
+ * @param {Object} defaultTransient Object defining initial transient values.
+ * @param {Object} defaultPersistent Object defining initial persistent values.
+ * @return {[Function, Function]} An array containing setTransient and setPersistent functions.
+ */
+export const createSetters = ( defaultTransient, defaultPersistent ) => {
+ const setTransient = ( oldState, newValues = {} ) =>
+ updateObject( oldState, newValues, defaultTransient );
+
+ const setPersistent = ( oldState, newValues = {} ) => ( {
+ ...oldState,
+ data: updateObject( oldState.data, newValues, defaultPersistent ),
+ } );
+
+ return [ setTransient, setPersistent ];
+};
+
+/**
+ * Creates a reducer function with predefined action handlers.
+ *
+ * @param {Object} defaultTransient Object defining initial transient values.
+ * @param {Object} defaultPersistent Object defining initial persistent values.
+ * @param {Object} handlers An object mapping action types to handler functions.
+ * @return {Function} A reducer function.
+ */
+export const createReducer = (
+ defaultTransient,
+ defaultPersistent,
+ handlers
+) => {
+ if ( Object.hasOwnProperty.call( defaultTransient, 'data' ) ) {
+ throw new Error(
+ 'The transient state cannot contain a "data" property.'
+ );
+ }
+
+ const initialState = {
+ ...defaultTransient,
+ data: defaultPersistent,
+ };
+
+ return function reducer( state = initialState, action ) {
+ if ( Object.hasOwnProperty.call( handlers, action.type ) ) {
+ return handlers[ action.type ]( state, action.payload ?? {} );
+ }
+
+ return state;
+ };
+};
diff --git a/modules/ppcp-settings/resources/js/utils/window.js b/modules/ppcp-settings/resources/js/utils/window.js
new file mode 100644
index 000000000..165874302
--- /dev/null
+++ b/modules/ppcp-settings/resources/js/utils/window.js
@@ -0,0 +1,42 @@
+/**
+ * Opens the provided URL, preferably in a popup window.
+ *
+ * Popups are usually only supported on desktop devices, when the browser is not in fullscreen mode.
+ *
+ * @param {string} url
+ * @param {Object} options
+ * @param {string} options.name
+ * @param {number} options.width
+ * @param {number} options.height
+ * @param {boolean} options.resizeable
+ * @return {null|Window} Popup window instance, or null.
+ */
+export const openPopup = (
+ url,
+ { name = '_blank', width = 450, height = 720, resizeable = false } = {}
+) => {
+ width = Math.max( 100, Math.min( window.screen.width - 40, width ) );
+ height = Math.max( 100, Math.min( window.screen.height - 40, height ) );
+
+ const left = ( window.screen.width - width ) / 2;
+ const top = ( window.screen.height - height ) / 2;
+
+ const features = [
+ `width=${ width }`,
+ `height=${ height }`,
+ `left=${ left }`,
+ `top=${ top }`,
+ `resizable=${ resizeable ? 'yes' : 'no' }`,
+ `scrollbars=yes`,
+ `status=no`,
+ ];
+
+ const popup = window.open( url, name, features.join( ',' ) );
+
+ if ( popup && ! popup.closed ) {
+ popup.focus();
+ return popup;
+ }
+
+ return null;
+};
diff --git a/modules/ppcp-settings/services.php b/modules/ppcp-settings/services.php
index 20ea96b09..d213aa4c0 100644
--- a/modules/ppcp-settings/services.php
+++ b/modules/ppcp-settings/services.php
@@ -9,11 +9,17 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
-use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
+use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
+use WooCommerce\PayPalCommerce\Settings\Data\CommonSettings;
+use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\OnboardingProfile;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\LoginLinkRestEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Service\ConnectionUrlGenerator;
+use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'settings.url' => static function ( ContainerInterface $container ) : string {
@@ -44,14 +50,29 @@ return array(
$can_use_card_payments
);
},
+ 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
+ return new GeneralSettings();
+ },
+ 'settings.data.common' => static function ( ContainerInterface $container ) : CommonSettings {
+ return new CommonSettings();
+ },
'settings.rest.onboarding' => static function ( ContainerInterface $container ) : OnboardingRestEndpoint {
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
},
+ 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint {
+ return new CommonRestEndpoint( $container->get( 'settings.data.common' ) );
+ },
'settings.rest.connect_manual' => static function ( ContainerInterface $container ) : ConnectManualRestEndpoint {
return new ConnectManualRestEndpoint(
$container->get( 'api.paypal-host-production' ),
$container->get( 'api.paypal-host-sandbox' ),
- $container->get( 'woocommerce.logger.woocommerce' )
+ $container->get( 'woocommerce.logger.woocommerce' ),
+ $container->get( 'settings.data.general' )
+ );
+ },
+ 'settings.rest.login_link' => static function ( ContainerInterface $container ) : LoginLinkRestEndpoint {
+ return new LoginLinkRestEndpoint(
+ $container->get( 'settings.service.connection-url-generators' ),
);
},
'settings.casual-selling.supported-countries' => static function ( ContainerInterface $container ) : array {
@@ -110,6 +131,35 @@ return array(
return in_array( $country, $eligible_countries, true );
},
+ 'settings.service.signup-link-cache' => static function ( ContainerInterface $container ) : Cache {
+ return new Cache( 'ppcp-paypal-signup-link' );
+ },
+ 'settings.service.connection-url-generators' => static function ( ContainerInterface $container ) : array {
+ // Define available environments.
+ $environments = array(
+ 'production' => array(
+ 'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-production' ),
+ ),
+ 'sandbox' => array(
+ 'partner_referrals' => $container->get( 'api.endpoint.partner-referrals-sandbox' ),
+ ),
+ );
+
+ $generators = array();
+
+ // Instantiate URL generators for each environment.
+ foreach ( $environments as $environment => $config ) {
+ $generators[ $environment ] = new ConnectionUrlGenerator(
+ $config['partner_referrals'],
+ $container->get( 'api.repository.partner-referrals-data' ),
+ $container->get( 'settings.service.signup-link-cache' ),
+ $environment,
+ $container->get( 'woocommerce.logger.woocommerce' )
+ );
+ }
+
+ return $generators;
+ },
'settings.switch-ui.endpoint' => static function ( ContainerInterface $container ) : SwitchSettingsUiEndpoint {
return new SwitchSettingsUiEndpoint(
$container->get( 'woocommerce.logger.woocommerce' ),
diff --git a/modules/ppcp-settings/src/Data/CommonSettings.php b/modules/ppcp-settings/src/Data/CommonSettings.php
new file mode 100644
index 000000000..8f7dd1ddf
--- /dev/null
+++ b/modules/ppcp-settings/src/Data/CommonSettings.php
@@ -0,0 +1,119 @@
+ false,
+ 'use_manual_connection' => false,
+ 'client_id' => '',
+ 'client_secret' => '',
+ );
+ }
+
+ // -----
+
+ /**
+ * Gets the 'use sandbox' setting.
+ *
+ * @return bool
+ */
+ public function get_sandbox() : bool {
+ return (bool) $this->data['use_sandbox'];
+ }
+
+ /**
+ * Sets the 'use sandbox' setting.
+ *
+ * @param bool $use_sandbox Whether to use sandbox mode.
+ */
+ public function set_sandbox( bool $use_sandbox ) : void {
+ $this->data['use_sandbox'] = $use_sandbox;
+ }
+
+ /**
+ * Gets the 'use manual connection' setting.
+ *
+ * @return bool
+ */
+ public function get_manual_connection() : bool {
+ return (bool) $this->data['use_manual_connection'];
+ }
+
+ /**
+ * Sets the 'use manual connection' setting.
+ *
+ * @param bool $use_manual_connection Whether to use manual connection.
+ */
+ public function set_manual_connection( bool $use_manual_connection ) : void {
+ $this->data['use_manual_connection'] = $use_manual_connection;
+ }
+
+ /**
+ * Gets the client ID.
+ *
+ * @return string
+ */
+ public function get_client_id() : string {
+ return $this->data['client_id'];
+ }
+
+ /**
+ * Sets the client ID.
+ *
+ * @param string $client_id The client ID.
+ */
+ public function set_client_id( string $client_id ) : void {
+ $this->data['client_id'] = sanitize_text_field( $client_id );
+ }
+
+ /**
+ * Gets the client secret.
+ *
+ * @return string
+ */
+ public function get_client_secret() : string {
+ return $this->data['client_secret'];
+ }
+
+ /**
+ * Sets the client secret.
+ *
+ * @param string $client_secret The client secret.
+ */
+ public function set_client_secret( string $client_secret ) : void {
+ $this->data['client_secret'] = sanitize_text_field( $client_secret );
+ }
+}
diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php
index e1f9e16b4..b04d7879f 100644
--- a/modules/ppcp-settings/src/Data/OnboardingProfile.php
+++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php
@@ -64,14 +64,11 @@ class OnboardingProfile extends AbstractDataModel {
*/
protected function get_defaults() : array {
return array(
- 'completed' => false,
- 'step' => 0,
- 'use_sandbox' => false,
- 'use_manual_connection' => false,
- 'client_id' => '',
- 'client_secret' => '',
- 'is_casual_seller' => null,
- 'products' => array(),
+ 'completed' => false,
+ 'step' => 0,
+ 'is_casual_seller' => null,
+ 'are_optional_payment_methods_enabled' => true,
+ 'products' => array(),
);
}
@@ -113,78 +110,6 @@ class OnboardingProfile extends AbstractDataModel {
$this->data['step'] = $step;
}
- /**
- * Gets the 'use sandbox' setting.
- *
- * @return bool
- */
- public function get_sandbox() : bool {
- return (bool) $this->data['use_sandbox'];
- }
-
- /**
- * Sets the 'use sandbox' setting.
- *
- * @param bool $use_sandbox Whether to use sandbox mode.
- */
- public function set_sandbox( bool $use_sandbox ) : void {
- $this->data['use_sandbox'] = $use_sandbox;
- }
-
- /**
- * Gets the 'use manual connection' setting.
- *
- * @return bool
- */
- public function get_manual_connection() : bool {
- return (bool) $this->data['use_manual_connection'];
- }
-
- /**
- * Sets the 'use manual connection' setting.
- *
- * @param bool $use_manual_connection Whether to use manual connection.
- */
- public function set_manual_connection( bool $use_manual_connection ) : void {
- $this->data['use_manual_connection'] = $use_manual_connection;
- }
-
- /**
- * Gets the client ID.
- *
- * @return string
- */
- public function get_client_id() : string {
- return $this->data['client_id'];
- }
-
- /**
- * Sets the client ID.
- *
- * @param string $client_id The client ID.
- */
- public function set_client_id( string $client_id ) : void {
- $this->data['client_id'] = sanitize_text_field( $client_id );
- }
-
- /**
- * Gets the client secret.
- *
- * @return string
- */
- public function get_client_secret() : string {
- return $this->data['client_secret'];
- }
-
- /**
- * Sets the client secret.
- *
- * @param string $client_secret The client secret.
- */
- public function set_client_secret( string $client_secret ) : void {
- $this->data['client_secret'] = sanitize_text_field( $client_secret );
- }
-
/**
* Gets the casual seller flag.
*
@@ -203,6 +128,15 @@ class OnboardingProfile extends AbstractDataModel {
$this->data['is_casual_seller'] = $casual_seller;
}
+ /**
+ * Sets the optional payment methods flag.
+ *
+ * @param bool|null $are_optional_payment_methods_enabled Whether the PayPal optional payment methods are enabled.
+ */
+ public function set_are_optional_payment_methods_enabled( ?bool $are_optional_payment_methods_enabled ) : void {
+ $this->data['are_optional_payment_methods_enabled'] = $are_optional_payment_methods_enabled;
+ }
+
/**
* Gets the active product types for this store.
*
diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
new file mode 100644
index 000000000..c7345148e
--- /dev/null
+++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php
@@ -0,0 +1,133 @@
+ array(
+ 'js_name' => 'useSandbox',
+ 'sanitize' => 'to_boolean',
+ ),
+ 'use_manual_connection' => array(
+ 'js_name' => 'useManualConnection',
+ 'sanitize' => 'to_boolean',
+ ),
+ 'client_id' => array(
+ 'js_name' => 'clientId',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ 'client_secret' => array(
+ 'js_name' => 'clientSecret',
+ 'sanitize' => 'sanitize_text_field',
+ ),
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param CommonSettings $settings The settings instance.
+ */
+ public function __construct( CommonSettings $settings ) {
+ $this->settings = $settings;
+ }
+
+ /**
+ * Configure REST API routes.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_details' ),
+ 'permission_callback' => array( $this, 'check_permission' ),
+ ),
+ )
+ );
+
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'update_details' ),
+ 'permission_callback' => array( $this, 'check_permission' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns all common details from the DB.
+ *
+ * @return WP_REST_Response The common settings.
+ */
+ public function get_details() : WP_REST_Response {
+ $js_data = $this->sanitize_for_javascript(
+ $this->settings->to_array(),
+ $this->field_map
+ );
+
+ return $this->return_success( $js_data );
+ }
+
+ /**
+ * Updates common details based on the request.
+ *
+ * @param WP_REST_Request $request Full data about the request.
+ *
+ * @return WP_REST_Response The new common settings.
+ */
+ public function update_details( WP_REST_Request $request ) : WP_REST_Response {
+ $wp_data = $this->sanitize_for_wordpress(
+ $request->get_params(),
+ $this->field_map
+ );
+
+ $this->settings->from_array( $wp_data );
+ $this->settings->save();
+
+ return $this->get_details();
+ }
+}
diff --git a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php
index af81b90ca..7046342a2 100644
--- a/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/ConnectManualRestEndpoint.php
@@ -10,17 +10,16 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use Exception;
-use Psr\Log\LoggerInterface;
-use RuntimeException;
use stdClass;
+use RuntimeException;
+use Psr\Log\LoggerInterface;
+use WP_REST_Request;
+use WP_REST_Response;
+use WP_REST_Server;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Helper\InMemoryCache;
-use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
-use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
-use WP_REST_Server;
-use WP_REST_Response;
-use WP_REST_Request;
+use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
/**
* REST controller for connection via manual credentials input.
@@ -55,6 +54,13 @@ class ConnectManualRestEndpoint extends RestEndpoint {
*/
protected $rest_base = 'connect_manual';
+ /**
+ * Settings instance.
+ *
+ * @var GeneralSettings
+ */
+ private $settings = null;
+
/**
* Field mapping for request.
*
@@ -78,19 +84,21 @@ class ConnectManualRestEndpoint extends RestEndpoint {
/**
* ConnectManualRestEndpoint constructor.
*
- * @param string $live_host The API host for the live mode.
+ * @param string $live_host The API host for the live mode.
* @param string $sandbox_host The API host for the sandbox mode.
- * @param LoggerInterface $logger The logger.
+ * @param LoggerInterface $logger The logger.
+ * @param GeneralSettings $settings Settings instance.
*/
public function __construct(
string $live_host,
string $sandbox_host,
- LoggerInterface $logger
+ LoggerInterface $logger,
+ GeneralSettings $settings
) {
-
$this->live_host = $live_host;
$this->sandbox_host = $sandbox_host;
$this->logger = $logger;
+ $this->settings = $settings;
}
/**
@@ -126,47 +134,52 @@ class ConnectManualRestEndpoint extends RestEndpoint {
$use_sandbox = (bool) ( $data['use_sandbox'] ?? false );
if ( empty( $client_id ) || empty( $client_secret ) ) {
- return rest_ensure_response(
- array(
- 'success' => false,
- 'message' => 'No client ID or secret provided.',
- )
- );
+ return $this->return_error( 'No client ID or secret provided.' );
}
try {
$payee = $this->request_payee( $client_id, $client_secret, $use_sandbox );
} catch ( Exception $exception ) {
- return rest_ensure_response(
- array(
- 'success' => false,
- 'message' => $exception->getMessage(),
- )
- );
-
+ return $this->return_error( $exception->getMessage() );
}
- $result = array(
- 'merchantId' => $payee->merchant_id,
- 'email' => $payee->email_address,
- 'success' => true,
- );
+ if ( $use_sandbox ) {
+ $this->settings->set_is_sandbox( true );
+ $this->settings->set_sandbox_client_id( $client_id );
+ $this->settings->set_sandbox_client_secret( $client_secret );
+ $this->settings->set_sandbox_merchant_id( $payee->merchant_id );
+ $this->settings->set_sandbox_merchant_email( $payee->email_address );
+ } else {
+ $this->settings->set_is_sandbox( false );
+ $this->settings->set_live_client_id( $client_id );
+ $this->settings->set_live_client_secret( $client_secret );
+ $this->settings->set_live_merchant_id( $payee->merchant_id );
+ $this->settings->set_live_merchant_email( $payee->email_address );
+ }
+ $this->settings->save();
- return rest_ensure_response( $result );
+ return $this->return_success(
+ array(
+ 'merchantId' => $payee->merchant_id,
+ 'email' => $payee->email_address,
+ )
+ );
}
/**
* Retrieves the payee object with the merchant data
* by creating a minimal PayPal order.
*
- * @param string $client_id The client ID.
- * @param string $client_secret The client secret.
- * @param bool $use_sandbox Whether to use the sandbox mode.
- * @return stdClass The payee object.
* @throws Exception When failed to retrieve payee.
*
* phpcs:disable Squiz.Commenting
* phpcs:disable Generic.Commenting
+ *
+ * @param string $client_secret The client secret.
+ * @param bool $use_sandbox Whether to use the sandbox mode.
+ * @param string $client_id The client ID.
+ *
+ * @return stdClass The payee object.
*/
private function request_payee(
string $client_id,
@@ -176,24 +189,13 @@ class ConnectManualRestEndpoint extends RestEndpoint {
$host = $use_sandbox ? $this->sandbox_host : $this->live_host;
- $empty_settings = new class() implements ContainerInterface
- {
- public function get( string $id ) {
- throw new NotFoundException();
- }
-
- public function has( string $id ) {
- return false;
- }
- };
-
$bearer = new PayPalBearer(
new InMemoryCache(),
$host,
$client_id,
$client_secret,
$this->logger,
- $empty_settings
+ null
);
$orders = new Orders(
diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php
new file mode 100644
index 000000000..8ed204383
--- /dev/null
+++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php
@@ -0,0 +1,105 @@
+url_generators = $url_generators;
+ }
+
+ /**
+ * Configure REST API routes.
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'callback' => array( $this, 'get_login_url' ),
+ 'permission_callback' => array( $this, 'check_permission' ),
+ 'args' => array(
+ 'environment' => array(
+ 'required' => true,
+ 'type' => 'string',
+ ),
+ 'products' => array(
+ 'required' => true,
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ ),
+ 'sanitize_callback' => function ( $products ) {
+ return array_map( 'sanitize_text_field', $products );
+ },
+ ),
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Returns the full login URL for the requested environment and products.
+ *
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return WP_REST_Response The login URL or an error response.
+ */
+ public function get_login_url( WP_REST_Request $request ) : WP_REST_Response {
+ $environment = $request->get_param( 'environment' );
+ $products = $request->get_param( 'products' );
+
+ if ( ! isset( $this->url_generators[ $environment ] ) ) {
+ return new WP_REST_Response(
+ array( 'error' => 'Invalid environment specified.' ),
+ 400
+ );
+ }
+
+ $url_generator = $this->url_generators[ $environment ];
+
+ try {
+ $url = $url_generator->generate( $products );
+
+ return $this->return_success( $url );
+ } catch ( \Exception $e ) {
+ return $this->return_error( $e->getMessage() );
+ }
+ }
+}
diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
index 6c59b1622..0fbb9fcf9 100644
--- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php
@@ -41,35 +41,23 @@ class OnboardingRestEndpoint extends RestEndpoint {
* @var array
*/
private array $field_map = array(
- 'completed' => array(
+ 'completed' => array(
'js_name' => 'completed',
'sanitize' => 'to_boolean',
),
- 'step' => array(
+ 'step' => array(
'js_name' => 'step',
'sanitize' => 'to_number',
),
- 'use_sandbox' => array(
- 'js_name' => 'useSandbox',
- 'sanitize' => 'to_boolean',
- ),
- 'use_manual_connection' => array(
- 'js_name' => 'useManualConnection',
- 'sanitize' => 'to_boolean',
- ),
- 'client_id' => array(
- 'js_name' => 'clientId',
- 'sanitize' => 'sanitize_text_field',
- ),
- 'client_secret' => array(
- 'js_name' => 'clientSecret',
- 'sanitize' => 'sanitize_text_field',
- ),
- 'is_casual_seller' => array(
+ 'is_casual_seller' => array(
'js_name' => 'isCasualSeller',
'sanitize' => 'to_boolean',
),
- 'products' => array(
+ 'are_optional_payment_methods_enabled' => array(
+ 'js_name' => 'areOptionalPaymentMethodsEnabled',
+ 'sanitize' => 'to_boolean',
+ ),
+ 'products' => array(
'js_name' => 'products',
),
);
@@ -147,9 +135,9 @@ class OnboardingRestEndpoint extends RestEndpoint {
$this->flag_map
);
- return rest_ensure_response(
+ return $this->return_success(
+ $js_data,
array(
- 'data' => $js_data,
'flags' => $js_flags,
)
);
diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php
index 08191276b..76626ac0c 100644
--- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php
@@ -10,14 +10,12 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Endpoint;
use WC_REST_Controller;
+use WP_REST_Response;
/**
* Base class for REST controllers in the settings module.
- *
- * This is a base class for specific REST endpoints; do not instantiate this
- * class directly.
*/
-class RestEndpoint extends WC_REST_Controller {
+abstract class RestEndpoint extends WC_REST_Controller {
/**
* Endpoint namespace.
*
@@ -34,6 +32,54 @@ class RestEndpoint extends WC_REST_Controller {
return current_user_can( 'manage_woocommerce' );
}
+ /**
+ * Returns a successful REST API response.
+ *
+ * @param mixed $data The main response data.
+ * @param array $extra Optional, additional response data.
+ *
+ * @return WP_REST_Response The successful response.
+ */
+ protected function return_success( $data, array $extra = array() ) : WP_REST_Response {
+ $response = array(
+ 'success' => true,
+ 'data' => $data,
+ );
+
+ if ( $extra ) {
+ foreach ( $extra as $key => $value ) {
+ if ( isset( $response[ $key ] ) ) {
+ continue;
+ }
+
+ $response[ $key ] = $value;
+ }
+ }
+
+ return rest_ensure_response( $response );
+ }
+
+ /**
+ * Returns an error REST API response.
+ *
+ * @param string $reason The reason for the error.
+ * @param mixed $details Optional details about the error.
+ *
+ * @return WP_REST_Response The error response.
+ */
+ protected function return_error( string $reason, $details = null ) : WP_REST_Response {
+ $response = array(
+ 'success' => false,
+ 'message' => $reason,
+ );
+
+ if ( ! is_null( $details ) ) {
+ $response['details'] = $details;
+ }
+
+ return rest_ensure_response( $response );
+ }
+
/**
* Sanitizes parameters based on a field mapping.
*
diff --git a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php b/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php
index ebc85d9dc..244c26dfe 100644
--- a/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php
+++ b/modules/ppcp-settings/src/Endpoint/SwitchSettingsUiEndpoint.php
@@ -15,6 +15,8 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class SwitchSettingsUiEndpoint
+ *
+ * Note: This is an ajax handler, not a REST endpoint
*/
class SwitchSettingsUiEndpoint {
diff --git a/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
new file mode 100644
index 000000000..6e91aba3a
--- /dev/null
+++ b/modules/ppcp-settings/src/Service/ConnectionUrlGenerator.php
@@ -0,0 +1,227 @@
+partner_referrals = $partner_referrals;
+ $this->referrals_data = $referrals_data;
+ $this->cache = $cache;
+ $this->environment = $environment;
+ $this->logger = $logger ?: new NullLogger();
+ }
+
+ /**
+ * Returns the environment for which the URL is being generated.
+ *
+ * @return string
+ */
+ public function environment() : string {
+ return $this->environment;
+ }
+
+ /**
+ * Generates a PayPal onboarding URL for merchant sign-up.
+ *
+ * This function creates a URL for merchants to sign up for PayPal services.
+ * It handles caching of the URL, generation of new URLs when necessary,
+ * and works for both production and sandbox environments.
+ *
+ * @param array $products An array of product identifiers to include in the sign-up process.
+ * These determine the PayPal onboarding experience.
+ *
+ * @return string The generated PayPal onboarding URL.
+ */
+ public function generate( array $products = array() ) : string {
+ $cache_key = $this->cache_key( $products );
+ $user_id = get_current_user_id();
+ $onboarding_url = new OnboardingUrl( $this->cache, $cache_key, $user_id );
+ $cached_url = $this->try_get_from_cache( $onboarding_url, $cache_key );
+
+ if ( $cached_url ) {
+ $this->logger->info( 'Using cached onboarding URL for: ' . $cache_key );
+
+ return $cached_url;
+ }
+
+ $this->logger->info( 'Generating onboarding URL for: ' . $cache_key );
+
+ $url = $this->generate_new_url( $products, $onboarding_url, $cache_key );
+
+ if ( $url ) {
+ $this->persist_url( $onboarding_url, $url );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Generates a cache key from the environment and sorted product array.
+ *
+ * @param array $products Product identifiers that are part of the cache key.
+ *
+ * @return string The cache key, defining the product list and environment.
+ */
+ protected function cache_key( array $products = array() ) : string {
+ // Sort products alphabetically, to improve cache implementation.
+ sort( $products );
+
+ return $this->environment() . '-' . implode( '-', $products );
+ }
+
+ /**
+ * Attempts to load the URL from cache.
+ *
+ * @param OnboardingUrl $onboarding_url The OnboardingUrl object.
+ * @param string $cache_key The cache key.
+ *
+ * @return string The cached URL, or an empty string if no URL is found.
+ */
+ protected function try_get_from_cache( OnboardingUrl $onboarding_url, string $cache_key ) : string {
+ try {
+ if ( $onboarding_url->load() ) {
+ $this->logger->debug( 'Loaded onboarding URL from cache: ' . $cache_key );
+
+ return $onboarding_url->get();
+ }
+ } catch ( Exception $e ) {
+ // No problem, return an empty string to generate a new URL.
+ $this->logger->warning( 'Failed to load onboarding URL from cache: ' . $cache_key );
+ }
+
+ return '';
+ }
+
+ /**
+ * Generates a new URL.
+ *
+ * @param array $products The products array.
+ * @param OnboardingUrl $onboarding_url The OnboardingUrl object.
+ * @param string $cache_key The cache key.
+ *
+ * @return string The generated URL or an empty string on failure.
+ */
+ protected function generate_new_url( array $products, OnboardingUrl $onboarding_url, string $cache_key ) : string {
+ $query_args = array( 'displayMode' => 'minibrowser' );
+ $onboarding_url->init();
+
+ try {
+ $onboarding_token = $onboarding_url->token();
+ } catch ( Exception $e ) {
+ $this->logger->warning( 'Could not generate an onboarding token for: ' . $cache_key );
+
+ return '';
+ }
+
+ $data = $this->prepare_referral_data( $products, $onboarding_token );
+
+ try {
+ $url = $this->partner_referrals->signup_link( $data );
+ } catch ( Exception $e ) {
+ $this->logger->warning( 'Could not generate an onboarding URL for: ' . $cache_key );
+
+ return '';
+ }
+
+ return add_query_arg( $query_args, $url );
+ }
+
+ /**
+ * Prepares the referral data.
+ *
+ * @param array $products The products array.
+ * @param string $onboarding_token The onboarding token.
+ *
+ * @return array The prepared referral data.
+ */
+ protected function prepare_referral_data( array $products, string $onboarding_token ) : array {
+ $data = $this->referrals_data
+ ->with_products( $products )
+ ->data();
+
+ return $this->referrals_data->append_onboarding_token( $data, $onboarding_token );
+ }
+
+ /**
+ * Persists the generated URL.
+ *
+ * @param OnboardingUrl $onboarding_url The OnboardingUrl object.
+ * @param string $url The URL to persist.
+ */
+ protected function persist_url( OnboardingUrl $onboarding_url, string $url ) : void {
+ $onboarding_url->set( $url );
+ $onboarding_url->persist();
+ }
+}
diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php
index f0c3770f3..7c9dca2f8 100644
--- a/modules/ppcp-settings/src/SettingsModule.php
+++ b/modules/ppcp-settings/src/SettingsModule.php
@@ -9,8 +9,7 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\ConnectManualRestEndpoint;
-use WooCommerce\PayPalCommerce\Settings\Endpoint\OnboardingRestEndpoint;
+use WooCommerce\PayPalCommerce\Settings\Endpoint\RestEndpoint;
use WooCommerce\PayPalCommerce\Settings\Endpoint\SwitchSettingsUiEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
@@ -26,7 +25,7 @@ class SettingsModule implements ServiceModule, ExecutableModule {
/**
* Returns whether the old settings UI should be loaded.
*/
- public static function should_use_the_old_ui(): bool {
+ public static function should_use_the_old_ui() : bool {
return apply_filters(
'woocommerce_paypal_payments_should_use_the_old_ui',
(bool) get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === true
@@ -89,7 +88,13 @@ class SettingsModule implements ServiceModule, ExecutableModule {
$endpoint = $container->get( 'settings.switch-ui.endpoint' );
assert( $endpoint instanceof SwitchSettingsUiEndpoint );
- add_action( 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT, array( $endpoint, 'handle_request' ) );
+ add_action(
+ 'wc_ajax_' . SwitchSettingsUiEndpoint::ENDPOINT,
+ array(
+ $endpoint,
+ 'handle_request',
+ )
+ );
return true;
}
@@ -170,13 +175,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
add_action(
'rest_api_init',
static function () use ( $container ) : void {
- $onboarding_endpoint = $container->get( 'settings.rest.onboarding' );
- assert( $onboarding_endpoint instanceof OnboardingRestEndpoint );
- $onboarding_endpoint->register_routes();
+ $endpoints = array(
+ $container->get( 'settings.rest.onboarding' ),
+ $container->get( 'settings.rest.common' ),
+ $container->get( 'settings.rest.connect_manual' ),
+ $container->get( 'settings.rest.login_link' ),
+ );
- $connect_manual_endpoint = $container->get( 'settings.rest.connect_manual' );
- assert( $connect_manual_endpoint instanceof ConnectManualRestEndpoint );
- $connect_manual_endpoint->register_routes();
+ foreach ( $endpoints as $endpoint ) {
+ assert( $endpoint instanceof RestEndpoint );
+ $endpoint->register_routes();
+ }
}
);
diff --git a/yarn.lock b/yarn.lock
index 8d605e524..f808c687c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2671,9 +2671,9 @@
mime "^3.0.0"
web-vitals "^4.2.1"
-"@wordpress/element@^6.1.0":
+"@wordpress/element@*", "@wordpress/element@^6.1.0":
version "6.11.0"
- resolved "https://registry.npmjs.org/@wordpress/element/-/element-6.11.0.tgz"
+ resolved "https://registry.yarnpkg.com/@wordpress/element/-/element-6.11.0.tgz#7bc3e453a95bb806a707b4dc617373afa108af19"
integrity sha512-UvHFYkT+EEaXEyEfw+iqLHRO9OwBjjsUydEMHcqntzkNcsYeAbmaL9V8R9ikXHLe6ftdbkwoXgF85xVPhVsL+Q==
dependencies:
"@babel/runtime" "7.25.7"
@@ -2715,6 +2715,15 @@
globals "^13.12.0"
requireindex "^1.2.0"
+"@wordpress/icons@^10.11.0":
+ version "10.11.0"
+ resolved "https://registry.yarnpkg.com/@wordpress/icons/-/icons-10.11.0.tgz#0beedef8ee49c135412fb81fc59440dd48d652aa"
+ integrity sha512-RMetpFwUIeh3sVj2+p6+QX5AW8pF7DvQzxH9jUr8YjaF2iLE64vy6m0cZz/X8xkSktHrXMuPJIr7YIVF20TEyw==
+ dependencies:
+ "@babel/runtime" "7.25.7"
+ "@wordpress/element" "*"
+ "@wordpress/primitives" "*"
+
"@wordpress/jest-console@*":
version "8.11.0"
resolved "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.11.0.tgz"
@@ -2749,6 +2758,15 @@
resolved "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.11.0.tgz"
integrity sha512-Aoc8+xWOyiXekodjaEjS44z85XK877LzHZqsQuhC0kNgneDLrKkwI5qNgzwzAMbJ9jI58MPqVISCOX0bDLUPbw==
+"@wordpress/primitives@*":
+ version "4.11.0"
+ resolved "https://registry.yarnpkg.com/@wordpress/primitives/-/primitives-4.11.0.tgz#7bc24c07ed11057340832791c1c21e75a5181194"
+ integrity sha512-CoBXbh0mOSxcZtuzL7gK3RVumFx71DXQBfd3IkbRHuuVxa+2hI4KDuFyomSsbjQDshHsfuVrKUvuT3UGt6pdpQ==
+ dependencies:
+ "@babel/runtime" "7.25.7"
+ "@wordpress/element" "*"
+ clsx "^2.1.1"
+
"@wordpress/scripts@~30.0.0":
version "30.0.6"
resolved "https://registry.npmjs.org/@wordpress/scripts/-/scripts-30.0.6.tgz"
@@ -3727,6 +3745,11 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
+clsx@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"