Merge pull request #3070 from woocommerce/PCP-4128-redux-refactor-controls-to-thunks

Redux: Refactor controls to thunks (4128)
This commit is contained in:
Philipp Stracker 2025-02-07 17:01:04 +01:00 committed by GitHub
commit 78c2208f2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 738 additions and 966 deletions

View file

@ -6,13 +6,10 @@
export default {
// Transient data.
SET_TRANSIENT: '<UNKNOWN>:SET_TRANSIENT',
SET_TRANSIENT: 'ppcp/<UNKNOWN>/SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: '<UNKNOWN>:SET_PERSISTENT',
RESET: '<UNKNOWN>:RESET',
HYDRATE: '<UNKNOWN>:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: '<UNKNOWN>:DO_PERSIST_DATA',
SET_PERSISTENT: 'ppcp/<UNKNOWN>/SET_PERSISTENT',
RESET: 'ppcp/<UNKNOWN>/RESET',
HYDRATE: 'ppcp/<UNKNOWN>/HYDRATE',
};

View file

@ -7,10 +7,10 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import { REST_PERSIST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
@ -69,12 +69,22 @@ export const setPersistent = ( prop, value ) => ( {
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of store data to the server.
* Thunk action creator. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
};
}

View file

@ -1,23 +0,0 @@
/**
* 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 } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -7,44 +7,51 @@
* @file
*/
import { useDispatch } from '@wordpress/data';
import { useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { createHooksForStore } from '../utils';
import { STORE_NAME } from './constants';
const useHooks = () => {
/**
* Single source of truth for access Redux details.
*
* This hook returns a stable API to access actions, selectors and special hooks to generate
* getter- and setters for transient or persistent properties.
*
* @return {{select, dispatch, useTransient, usePersistent}} Store data API.
*/
const useStoreData = () => {
const select = useSelect( ( selectors ) => selectors( STORE_NAME ), [] );
const dispatch = useDispatch( STORE_NAME );
const { useTransient, usePersistent } = createHooksForStore( STORE_NAME );
const { persist } = useDispatch( STORE_NAME );
// Read-only flags and derived state.
// Nothing here yet.
// Transient accessors.
const [ isReady ] = useTransient( 'isReady' );
// Persistent accessors.
// TODO: Replace with real property.
const [ sampleValue, setSampleValue ] = usePersistent( 'sampleValue' );
return {
persist,
isReady,
sampleValue,
setSampleValue,
};
return useMemo(
() => ( {
select,
dispatch,
useTransient,
usePersistent,
} ),
[ select, dispatch, useTransient, usePersistent ]
);
};
export const useStore = () => {
const { persist, isReady } = useHooks();
return { persist, isReady };
const { dispatch, useTransient } = useStoreData();
const [ isReady ] = useTransient( 'isReady' );
return { persist: dispatch.persist, isReady };
};
// TODO: Replace with real hook.
export const useSampleValue = () => {
const { sampleValue, setSampleValue } = useHooks();
const { usePersistent, select } = useStoreData();
const [ sampleValue, setSampleValue ] = usePersistent( 'sampleValue' );
return {
sampleValue,
setSampleValue,
flags: select.flags(),
};
};

View file

@ -1,13 +1,11 @@
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';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -18,7 +16,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -8,30 +8,31 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import { REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
/**
* Retrieve settings from the site's REST API.
*/
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const result = await apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
// TODO: Add the module name to the error message.
__(
'Error retrieving <UNKNOWN> details.',
'woocommerce-paypal-payments'
)
);
// TODO: Add the module name to the error message.
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving <UNKNOWN> details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -6,26 +6,15 @@
export default {
// Transient data.
SET_TRANSIENT: 'COMMON:SET_TRANSIENT',
SET_TRANSIENT: 'ppcp/common/SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'COMMON:SET_PERSISTENT',
RESET: 'COMMON:RESET',
HYDRATE: 'COMMON:HYDRATE',
SET_PERSISTENT: 'ppcp/common/SET_PERSISTENT',
RESET: 'ppcp/common/RESET',
HYDRATE: 'ppcp/common/HYDRATE',
RESET_MERCHANT: 'ppcp/common/RESET_MERCHANT',
// Activity management (advanced solution that replaces the isBusy state).
START_ACTIVITY: 'COMMON:START_ACTIVITY',
STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'COMMON:DO_PERSIST_DATA',
DO_DIRECT_API_AUTHENTICATION: 'COMMON:DO_DIRECT_API_AUTHENTICATION',
DO_OAUTH_AUTHENTICATION: 'COMMON:DO_OAUTH_AUTHENTICATION',
DO_DISCONNECT_MERCHANT: 'COMMON:DO_DISCONNECT_MERCHANT',
DO_GENERATE_ONBOARDING_URL: 'COMMON:DO_GENERATE_ONBOARDING_URL',
DO_REFRESH_MERCHANT: 'COMMON:DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'COMMON:DO_REFRESH_FEATURES',
DO_RESUBSCRIBE_WEBHOOKS: 'COMMON:DO_RESUBSCRIBE_WEBHOOKS',
DO_START_WEBHOOK_SIMULATION: 'COMMON:DO_START_WEBHOOK_SIMULATION',
DO_CHECK_WEBHOOK_SIMULATION: 'COMMON:DO_CHECK_WEBHOOK_SIMULATION',
START_ACTIVITY: 'ppcp/common/START_ACTIVITY',
STOP_ACTIVITY: 'ppcp/common/STOP_ACTIVITY',
};

View file

@ -0,0 +1,281 @@
import apiFetch from '@wordpress/api-fetch';
import {
REST_CONNECTION_URL_PATH,
REST_DIRECT_AUTHENTICATION_PATH,
REST_DISCONNECT_MERCHANT_PATH,
REST_HYDRATE_MERCHANT_PATH,
REST_OAUTH_AUTHENTICATION_PATH,
REST_PERSIST_PATH,
REST_REFRESH_FEATURES_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} from './constants';
/**
* Side effect. Saves the persistent details to the WP database.
*
* @return {Function} The thunk function.
*/
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
};
}
/**
* Side effect. Fetches the ISU-login URL for a sandbox account.
*
* @return {Function} The thunk function.
*/
export function sandboxOnboardingUrl() {
return async () => {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
useSandbox: true,
products: [ 'EXPRESS_CHECKOUT' ],
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect. Fetches the ISU-login URL for a production account.
*
* @param {string[]} products Which products/features to display in the ISU popup.
* @return {Function} The thunk function.
*/
export function productionOnboardingUrl( products = [] ) {
return async () => {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: {
useSandbox: false,
products,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect. Initiates a direct connection attempt using the provided client ID and secret.
*
* This action accepts parameters instead of fetching data from the Redux state because the
* values (ID and secret) are not managed by a central redux store, but might come from private
* component state.
*
* @param {string} clientId - AP client ID (always 80-characters, starting with "A").
* @param {string} clientSecret - API client secret.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Function} The thunk function.
*/
export function authenticateWithCredentials(
clientId,
clientSecret,
useSandbox
) {
return async () => {
try {
return await apiFetch( {
path: REST_DIRECT_AUTHENTICATION_PATH,
method: 'POST',
data: {
clientId,
clientSecret,
useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect. Completes the ISU login by authenticating the user via the one time sharedId and
* authCode provided by PayPal.
*
* This action accepts parameters instead of fetching data from the Redux state because all
* parameters are dynamically generated during the authentication process, and not managed by our
* Redux store.
*
* @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the
* API client ID.
* @param {string} authCode - OAuth authorization code provided during onboarding.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Function} The thunk function.
*/
export function authenticateWithOAuth( sharedId, authCode, useSandbox ) {
return async () => {
try {
return await apiFetch( {
path: REST_OAUTH_AUTHENTICATION_PATH,
method: 'POST',
data: {
sharedId,
authCode,
useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect. Checks webhook simulation.
*
* @return {Function} The thunk function.
*/
export function disconnectMerchant() {
return async () => {
return await apiFetch( {
path: REST_DISCONNECT_MERCHANT_PATH,
method: 'POST',
} );
};
}
/**
* Side effect. Clears and refreshes the merchant data via a REST request.
*
* @return {Function} The thunk function.
*/
export function refreshMerchantData() {
return async ( { dispatch } ) => {
try {
await dispatch.resetMerchant();
const result = await apiFetch( {
path: REST_HYDRATE_MERCHANT_PATH,
} );
if ( result.success && result.merchant ) {
dispatch.hydrate( result );
}
return result;
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect.
* Purges all feature status data via a REST request.
* Refreshes the merchant data via a REST request.
*
* @return {Function} The thunk function.
*/
export function refreshFeatureStatuses() {
return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
path: REST_REFRESH_FEATURES_PATH,
method: 'POST',
} );
if ( result && result.success ) {
// TODO: Review if we can get the updated feature details in the result.data
// instead of doing a second refreshMerchantData() request.
await dispatch.refreshMerchantData();
}
return result;
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
};
}
/**
* Side effect
* Refreshes subscribed webhooks via a REST request
*
* @return {Function} The thunk function.
*/
export function resubscribeWebhooks() {
return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS,
} );
if ( result.success && result.merchant ) {
dispatch.hydrate( result );
}
return result;
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect. Starts webhook simulation.
*
* @return {Function} The thunk function.
*/
export function startWebhookSimulation() {
return async () => {
return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS_SIMULATE,
} );
};
}
/**
* Side effect. Checks webhook simulation.
*
* @return {Function} The thunk function.
*/
export function checkWebhookSimulationState() {
return async () => {
return await apiFetch( {
path: REST_WEBHOOKS_SIMULATE,
} );
};
}

View file

@ -7,10 +7,7 @@
* @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.
@ -86,6 +83,42 @@ export const setActiveModal = ( activeModal ) =>
export const setActiveHighlight = ( activeHighlight ) =>
setTransient( 'activeHighlight', activeHighlight );
/**
* Persistent. Sets the sandbox mode on or off.
*
* @param {boolean} useSandbox
* @return {Action} The action.
*/
export const setSandboxMode = ( useSandbox ) =>
setPersistent( 'useSandbox', useSandbox );
/**
* Persistent. Toggles the "Manual Connection" mode on or off.
*
* @param {boolean} useManualConnection
* @return {Action} The action.
*/
export const setManualConnectionMode = ( useManualConnection ) =>
setPersistent( 'useManualConnection', useManualConnection );
/**
* Persistent. Changes the "webhooks" value.
*
* @param {string} webhooks
* @return {Action} The action.
*/
export const setWebhooks = ( webhooks ) =>
setPersistent( 'webhooks', webhooks );
/**
* Reset merchant details in the store.
*
* @return {Action} The action.
*/
export const resetMerchant = () => ( { type: ACTION_TYPES.RESET_MERCHANT } );
// Activity control - see useBusyState() hook.
/**
* Transient (Activity): Marks the start of an async activity
* Think of it as "setIsBusy(true)"
@ -117,198 +150,3 @@ export const stopActivity = ( id ) => ( {
type: ACTION_TYPES.STOP_ACTIVITY,
payload: { id },
} );
/**
* Persistent. Sets the sandbox mode on or off.
*
* @param {boolean} useSandbox
* @return {Action} The action.
*/
export const setSandboxMode = ( useSandbox ) =>
setPersistent( 'useSandbox', useSandbox );
/**
* Persistent. Toggles the "Manual Connection" mode on or off.
*
* @param {boolean} useManualConnection
* @return {Action} The action.
*/
export const setManualConnectionMode = ( useManualConnection ) =>
setPersistent( 'useManualConnection', useManualConnection );
/**
* 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. Fetches the ISU-login URL for a sandbox account.
*
* @return {Action} The action.
*/
export const sandboxOnboardingUrl = function* () {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: true,
products: [ 'EXPRESS_CHECKOUT' ],
};
};
/**
* Side effect. Fetches the ISU-login URL for a production account.
*
* @param {string[]} products Which products/features to display in the ISU popup.
* @return {Action} The action.
*/
export const productionOnboardingUrl = function* ( products = [] ) {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: false,
products,
};
};
/**
* Side effect. Initiates a direct connection attempt using the provided client ID and secret.
*
* This action accepts parameters instead of fetching data from the Redux state because the
* values (ID and secret) are not managed by a central redux store, but might come from private
* component state.
*
* @param {string} clientId - AP client ID (always 80-characters, starting with "A").
* @param {string} clientSecret - API client secret.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action.
*/
export const authenticateWithCredentials = function* (
clientId,
clientSecret,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION,
clientId,
clientSecret,
useSandbox,
};
};
/**
* Side effect. Completes the ISU login by authenticating the user via the one time sharedId and
* authCode provided by PayPal.
*
* This action accepts parameters instead of fetching data from the Redux state because all
* parameters are dynamically generated during the authentication process, and not managed by our
* Redux store.
*
* @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID.
* @param {string} authCode - OAuth authorization code provided during onboarding.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action.
*/
export const authenticateWithOAuth = function* (
sharedId,
authCode,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION,
sharedId,
authCode,
useSandbox,
};
};
/**
* Side effect. Checks webhook simulation.
*
* @return {Action} The action.
*/
export const disconnectMerchant = function* () {
return yield { type: ACTION_TYPES.DO_DISCONNECT_MERCHANT };
};
/**
* Side effect. Clears and refreshes the merchant data via a REST request.
*
* @return {Action} The action.
*/
export const refreshMerchantData = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
if ( result.success && result.merchant ) {
yield hydrate( result );
}
return result;
};
/**
* Side effect.
* Purges all feature status data via a REST request.
* Refreshes the merchant data via a REST request.
*
* @return {Action} The action.
*/
export const refreshFeatureStatuses = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES };
if ( result && result.success ) {
// TODO: Review if we can get the updated feature details in the result.data instead of
// doing a second refreshMerchantData() request.
yield refreshMerchantData();
}
return result;
};
/**
* Persistent. Changes the "webhooks" value.
*
* @param {string} webhooks
* @return {Action} The action.
*/
export const setWebhooks = ( webhooks ) => ( {
type: ACTION_TYPES.SET_PERSISTENT,
payload: { webhooks },
} );
/**
* Side effect
* Refreshes subscribed webhooks via a REST request
*
* @return {Action} The action.
*/
export const resubscribeWebhooks = function* () {
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS };
if ( result && result.success ) {
yield hydrate( result );
}
return result;
};
/**
* Side effect. Starts webhook simulation.
*
* @return {Action} The action.
*/
export const startWebhookSimulation = function* () {
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION };
};
/**
* Side effect. Checks webhook simulation.
*
* @return {Action} The action.
*/
export const checkWebhookSimulationState = function* () {
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION };
};

View file

@ -1,154 +0,0 @@
/**
* 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_CONNECTION_URL_PATH,
REST_HYDRATE_MERCHANT_PATH,
REST_REFRESH_FEATURES_PATH,
REST_DIRECT_AUTHENTICATION_PATH,
REST_OAUTH_AUTHENTICATION_PATH,
REST_DISCONNECT_MERCHANT_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} 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 ( error ) {
console.error( 'Error saving data.', error );
}
},
async [ ACTION_TYPES.DO_GENERATE_ONBOARDING_URL ]( {
products,
useSandbox,
} ) {
try {
return apiFetch( {
path: REST_CONNECTION_URL_PATH,
method: 'POST',
data: { useSandbox, products },
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION ]( {
clientId,
clientSecret,
useSandbox,
} ) {
try {
return await apiFetch( {
path: REST_DIRECT_AUTHENTICATION_PATH,
method: 'POST',
data: {
clientId,
clientSecret,
useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_OAUTH_AUTHENTICATION ]( {
sharedId,
authCode,
useSandbox,
} ) {
try {
return await apiFetch( {
path: REST_OAUTH_AUTHENTICATION_PATH,
method: 'POST',
data: {
sharedId,
authCode,
useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_DISCONNECT_MERCHANT ]() {
return await apiFetch( {
path: REST_DISCONNECT_MERCHANT_PATH,
method: 'POST',
} );
},
async [ ACTION_TYPES.DO_REFRESH_MERCHANT ]() {
try {
return await apiFetch( { path: REST_HYDRATE_MERCHANT_PATH } );
} catch ( e ) {
return {
success: false,
error: e,
};
}
},
async [ ACTION_TYPES.DO_REFRESH_FEATURES ]() {
try {
return await apiFetch( {
path: REST_REFRESH_FEATURES_PATH,
method: 'POST',
} );
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
},
async [ ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS ]() {
return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS,
} );
},
async [ ACTION_TYPES.DO_START_WEBHOOK_SIMULATION ]() {
return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS_SIMULATE,
} );
},
async [ ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION ]() {
return await apiFetch( {
path: REST_WEBHOOKS_SIMULATE,
} );
},
};

View file

@ -1,13 +1,12 @@
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 thunkActions from './actions-thunk';
import * as hooks from './hooks';
import { resolvers } from './resolvers';
import { controls } from './controls';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -18,8 +17,7 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
actions: { ...actions, ...thunkActions },
selectors,
resolvers,
} );

View file

@ -106,7 +106,8 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
return changeTransient( state, { activities: newActivities } );
},
[ ACTION_TYPES.DO_REFRESH_MERCHANT ]: ( state ) => ( {
// Instantly reset the merchant data and features before refreshing the details.
[ ACTION_TYPES.RESET_MERCHANT ]: ( state ) => ( {
...state,
merchant: Object.freeze( { ...defaultTransient.merchant } ),
features: Object.freeze( { ...defaultTransient.features } ),

View file

@ -8,34 +8,37 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants';
import { REST_HYDRATE_PATH, REST_WEBHOOKS } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
/**
* Retrieve settings from the site's REST API.
*/
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const webhooks = yield apiFetch( { path: REST_WEBHOOKS } );
const [ result, webhooks ] = await Promise.all( [
apiFetch( { path: REST_HYDRATE_PATH } ),
apiFetch( { path: REST_WEBHOOKS } ),
] );
if ( webhooks.success && webhooks.data ) {
if ( result?.success && webhooks?.success && webhooks.data ) {
result.webhooks = webhooks.data;
}
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
__(
'Error retrieving plugin details.',
'woocommerce-paypal-payments'
)
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving plugin details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -56,7 +56,7 @@ export const addDebugTools = ( context, modules ) => {
if ( isConnected ) {
// Make sure the Onboarding wizard is "completed".
const onboarding = wp.data.dispatch( OnboardingStoreName );
onboarding.setCompleted( true );
onboarding.setPersistent( 'completed', true );
onboarding.persist();
// Reset all stores, except for the onboarding store.
@ -102,7 +102,7 @@ export const addDebugTools = ( context, modules ) => {
debugApi.onboardingMode = ( state ) => {
const onboarding = wp.data.dispatch( OnboardingStoreName );
onboarding.setCompleted( ! state );
onboarding.setPersistent( 'completed', ! state );
onboarding.persist();
};

View file

@ -6,13 +6,10 @@
export default {
// Transient data.
SET_TRANSIENT: 'ONBOARDING:SET_TRANSIENT',
SET_TRANSIENT: 'ppcp/onboarding/SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'ONBOARDING:SET_PERSISTENT',
RESET: 'ONBOARDING:RESET',
HYDRATE: 'ONBOARDING:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'ONBOARDING:DO_PERSIST_DATA',
SET_PERSISTENT: 'ppcp/onboarding/SET_PERSISTENT',
RESET: 'ppcp/onboarding/RESET',
HYDRATE: 'ppcp/onboarding/HYDRATE',
};

View file

@ -7,10 +7,10 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import { REST_PERSIST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
@ -69,12 +69,22 @@ export const setPersistent = ( prop, value ) => ( {
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of onboarding data to the server.
* Thunk action creator. Triggers the persistence of onboarding data to the server.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
};
}

View file

@ -1,27 +0,0 @@
/**
* 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 );
}
},
};

View file

@ -1,13 +1,11 @@
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';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -18,7 +16,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -8,29 +8,27 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import { REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const result = await apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
__(
'Error retrieving onboarding details.',
'woocommerce-paypal-payments'
)
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving onboarding details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -6,13 +6,10 @@
export default {
// Transient data.
SET_TRANSIENT: 'PAY_LATER_MESSAGING:SET_TRANSIENT',
SET_TRANSIENT: 'ppcp/paylater/SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'PAY_LATER_MESSAGING:SET_PERSISTENT',
RESET: 'PAY_LATER_MESSAGING:RESET',
HYDRATE: 'PAY_LATER_MESSAGING:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'PAY_LATER_MESSAGING:DO_PERSIST_DATA',
SET_PERSISTENT: 'ppcp/paylater/SET_PERSISTENT',
RESET: 'ppcp/paylater/RESET',
HYDRATE: 'ppcp/paylater/HYDRATE',
};

View file

@ -7,10 +7,10 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import { REST_PERSIST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
@ -69,12 +69,22 @@ export const setPersistent = ( prop, value ) => ( {
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of store data to the server.
* Thunk action creator. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
};
}

View file

@ -1,23 +0,0 @@
/**
* 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 } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -1,13 +1,11 @@
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';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -18,7 +16,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -8,30 +8,30 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import { REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
/**
* Retrieve settings from the site's REST API.
*/
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const result = await apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
// TODO: Add the module name to the error message.
__(
'Error retrieving Pay Later Messaging config details.',
'woocommerce-paypal-payments'
)
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving Pay Later Messaging details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -13,7 +13,4 @@ export default {
RESET: 'PAYMENT:RESET',
HYDRATE: 'PAYMENT:HYDRATE',
CHANGE_PAYMENT_SETTING: 'PAYMENT:CHANGE_PAYMENT_SETTING',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'PAYMENT:DO_PERSIST_DATA',
};

View file

@ -7,10 +7,10 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import { REST_PERSIST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
@ -81,12 +81,22 @@ export const changePaymentSettings = ( id, props ) => ( {
} );
/**
* Side effect. Triggers the persistence of store data to the server.
* Thunk action creator. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
};
}

View file

@ -1,23 +0,0 @@
/**
* 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 } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -1,13 +1,11 @@
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';
import * as resolvers from './resolvers';
import { initTodoSync } from '../sync/todo-state-sync';
/**
@ -19,7 +17,6 @@ import { initTodoSync } from '../sync/todo-state-sync';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -8,29 +8,30 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import { REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
/**
* Retrieve settings from the site's REST API.
*/
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const result = await apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
__(
'Error retrieving payment details.',
'woocommerce-paypal-payments'
)
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving payment details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -31,10 +31,4 @@ export default {
* to set up the initial state with data from the server.
*/
HYDRATE: 'ppcp/settings/HYDRATE',
/**
* Triggers the persistence of store data to the server.
* Used when changes need to be saved to the backend.
*/
DO_PERSIST_DATA: 'ppcp/settings/DO_PERSIST_DATA',
};

View file

@ -7,9 +7,10 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import { REST_PERSIST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
@ -70,12 +71,22 @@ export const setPersistent = ( prop, value ) => ( {
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
/**
* Side effect. Triggers the persistence of store data to the server.
* Yields an action with the current persistent data to be saved.
* Thunk action creator. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
};
}

View file

@ -1,34 +0,0 @@
/**
* 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';
/**
* Control handlers for settings store actions.
* Each handler maps to an ACTION_TYPE and performs the corresponding async operation.
*/
export const controls = {
/**
* Persists settings data to the server via REST API.
* Triggered by the DO_PERSIST_DATA action to save settings changes.
*
* @param {Object} action The action object
* @param {Object} action.data The settings data to persist
* @return {Promise<Object>} The API response
*/
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -8,15 +8,13 @@
*/
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';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -27,7 +25,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -8,50 +8,30 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import apiFetch from '@wordpress/api-fetch';
export const resolvers = {
/**
* Retrieve PayPal settings from the site's REST API.
* Hydrates the store with the retrieved data and marks it as ready.
*
* @generator
* @yield {Object} API fetch and dispatch actions
*/
*persistentData() {
import { REST_HYDRATE_PATH } from './constants';
/**
* Retrieve settings from the site's REST API.
*/
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
// Fetch settings data from REST API
const result = yield apiFetch( {
path: REST_HYDRATE_PATH,
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
} );
const result = await apiFetch( { path: REST_HYDRATE_PATH } );
// Update store with retrieved data
yield dispatch( STORE_NAME ).hydrate( result );
// Mark store as ready for use
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
// Log detailed error information for debugging
console.error( 'Full error details:', {
error: e,
path: REST_HYDRATE_PATH,
store: STORE_NAME,
} );
// Display user-friendly error notice
yield dispatch( 'core/notices' ).createErrorNotice(
__(
'Error retrieving PayPal settings details.',
'woocommerce-paypal-payments'
)
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving PayPal Settings details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -6,13 +6,10 @@
export default {
// Transient data.
SET_TRANSIENT: 'STYLE:SET_TRANSIENT',
SET_TRANSIENT: 'ppcp/style/SET_TRANSIENT',
// Persistent data.
SET_PERSISTENT: 'STYLE:SET_PERSISTENT',
RESET: 'STYLE:RESET',
HYDRATE: 'STYLE:HYDRATE',
// Controls - always start with "DO_".
DO_PERSIST_DATA: 'STYLE:DO_PERSIST_DATA',
SET_PERSISTENT: 'ppcp/style/SET_PERSISTENT',
RESET: 'ppcp/style/RESET',
HYDRATE: 'ppcp/style/HYDRATE',
};

View file

@ -7,10 +7,10 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import { REST_PERSIST_PATH } from './constants';
/**
* @typedef {Object} Action An action object that is handled by a reducer or control.
@ -69,12 +69,22 @@ export const setPersistent = ( prop, value ) => ( {
export const setIsReady = ( state ) => setTransient( 'isReady', state );
/**
* Side effect. Triggers the persistence of store data to the server.
* Thunk action creator. Triggers the persistence of store data to the server.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
export function persist() {
return async ( { select } ) => {
const data = select.persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
try {
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
} catch ( e ) {
console.error( 'Error saving progress.', e );
}
};
}

View file

@ -1,23 +0,0 @@
/**
* 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 } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
};

View file

@ -1,13 +1,11 @@
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';
import * as resolvers from './resolvers';
/**
* Initializes and registers the settings store with WordPress data layer.
@ -18,7 +16,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -8,29 +8,30 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import apiFetch from '@wordpress/api-fetch';
import { STORE_NAME, REST_HYDRATE_PATH } from './constants';
import { REST_HYDRATE_PATH } from './constants';
export const resolvers = {
/**
* Retrieve settings from the site's REST API.
*/
*persistentData() {
/**
* Retrieve settings from the site's REST API.
*/
export function persistentData() {
return async ( { dispatch, registry } ) => {
try {
const result = yield apiFetch( { path: REST_HYDRATE_PATH } );
const result = await apiFetch( { path: REST_HYDRATE_PATH } );
yield dispatch( STORE_NAME ).hydrate( result );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.hydrate( result );
await dispatch.setIsReady( true );
} catch ( e ) {
yield dispatch( 'core/notices' ).createErrorNotice(
__(
'Error retrieving style-details.',
'woocommerce-paypal-payments'
)
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving Styling details.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -6,16 +6,11 @@
export default {
// Transient data
SET_TRANSIENT: 'TODOS:SET_TRANSIENT',
SET_COMPLETED_TODOS: 'TODOS:SET_COMPLETED_TODOS',
SET_TRANSIENT: 'ppcp/todos/SET_TRANSIENT',
SET_COMPLETED_TODOS: 'ppcp/todos/SET_COMPLETED_TODOS',
// Persistent data
SET_TODOS: 'TODOS:SET_TODOS',
SET_DISMISSED_TODOS: 'TODOS:SET_DISMISSED_TODOS',
// Controls
DO_FETCH_TODOS: 'TODOS:DO_FETCH_TODOS',
DO_PERSIST_DATA: 'TODOS:DO_PERSIST_DATA',
DO_RESET_DISMISSED_TODOS: 'TODOS:DO_RESET_DISMISSED_TODOS',
DO_COMPLETE_ONCLICK: 'TODOS:DO_COMPLETE_ONCLICK',
SET_TODOS: 'ppcp/todos/SET_TODOS',
SET_DISMISSED_TODOS: 'ppcp/todos/SET_DISMISSED_TODOS',
RESET_DISMISSED_TODOS: 'ppcp/todos/RESET_DISMISSED_TODOS',
};

View file

@ -7,9 +7,15 @@
* @file
*/
import { select } from '@wordpress/data';
import apiFetch from '@wordpress/api-fetch';
import ACTION_TYPES from './action-types';
import { STORE_NAME } from './constants';
import {
REST_COMPLETE_ONCLICK_PATH,
REST_PATH,
REST_PERSIST_PATH,
REST_RESET_DISMISSED_TODOS_PATH,
} from './constants';
export const setIsReady = ( isReady ) => ( {
type: ACTION_TYPES.SET_TRANSIENT,
@ -26,42 +32,76 @@ export const setDismissedTodos = ( dismissedTodos ) => ( {
payload: dismissedTodos,
} );
export const fetchTodos = function* () {
yield { type: ACTION_TYPES.DO_FETCH_TODOS };
};
export const persist = function* () {
const data = yield select( STORE_NAME ).persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data };
};
export const resetDismissedTodos = function* () {
const result = yield { type: ACTION_TYPES.DO_RESET_DISMISSED_TODOS };
if ( result && result.success ) {
yield setDismissedTodos( [] );
}
return result;
};
export const setCompletedTodos = ( completedTodos ) => ( {
type: ACTION_TYPES.SET_COMPLETED_TODOS,
payload: completedTodos,
} );
export const completeOnClick = function* ( todoId ) {
const result = yield {
type: ACTION_TYPES.DO_COMPLETE_ONCLICK,
todoId,
// Thunks
export function fetchTodos() {
return async () => {
const response = await apiFetch( { path: REST_PATH } );
return response?.data || [];
};
}
if ( result && result.success ) {
// Set transient completed state for visual feedback
const currentTransientCompleted =
yield select( STORE_NAME ).getCompletedTodos();
yield setCompletedTodos( [ ...currentTransientCompleted, todoId ] );
}
export function persist() {
return async ( { select } ) => {
const data = await select.persistentData();
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
};
}
return result;
};
export function resetDismissedTodos() {
return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
path: REST_RESET_DISMISSED_TODOS_PATH,
method: 'POST',
} );
if ( result && result.success ) {
await dispatch.setDismissedTodos( [] );
}
return result;
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
};
}
export function completeOnClick( todoId ) {
return async ( { select, dispatch } ) => {
try {
const result = await apiFetch( {
path: REST_COMPLETE_ONCLICK_PATH,
method: 'POST',
data: { todoId },
} );
if ( result?.success ) {
// Set transient completed state for visual feedback
const completed = await select.getCompletedTodos();
await dispatch.setCompletedTodos( [ ...completed, todoId ] );
}
return result;
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
};
}

View file

@ -1,65 +0,0 @@
/**
* 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_PATH,
REST_PERSIST_PATH,
REST_RESET_DISMISSED_TODOS_PATH,
REST_COMPLETE_ONCLICK_PATH,
} from './constants';
import ACTION_TYPES from './action-types';
export const controls = {
async [ ACTION_TYPES.DO_FETCH_TODOS ]() {
const response = await apiFetch( {
path: REST_PATH,
method: 'GET',
} );
return response?.data || [];
},
async [ ACTION_TYPES.DO_PERSIST_DATA ]( { data } ) {
return await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
},
async [ ACTION_TYPES.DO_RESET_DISMISSED_TODOS ]() {
try {
return await apiFetch( {
path: REST_RESET_DISMISSED_TODOS_PATH,
method: 'POST',
} );
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
},
async [ ACTION_TYPES.DO_COMPLETE_ONCLICK ]( { todoId } ) {
try {
const response = await apiFetch( {
path: REST_COMPLETE_ONCLICK_PATH,
method: 'POST',
data: { todoId },
} );
return response;
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
},
};

View file

@ -1,13 +1,11 @@
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';
import * as resolvers from './resolvers';
/**
* Initializes and registers the todos store with WordPress data layer.
@ -18,7 +16,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions,
selectors,
resolvers,

View file

@ -94,25 +94,10 @@ const reducer = createReducer( defaultTransient, defaultPersistent, {
* @param {Object} state Current state
* @return {Object} Updated state
*/
[ ACTION_TYPES.DO_RESET_DISMISSED_TODOS ]: ( state ) => {
[ ACTION_TYPES.RESET_DISMISSED_TODOS ]: ( state ) => {
return changePersistent( state, { dismissedTodos: [] } );
},
/**
* 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;
},
/**
* Initializes persistent state with data from the server
*

View file

@ -8,27 +8,33 @@
* @file
*/
import { dispatch } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { apiFetch } from '@wordpress/data-controls';
import { STORE_NAME, REST_PATH } from './constants';
import apiFetch from '@wordpress/api-fetch';
export const resolvers = {
*getTodos() {
import { REST_PATH } from './constants';
/**
* Retrieve settings from the site's REST API.
*/
export function getTodos() {
return async ( { dispatch, registry } ) => {
try {
const response = yield apiFetch( { path: REST_PATH } );
const response = await apiFetch( { path: REST_PATH } );
const { todos = [], dismissedTodos = [] } = response?.data || {};
yield dispatch( STORE_NAME ).setTodos( todos );
yield dispatch( STORE_NAME ).setDismissedTodos( dismissedTodos );
yield dispatch( STORE_NAME ).setIsReady( true );
await dispatch.setTodos( todos );
await dispatch.setDismissedTodos( dismissedTodos );
await dispatch.setIsReady( true );
} catch ( e ) {
console.error( 'Resolver error:', e );
yield dispatch( STORE_NAME ).setIsReady( false );
yield dispatch( 'core/notices' ).createErrorNotice(
__( 'Error retrieving todos.', 'woocommerce-paypal-payments' )
);
await registry
.dispatch( 'core/notices' )
.createErrorNotice(
__(
'Error retrieving todos.',
'woocommerce-paypal-payments'
)
);
}
},
};
};
}

View file

@ -141,6 +141,7 @@ class SettingsModel extends AbstractDataModel {
public function set_soft_descriptor( string $descriptor ) : void {
$descriptor = $this->sanitizer->sanitize_text( $descriptor );
$descriptor = preg_replace( '/[^a-zA-Z0-9\-*. ]/', '', $descriptor ) ?? '';
$this->data['soft_descriptor'] = substr( $descriptor, 0, 22 );
}