♻️ Common store: Convert to thunks

This commit is contained in:
Philipp Stracker 2025-02-06 14:20:22 +01:00
parent e18f7fd7ae
commit d90a893a38
No known key found for this signature in database
5 changed files with 232 additions and 278 deletions

View file

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

View file

@ -1,46 +1,83 @@
import { select } from '@wordpress/data'; import {
REST_CONNECTION_URL_PATH,
import ACTION_TYPES from './action-types'; REST_DIRECT_AUTHENTICATION_PATH,
import { hydrate } from './actions'; REST_DISCONNECT_MERCHANT_PATH,
import { STORE_NAME } from './constants'; REST_HYDRATE_MERCHANT_PATH,
REST_OAUTH_AUTHENTICATION_PATH,
REST_PERSIST_PATH,
REST_REFRESH_FEATURES_PATH,
REST_WEBHOOKS,
REST_WEBHOOKS_SIMULATE,
} from './constants';
import apiFetch from '@wordpress/api-fetch';
/** /**
* Side effect. Saves the persistent details to the WP database. * Side effect. Saves the persistent details to the WP database.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const persist = function* () { export function persist() {
const data = yield select( STORE_NAME ).persistentData(); return async ( { select } ) => {
const data = select.persistentData();
yield { type: ACTION_TYPES.DO_PERSIST_DATA, data }; await apiFetch( {
}; path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
};
}
/** /**
* Side effect. Fetches the ISU-login URL for a sandbox account. * Side effect. Fetches the ISU-login URL for a sandbox account.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const sandboxOnboardingUrl = function* () { export function sandboxOnboardingUrl() {
return yield { return async () => {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, try {
useSandbox: true, return apiFetch( {
products: [ 'EXPRESS_CHECKOUT' ], 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. * Side effect. Fetches the ISU-login URL for a production account.
* *
* @param {string[]} products Which products/features to display in the ISU popup. * @param {string[]} products Which products/features to display in the ISU popup.
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const productionOnboardingUrl = function* ( products = [] ) { export function productionOnboardingUrl( products = [] ) {
return yield { return async () => {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL, try {
useSandbox: false, return apiFetch( {
products, 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. * Side effect. Initiates a direct connection attempt using the provided client ID and secret.
@ -52,20 +89,32 @@ export const productionOnboardingUrl = function* ( products = [] ) {
* @param {string} clientId - AP client ID (always 80-characters, starting with "A"). * @param {string} clientId - AP client ID (always 80-characters, starting with "A").
* @param {string} clientSecret - API client secret. * @param {string} clientSecret - API client secret.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account. * @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const authenticateWithCredentials = function* ( export function authenticateWithCredentials(
clientId, clientId,
clientSecret, clientSecret,
useSandbox useSandbox
) { ) {
return yield { return async () => {
type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION, try {
clientId, return await apiFetch( {
clientSecret, path: REST_DIRECT_AUTHENTICATION_PATH,
useSandbox, 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 * Side effect. Completes the ISU login by authenticating the user via the one time sharedId and
@ -75,97 +124,156 @@ export const authenticateWithCredentials = function* (
* parameters are dynamically generated during the authentication process, and not managed by our * parameters are dynamically generated during the authentication process, and not managed by our
* Redux store. * Redux store.
* *
* @param {string} sharedId - OAuth client ID; called "sharedId" to prevent confusion with the API client ID. * @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 {string} authCode - OAuth authorization code provided during onboarding.
* @param {boolean} useSandbox - Whether the credentials are for a sandbox account. * @param {boolean} useSandbox - Whether the credentials are for a sandbox account.
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const authenticateWithOAuth = function* ( export function authenticateWithOAuth( sharedId, authCode, useSandbox ) {
sharedId, return async () => {
authCode, try {
useSandbox return await apiFetch( {
) { path: REST_OAUTH_AUTHENTICATION_PATH,
return yield { method: 'POST',
type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION, data: {
sharedId, sharedId,
authCode, authCode,
useSandbox, useSandbox,
},
} );
} catch ( e ) {
return {
success: false,
error: e,
};
}
}; };
}; }
/** /**
* Side effect. Checks webhook simulation. * Side effect. Checks webhook simulation.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const disconnectMerchant = function* () { export function disconnectMerchant() {
return yield { type: ACTION_TYPES.DO_DISCONNECT_MERCHANT }; return async () => {
}; return await apiFetch( {
path: REST_DISCONNECT_MERCHANT_PATH,
method: 'POST',
} );
};
}
/** /**
* Side effect. Clears and refreshes the merchant data via a REST request. * Side effect. Clears and refreshes the merchant data via a REST request.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const refreshMerchantData = function* () { export function refreshMerchantData() {
const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT }; return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
path: REST_HYDRATE_MERCHANT_PATH,
} );
if ( result.success && result.merchant ) { if ( result.success && result.merchant ) {
yield hydrate( result ); dispatch.hydrate( result );
} }
return result; return result;
}; } catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/** /**
* Side effect. * Side effect.
* Purges all feature status data via a REST request. * Purges all feature status data via a REST request.
* Refreshes the merchant data via a REST request. * Refreshes the merchant data via a REST request.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const refreshFeatureStatuses = function* () { export function refreshFeatureStatuses() {
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES }; return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
path: REST_REFRESH_FEATURES_PATH,
method: 'POST',
} );
if ( result && result.success ) { if ( result && result.success ) {
// TODO: Review if we can get the updated feature details in the result.data instead of // TODO: Review if we can get the updated feature details in the result.data
// doing a second refreshMerchantData() request. // instead of doing a second refreshMerchantData() request.
yield refreshMerchantData(); await dispatch.refreshMerchantData();
} }
return result; return result;
}; } catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
};
}
/** /**
* Side effect * Side effect
* Refreshes subscribed webhooks via a REST request * Refreshes subscribed webhooks via a REST request
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const resubscribeWebhooks = function* () { export function resubscribeWebhooks() {
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS }; return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS,
} );
if ( result && result.success ) { if ( result.success && result.merchant ) {
yield hydrate( result ); dispatch.hydrate( result );
} }
return result; return result;
}; } catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/** /**
* Side effect. Starts webhook simulation. * Side effect. Starts webhook simulation.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const startWebhookSimulation = function* () { export function startWebhookSimulation() {
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION }; return async () => {
}; return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS_SIMULATE,
} );
};
}
/** /**
* Side effect. Checks webhook simulation. * Side effect. Checks webhook simulation.
* *
* @return {Action} The action. * @return {Function} The thunk function.
*/ */
export const checkWebhookSimulationState = function* () { export function checkWebhookSimulationState() {
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION }; return async () => {
}; return await apiFetch( {
path: REST_WEBHOOKS_SIMULATE,
} );
};
}

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

View file

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