♻️ 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 {
// 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',
// Activity management (advanced solution that replaces the isBusy state).
START_ACTIVITY: 'COMMON:START_ACTIVITY',
STOP_ACTIVITY: 'COMMON:STOP_ACTIVITY',
START_ACTIVITY: 'ppcp/common/START_ACTIVITY',
STOP_ACTIVITY: 'ppcp/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',
DO_PERSIST_DATA: 'ppcp/common/DO_PERSIST_DATA',
DO_DIRECT_API_AUTHENTICATION: 'ppcp/common/DO_DIRECT_API_AUTHENTICATION',
DO_OAUTH_AUTHENTICATION: 'ppcp/common/DO_OAUTH_AUTHENTICATION',
DO_DISCONNECT_MERCHANT: 'ppcp/common/DO_DISCONNECT_MERCHANT',
DO_GENERATE_ONBOARDING_URL: 'ppcp/common/DO_GENERATE_ONBOARDING_URL',
DO_REFRESH_MERCHANT: 'ppcp/common/DO_REFRESH_MERCHANT',
DO_REFRESH_FEATURES: 'ppcp/common/DO_REFRESH_FEATURES',
DO_RESUBSCRIBE_WEBHOOKS: 'ppcp/common/DO_RESUBSCRIBE_WEBHOOKS',
DO_START_WEBHOOK_SIMULATION: 'ppcp/common/DO_START_WEBHOOK_SIMULATION',
DO_CHECK_WEBHOOK_SIMULATION: 'ppcp/common/DO_CHECK_WEBHOOK_SIMULATION',
};

View file

@ -1,46 +1,83 @@
import { select } from '@wordpress/data';
import ACTION_TYPES from './action-types';
import { hydrate } from './actions';
import { STORE_NAME } from './constants';
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';
import apiFetch from '@wordpress/api-fetch';
/**
* Side effect. Saves the persistent details to the WP database.
*
* @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 };
};
await apiFetch( {
path: REST_PERSIST_PATH,
method: 'POST',
data,
} );
};
}
/**
* Side effect. Fetches the ISU-login URL for a sandbox account.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const sandboxOnboardingUrl = function* () {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: true,
products: [ 'EXPRESS_CHECKOUT' ],
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 {Action} The action.
* @return {Function} The thunk function.
*/
export const productionOnboardingUrl = function* ( products = [] ) {
return yield {
type: ACTION_TYPES.DO_GENERATE_ONBOARDING_URL,
useSandbox: false,
products,
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.
@ -52,20 +89,32 @@ export const productionOnboardingUrl = function* ( products = [] ) {
* @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.
* @return {Function} The thunk function.
*/
export const authenticateWithCredentials = function* (
export function authenticateWithCredentials(
clientId,
clientSecret,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_DIRECT_API_AUTHENTICATION,
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
@ -75,97 +124,156 @@ export const authenticateWithCredentials = function* (
* 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} 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.
* @return {Function} The thunk function.
*/
export const authenticateWithOAuth = function* (
sharedId,
authCode,
useSandbox
) {
return yield {
type: ACTION_TYPES.DO_OAUTH_AUTHENTICATION,
sharedId,
authCode,
useSandbox,
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 {Action} The action.
* @return {Function} The thunk function.
*/
export const disconnectMerchant = function* () {
return yield { type: ACTION_TYPES.DO_DISCONNECT_MERCHANT };
};
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 {Action} The action.
* @return {Function} The thunk function.
*/
export const refreshMerchantData = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_MERCHANT };
export function refreshMerchantData() {
return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
path: REST_HYDRATE_MERCHANT_PATH,
} );
if ( result.success && result.merchant ) {
yield hydrate( result );
}
if ( result.success && result.merchant ) {
dispatch.hydrate( result );
}
return 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 {Action} The action.
* @return {Function} The thunk function.
*/
export const refreshFeatureStatuses = function* () {
const result = yield { type: ACTION_TYPES.DO_REFRESH_FEATURES };
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.
yield refreshMerchantData();
}
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;
};
return result;
} catch ( e ) {
return {
success: false,
error: e,
message: e.message,
};
}
};
}
/**
* Side effect
* Refreshes subscribed webhooks via a REST request
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const resubscribeWebhooks = function* () {
const result = yield { type: ACTION_TYPES.DO_RESUBSCRIBE_WEBHOOKS };
export function resubscribeWebhooks() {
return async ( { dispatch } ) => {
try {
const result = await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS,
} );
if ( result && result.success ) {
yield hydrate( result );
}
if ( result.success && result.merchant ) {
dispatch.hydrate( result );
}
return result;
};
return result;
} catch ( e ) {
return {
success: false,
error: e,
};
}
};
}
/**
* Side effect. Starts webhook simulation.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const startWebhookSimulation = function* () {
return yield { type: ACTION_TYPES.DO_START_WEBHOOK_SIMULATION };
};
export function startWebhookSimulation() {
return async () => {
return await apiFetch( {
method: 'POST',
path: REST_WEBHOOKS_SIMULATE,
} );
};
}
/**
* Side effect. Checks webhook simulation.
*
* @return {Action} The action.
* @return {Function} The thunk function.
*/
export const checkWebhookSimulationState = function* () {
return yield { type: ACTION_TYPES.DO_CHECK_WEBHOOK_SIMULATION };
};
export function checkWebhookSimulationState() {
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 { controls as wpControls } from '@wordpress/data-controls';
import { STORE_NAME } from './constants';
import reducer from './reducer';
@ -7,8 +6,7 @@ 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.
@ -19,7 +17,6 @@ import { controls } from './controls';
export const initStore = () => {
const store = createReduxStore( STORE_NAME, {
reducer,
controls: { ...wpControls, ...controls },
actions: { ...actions, ...thunkActions },
selectors,
resolvers,

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'
)
);
}
},
};
};
}