mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-01 07:02:48 +08:00
🏗️ Clean up the tracking architecture and add a separate tracking data store
This commit is contained in:
parent
13dfd8f704
commit
2ee3d68ecc
22 changed files with 470 additions and 723 deletions
|
@ -18,7 +18,4 @@ export default {
|
|||
// Activity management (advanced solution that replaces the isBusy state).
|
||||
START_ACTIVITY: 'ppcp/common/START_ACTIVITY',
|
||||
STOP_ACTIVITY: 'ppcp/common/STOP_ACTIVITY',
|
||||
|
||||
// Tracking.
|
||||
CLEAR_FIELD_SOURCE: 'ppcp/common/CLEAR_FIELD_SOURCE',
|
||||
};
|
||||
|
|
|
@ -36,34 +36,26 @@ export const hydrate = ( payload ) => ( {
|
|||
/**
|
||||
* Generic transient-data updater.
|
||||
*
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setTransient = ( prop, value, source = '' ) => ( {
|
||||
export const setTransient = ( prop, value ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { [ prop ]: value },
|
||||
source,
|
||||
fieldName: prop,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Generic persistent-data updater.
|
||||
*
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setPersistent = ( prop, value, source = '' ) => {
|
||||
return {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { [ prop ]: value },
|
||||
source,
|
||||
fieldName: prop,
|
||||
};
|
||||
};
|
||||
export const setPersistent = ( prop, value ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { [ prop ]: value },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Marks the onboarding details as "ready", i.e., fully initialized.
|
||||
|
@ -86,22 +78,19 @@ export const setActiveModal = ( activeModal ) =>
|
|||
* Persistent. Sets the sandbox mode on or off.
|
||||
*
|
||||
* @param {boolean} useSandbox
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setSandboxMode = ( useSandbox, source ) => {
|
||||
return setPersistent( 'useSandbox', useSandbox, source );
|
||||
};
|
||||
export const setSandboxMode = ( useSandbox ) =>
|
||||
setPersistent( 'useSandbox', useSandbox );
|
||||
|
||||
/**
|
||||
* Persistent. Toggles the "Manual Connection" mode on or off.
|
||||
*
|
||||
* @param {boolean} useManualConnection
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setManualConnectionMode = ( useManualConnection, source ) =>
|
||||
setPersistent( 'useManualConnection', useManualConnection, source );
|
||||
export const setManualConnectionMode = ( useManualConnection ) =>
|
||||
setPersistent( 'useManualConnection', useManualConnection );
|
||||
|
||||
/**
|
||||
* Persistent. Changes the "webhooks" value.
|
||||
|
@ -163,14 +152,3 @@ export const stopActivity = ( id ) => ( {
|
|||
type: ACTION_TYPES.STOP_ACTIVITY,
|
||||
payload: { id },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Action creator to clear field source tracking.
|
||||
*
|
||||
* @param {string} fieldName - Name of the field to clear source for.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const clearFieldSource = ( fieldName ) => ( {
|
||||
type: ACTION_TYPES.CLEAR_FIELD_SOURCE,
|
||||
payload: { fieldName },
|
||||
} );
|
||||
|
|
|
@ -17,7 +17,6 @@ const defaultTransient = Object.freeze( {
|
|||
activities: new Map(),
|
||||
activeModal: '',
|
||||
activeHighlight: '',
|
||||
fieldSources: Object.freeze( {} ),
|
||||
|
||||
// Read only values, provided by the server via hydrate.
|
||||
merchant: Object.freeze( {
|
||||
|
@ -74,76 +73,21 @@ const defaultPersistent = Object.freeze( {
|
|||
useManualConnection: false,
|
||||
} );
|
||||
|
||||
const updateFieldSources = ( currentSources, fieldName, source ) => {
|
||||
if ( ! source ) {
|
||||
return currentSources;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentSources,
|
||||
[ fieldName ]: {
|
||||
source,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const clearFieldSource = ( currentSources, fieldName ) => {
|
||||
const newSources = { ...currentSources };
|
||||
delete newSources[ fieldName ];
|
||||
return newSources;
|
||||
};
|
||||
|
||||
// Reducer logic.
|
||||
|
||||
const [ changeTransient, changePersistent ] = createReducerSetters(
|
||||
defaultTransient,
|
||||
defaultPersistent
|
||||
);
|
||||
|
||||
const commonReducer = createReducer( defaultTransient, defaultPersistent, {
|
||||
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload, action ) => {
|
||||
const newState = changeTransient( state, payload );
|
||||
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, action ) =>
|
||||
changeTransient( state, action ),
|
||||
|
||||
if ( action && action.source ) {
|
||||
const fieldName = Object.keys( payload )[ 0 ];
|
||||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, action ) =>
|
||||
changePersistent( state, action ),
|
||||
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
fieldName,
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload, action ) => {
|
||||
const newState = changePersistent( state, payload );
|
||||
|
||||
if ( action && action.source ) {
|
||||
const fieldName = Object.keys( payload )[ 0 ];
|
||||
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
fieldName,
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.CLEAR_FIELD_SOURCE ]: ( state, payload ) => {
|
||||
const newState = { ...state };
|
||||
newState.fieldSources = clearFieldSource(
|
||||
newState.fieldSources,
|
||||
payload.fieldName
|
||||
);
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.RESET ]: ( state, payload, action ) => {
|
||||
[ ACTION_TYPES.RESET ]: ( state ) => {
|
||||
const cleanState = changeTransient(
|
||||
changePersistent( state, defaultPersistent ),
|
||||
defaultTransient
|
||||
|
@ -155,109 +99,37 @@ const commonReducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
cleanState.features = { ...state.features };
|
||||
cleanState.isReady = true;
|
||||
|
||||
if ( action && action.source ) {
|
||||
cleanState.fieldSources = updateFieldSources(
|
||||
cleanState.fieldSources,
|
||||
'reset',
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return cleanState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.START_ACTIVITY ]: ( state, payload, action ) => {
|
||||
const newState = changeTransient( state, {
|
||||
[ ACTION_TYPES.START_ACTIVITY ]: ( state, payload ) => {
|
||||
return changeTransient( state, {
|
||||
activities: new Map( state.activities ).set(
|
||||
payload.id,
|
||||
payload.description
|
||||
),
|
||||
} );
|
||||
|
||||
if ( action && action.source ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'activity_' + payload.id,
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload, action ) => {
|
||||
[ ACTION_TYPES.STOP_ACTIVITY ]: ( state, payload ) => {
|
||||
const newActivities = new Map( state.activities );
|
||||
newActivities.delete( payload.id );
|
||||
const newState = changeTransient( state, {
|
||||
activities: newActivities,
|
||||
} );
|
||||
|
||||
if ( action && action.source ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'activity_stop_' + payload.id,
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
return changeTransient( state, { activities: newActivities } );
|
||||
},
|
||||
|
||||
// Instantly reset the merchant data and features before refreshing the details.
|
||||
[ ACTION_TYPES.RESET_MERCHANT ]: ( state, payload, action ) => {
|
||||
const newState = {
|
||||
...state,
|
||||
merchant: Object.freeze( { ...defaultTransient.merchant } ),
|
||||
features: Object.freeze( { ...defaultTransient.features } ),
|
||||
};
|
||||
[ ACTION_TYPES.RESET_MERCHANT ]: ( state ) => ( {
|
||||
...state,
|
||||
merchant: Object.freeze( { ...defaultTransient.merchant } ),
|
||||
features: Object.freeze( { ...defaultTransient.features } ),
|
||||
} ),
|
||||
|
||||
if ( action && action.source ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'reset_merchant',
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
[ ACTION_TYPES.SET_MERCHANT ]: ( state, payload ) => {
|
||||
return changePersistent( state, { merchant: payload.merchant } );
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.SET_MERCHANT ]: ( state, payload, action ) => {
|
||||
const newState = changePersistent( state, {
|
||||
merchant: payload.merchant,
|
||||
} );
|
||||
|
||||
if ( action && action.source ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'merchant',
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload, action ) => {
|
||||
let newState = { ...state };
|
||||
|
||||
if ( action && action.source && payload.data ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'hydrate',
|
||||
action.source
|
||||
);
|
||||
|
||||
Object.keys( payload.data ).forEach( ( fieldName ) => {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
fieldName,
|
||||
action.source
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
newState = changePersistent( newState, payload.data );
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||
const newState = changePersistent( state, payload.data );
|
||||
|
||||
// Populate read-only properties.
|
||||
[ 'wooSettings', 'merchant', 'features', 'webhooks' ].forEach(
|
||||
|
|
|
@ -32,32 +32,6 @@ export const getActivityList = ( state ) => {
|
|||
return Object.fromEntries( activities );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the source information for a specific field.
|
||||
* @param {Object} state - Store state
|
||||
* @param {string} fieldName
|
||||
*/
|
||||
export const getFieldSource = ( state, fieldName ) => {
|
||||
const fieldSources = state?.fieldSources || {};
|
||||
const fieldSource = fieldSources[ fieldName ];
|
||||
|
||||
if ( ! fieldSource ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fieldSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all field sources for debugging.
|
||||
* @param {Object} state - Store state
|
||||
* @return {Object} All field source information
|
||||
*/
|
||||
export const getAllFieldSources = ( state ) => {
|
||||
const currentState = getState( state );
|
||||
return currentState.fieldSources || {};
|
||||
};
|
||||
|
||||
export const merchant = ( state ) => {
|
||||
return getState( state ).merchant || EMPTY_OBJ;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,9 @@ import {
|
|||
SettingsStoreName,
|
||||
StylingStoreName,
|
||||
TodosStoreName,
|
||||
PayLaterMessagingStoreName,
|
||||
FeaturesStoreName,
|
||||
TrackingStoreName,
|
||||
} from './index';
|
||||
|
||||
export const addDebugTools = ( context, modules ) => {
|
||||
|
@ -80,6 +83,9 @@ export const addDebugTools = ( context, modules ) => {
|
|||
stores.push( SettingsStoreName );
|
||||
stores.push( StylingStoreName );
|
||||
stores.push( TodosStoreName );
|
||||
stores.push( PayLaterMessagingStoreName );
|
||||
stores.push( FeaturesStoreName );
|
||||
stores.push( TrackingStoreName );
|
||||
|
||||
// Only reset the onboarding store when the wizard is not completed.
|
||||
if ( ! completed ) {
|
||||
|
@ -119,6 +125,9 @@ export const addDebugTools = ( context, modules ) => {
|
|||
stores.push( StylingStoreName );
|
||||
stores.push( TodosStoreName );
|
||||
stores.push( OnboardingStoreName );
|
||||
stores.push( PayLaterMessagingStoreName );
|
||||
stores.push( FeaturesStoreName );
|
||||
stores.push( TrackingStoreName );
|
||||
|
||||
stores.forEach( ( storeName ) => {
|
||||
const store = wp.data.dispatch( storeName );
|
||||
|
|
|
@ -7,6 +7,7 @@ import * as Styling from './styling';
|
|||
import * as Todos from './todos';
|
||||
import * as PayLaterMessaging from './pay-later-messaging';
|
||||
import * as Features from './features';
|
||||
import * as Tracking from './tracking';
|
||||
|
||||
// Initialize tracking funnels before any store initialization.
|
||||
import '../services/tracking/init';
|
||||
|
@ -20,6 +21,7 @@ const stores = [
|
|||
Todos,
|
||||
PayLaterMessaging,
|
||||
Features,
|
||||
Tracking,
|
||||
];
|
||||
|
||||
stores.forEach( ( store ) => {
|
||||
|
@ -55,6 +57,7 @@ export const StylingStoreName = Styling.STORE_NAME;
|
|||
export const TodosStoreName = Todos.STORE_NAME;
|
||||
export const PayLaterMessagingStoreName = PayLaterMessaging.STORE_NAME;
|
||||
export const FeaturesStoreName = Features.STORE_NAME;
|
||||
export const TrackingStoreName = Tracking.STORE_NAME;
|
||||
|
||||
export * from './configuration';
|
||||
|
||||
|
|
|
@ -16,7 +16,4 @@ export default {
|
|||
// Gateway sync flag.
|
||||
SYNC_GATEWAYS: 'ppcp/onboarding/SYNC_GATEWAYS',
|
||||
REFRESH_GATEWAYS: 'ppcp/onboarding/REFRESH_GATEWAYS',
|
||||
|
||||
// Tracking.
|
||||
CLEAR_FIELD_SOURCE: 'ppcp/onboarding/CLEAR_FIELD_SOURCE',
|
||||
};
|
||||
|
|
|
@ -16,19 +16,14 @@ import { REST_PERSIST_PATH } 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.
|
||||
* @property {string?} source - Optional source context for tracking.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Special. Resets all values in the onboarding store to initial defaults.
|
||||
*
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const reset = ( source ) => ( {
|
||||
type: ACTION_TYPES.RESET,
|
||||
source,
|
||||
} );
|
||||
export const reset = () => ( { type: ACTION_TYPES.RESET } );
|
||||
|
||||
/**
|
||||
* Persistent. Set the full onboarding details, usually during app initialization.
|
||||
|
@ -39,37 +34,30 @@ export const reset = ( source ) => ( {
|
|||
export const hydrate = ( payload ) => ( {
|
||||
type: ACTION_TYPES.HYDRATE,
|
||||
payload,
|
||||
source: 'system',
|
||||
} );
|
||||
|
||||
/**
|
||||
* Generic transient-data updater.
|
||||
*
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setTransient = ( prop, value, source = '' ) => ( {
|
||||
export const setTransient = ( prop, value ) => ( {
|
||||
type: ACTION_TYPES.SET_TRANSIENT,
|
||||
payload: { [ prop ]: value },
|
||||
source,
|
||||
fieldName: prop,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Generic persistent-data updater.
|
||||
*
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @param {string} prop Name of the property to update.
|
||||
* @param {any} value The new value of the property.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setPersistent = ( prop, value, source ) => ( {
|
||||
export const setPersistent = ( prop, value ) => ( {
|
||||
type: ACTION_TYPES.SET_PERSISTENT,
|
||||
payload: { [ prop ]: value },
|
||||
source,
|
||||
fieldName: prop,
|
||||
} );
|
||||
|
||||
/**
|
||||
|
@ -78,8 +66,7 @@ export const setPersistent = ( prop, value, source ) => ( {
|
|||
* @param {boolean} isReady
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsReady = ( isReady ) =>
|
||||
setTransient( 'isReady', isReady, 'system' );
|
||||
export const setIsReady = ( isReady ) => setTransient( 'isReady', isReady );
|
||||
|
||||
/**
|
||||
* Thunk action creator. Triggers the persistence of onboarding data to the server.
|
||||
|
@ -117,139 +104,39 @@ export function refresh() {
|
|||
/**
|
||||
* Persistent. Updates the gateway synced status.
|
||||
*
|
||||
* @param {boolean} synced The sync status to set.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @param {boolean} synced The sync status to set
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const updateGatewaysSynced = ( synced = true, source ) =>
|
||||
setPersistent( 'gatewaysSynced', synced, source );
|
||||
export const updateGatewaysSynced = ( synced = true ) =>
|
||||
setPersistent( 'gatewaysSynced', synced );
|
||||
|
||||
/**
|
||||
* Persistent. Updates the gateway refreshed status.
|
||||
*
|
||||
* @param {boolean} refreshed The refreshed status to set.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @param {boolean} refreshed The refreshed status to set
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const updateGatewaysRefreshed = ( refreshed = true, source ) =>
|
||||
setPersistent( 'gatewaysRefreshed', refreshed, source );
|
||||
export const updateGatewaysRefreshed = ( refreshed = true ) =>
|
||||
setPersistent( 'gatewaysRefreshed', refreshed );
|
||||
|
||||
/**
|
||||
* Action creator to sync payment gateways.
|
||||
* This will both update the state and persist it.
|
||||
*
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Function} The thunk function.
|
||||
*/
|
||||
export function syncGateways( source ) {
|
||||
export function syncGateways() {
|
||||
return async ( { dispatch } ) => {
|
||||
dispatch( setPersistent( 'gatewaysSynced', true, source ) );
|
||||
dispatch( setPersistent( 'gatewaysSynced', true ) );
|
||||
await dispatch.persist();
|
||||
return { success: true };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action creator to refresh payment gateways.
|
||||
*
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Function} The thunk function.
|
||||
*/
|
||||
export function refreshGateways( source ) {
|
||||
export function refreshGateways() {
|
||||
return async ( { dispatch } ) => {
|
||||
dispatch( setPersistent( 'gatewaysRefreshed', true, source ) );
|
||||
dispatch( setPersistent( 'gatewaysRefreshed', true ) );
|
||||
await dispatch.persist();
|
||||
return { success: true };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action creator to clear field source tracking.
|
||||
*
|
||||
* @param {string} fieldName - Name of the field to clear source for.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const clearFieldSource = ( fieldName ) => ( {
|
||||
type: ACTION_TYPES.CLEAR_FIELD_SOURCE,
|
||||
payload: { fieldName },
|
||||
} );
|
||||
|
||||
/**
|
||||
* Transient. Updates the connection button clicked status.
|
||||
*
|
||||
* @param {boolean} clicked Whether the button was clicked.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setConnectionButtonClicked = ( clicked = true, source = 'user' ) =>
|
||||
setTransient( 'connectionButtonClicked', clicked, source );
|
||||
|
||||
/**
|
||||
* Persistent. Updates the current step in the onboarding flow.
|
||||
*
|
||||
* @param {number} step The step number to set.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setStep = ( step, source ) =>
|
||||
setPersistent( 'step', step, source );
|
||||
|
||||
/**
|
||||
* Persistent. Updates the completed status of the onboarding.
|
||||
*
|
||||
* @param {boolean} completed Whether onboarding is completed.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setCompleted = ( completed, source ) =>
|
||||
setPersistent( 'completed', completed, source );
|
||||
|
||||
/**
|
||||
* Persistent. Updates the casual seller status.
|
||||
*
|
||||
* @param {boolean} isCasualSeller Whether the user is a casual seller.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setIsCasualSeller = ( isCasualSeller, source ) =>
|
||||
setPersistent( 'isCasualSeller', isCasualSeller, source );
|
||||
|
||||
/**
|
||||
* Persistent. Updates the optional payment methods setting.
|
||||
*
|
||||
* @param {boolean} enabled Whether optional payment methods are enabled.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setOptionalPaymentMethods = ( enabled, source ) =>
|
||||
setPersistent( 'areOptionalPaymentMethodsEnabled', enabled, source );
|
||||
|
||||
/**
|
||||
* Persistent. Updates the selected products.
|
||||
*
|
||||
* @param {Array} products Array of selected product types.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setProducts = ( products, source ) =>
|
||||
setPersistent( 'products', products, source );
|
||||
|
||||
/**
|
||||
* Transient. Updates the manual client ID.
|
||||
*
|
||||
* @param {string} clientId The manual client ID.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setManualClientId = ( clientId, source ) =>
|
||||
setTransient( 'manualClientId', clientId, source );
|
||||
|
||||
/**
|
||||
* Transient. Updates the manual client secret.
|
||||
*
|
||||
* @param {string} clientSecret The manual client secret.
|
||||
* @param {string} source Optional source context for tracking.
|
||||
* @return {Action} The action.
|
||||
*/
|
||||
export const setManualClientSecret = ( clientSecret, source ) =>
|
||||
setTransient( 'manualClientSecret', clientSecret, source );
|
||||
|
|
|
@ -17,7 +17,6 @@ const defaultTransient = Object.freeze( {
|
|||
manualClientId: '',
|
||||
manualClientSecret: '',
|
||||
connectionButtonClicked: false,
|
||||
fieldSources: Object.freeze( {} ),
|
||||
|
||||
// Read only values, provided by the server.
|
||||
flags: Object.freeze( {
|
||||
|
@ -41,26 +40,6 @@ const defaultPersistent = Object.freeze( {
|
|||
gatewaysRefreshed: false,
|
||||
} );
|
||||
|
||||
const updateFieldSources = ( currentSources, fieldName, source ) => {
|
||||
if ( ! source ) {
|
||||
return currentSources;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentSources,
|
||||
[ fieldName ]: {
|
||||
source,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const clearFieldSource = ( currentSources, fieldName ) => {
|
||||
const newSources = { ...currentSources };
|
||||
delete newSources[ fieldName ];
|
||||
return newSources;
|
||||
};
|
||||
|
||||
// Reducer logic.
|
||||
|
||||
const [ changeTransient, changePersistent ] = createReducerSetters(
|
||||
|
@ -69,48 +48,13 @@ const [ changeTransient, changePersistent ] = createReducerSetters(
|
|||
);
|
||||
|
||||
const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
|
||||
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload, action ) => {
|
||||
const newState = changeTransient( state, payload );
|
||||
[ ACTION_TYPES.SET_TRANSIENT ]: ( state, payload ) =>
|
||||
changeTransient( state, payload ),
|
||||
|
||||
if ( action && action.source ) {
|
||||
const fieldName = Object.keys( payload )[ 0 ];
|
||||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload ) =>
|
||||
changePersistent( state, payload ),
|
||||
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
fieldName,
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.SET_PERSISTENT ]: ( state, payload, action ) => {
|
||||
const newState = changePersistent( state, payload );
|
||||
|
||||
if ( action && action.source ) {
|
||||
const fieldName = Object.keys( payload )[ 0 ];
|
||||
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
fieldName,
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.CLEAR_FIELD_SOURCE ]: ( state, payload ) => {
|
||||
const newState = { ...state };
|
||||
newState.fieldSources = clearFieldSource(
|
||||
newState.fieldSources,
|
||||
payload.fieldName
|
||||
);
|
||||
|
||||
return newState;
|
||||
},
|
||||
[ ACTION_TYPES.RESET ]: ( state, payload, action ) => {
|
||||
[ ACTION_TYPES.RESET ]: ( state ) => {
|
||||
const cleanState = changeTransient(
|
||||
changePersistent( state, defaultPersistent ),
|
||||
defaultTransient
|
||||
|
@ -120,37 +64,11 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
cleanState.flags = { ...state.flags };
|
||||
cleanState.isReady = true;
|
||||
|
||||
if ( action && action.source ) {
|
||||
cleanState.fieldSources = updateFieldSources(
|
||||
cleanState.fieldSources,
|
||||
'reset',
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return cleanState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload, action ) => {
|
||||
let newState = { ...state };
|
||||
|
||||
if ( action && action.source && payload.data ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'hydrate',
|
||||
action.source
|
||||
);
|
||||
|
||||
Object.keys( payload.data ).forEach( ( fieldName ) => {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
fieldName,
|
||||
action.source
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
newState = changePersistent( newState, payload.data );
|
||||
[ ACTION_TYPES.HYDRATE ]: ( state, payload ) => {
|
||||
const newState = changePersistent( state, payload.data );
|
||||
|
||||
// Flags are not updated by `changePersistent()`.
|
||||
if ( payload.flags ) {
|
||||
|
@ -163,34 +81,12 @@ const onboardingReducer = createReducer( defaultTransient, defaultPersistent, {
|
|||
return newState;
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.SYNC_GATEWAYS ]: ( state, payload, action ) => {
|
||||
const newState = changePersistent( state, { gatewaysSynced: true } );
|
||||
|
||||
if ( action && action.source ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'gatewaysSynced',
|
||||
action.source
|
||||
);
|
||||
}
|
||||
|
||||
return newState;
|
||||
[ ACTION_TYPES.SYNC_GATEWAYS ]: ( state ) => {
|
||||
return changePersistent( state, { gatewaysSynced: true } );
|
||||
},
|
||||
|
||||
[ ACTION_TYPES.REFRESH_GATEWAYS ]: ( state, payload, action ) => {
|
||||
const newState = changePersistent( state, { gatewaysRefreshed: true } );
|
||||
|
||||
if ( action ) {
|
||||
if ( action.source ) {
|
||||
newState.fieldSources = updateFieldSources(
|
||||
newState.fieldSources,
|
||||
'gatewaysRefreshed',
|
||||
action.source
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
[ ACTION_TYPES.REFRESH_GATEWAYS ]: ( state ) => {
|
||||
return changePersistent( state, { gatewaysRefreshed: true } );
|
||||
},
|
||||
} );
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export const persistentData = ( state ) => {
|
|||
};
|
||||
|
||||
export const transientData = ( state ) => {
|
||||
const { data, flags, fieldSources, ...transientState } = getState( state );
|
||||
const { data, flags, ...transientState } = getState( state );
|
||||
return transientState || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
|
@ -26,43 +26,6 @@ export const flags = ( state ) => {
|
|||
return getState( state ).flags || EMPTY_OBJ;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the source information for a specific field.
|
||||
* @param {Object} state - Store state.
|
||||
* @param {string} fieldName
|
||||
*/
|
||||
export const getFieldSource = ( state, fieldName ) => {
|
||||
const fieldSources = state?.fieldSources || {};
|
||||
const fieldSource = fieldSources[ fieldName ];
|
||||
|
||||
if ( ! fieldSource ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fieldSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all field sources for debugging.
|
||||
* @param {Object} state - Store state.
|
||||
* @return {Object} All field source information.
|
||||
*/
|
||||
export const getAllFieldSources = ( state ) => {
|
||||
const currentState = getState( state );
|
||||
return currentState.fieldSources || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the connection button clicked status.
|
||||
*
|
||||
* @param {Object} state - Store state.
|
||||
* @return {boolean} Whether the connection button has been clicked.
|
||||
*/
|
||||
export const getConnectionButtonClicked = ( state ) => {
|
||||
const transient = transientData( state );
|
||||
return transient.connectionButtonClicked || false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns details about products and capabilities to use for the production login link in
|
||||
* the last onboarding step.
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Action Types: Define unique identifiers for actions across all store modules.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
export default {
|
||||
UPDATE_SOURCES: 'ppcp/tracking/UPDATE_SOURCES',
|
||||
CLEAR_SOURCES: 'ppcp/tracking/CLEAR_SOURCES',
|
||||
CLEAR_FIELD_SOURCE: 'ppcp/tracking/CLEAR_FIELD_SOURCE',
|
||||
RESET: 'ppcp/tracking/RESET',
|
||||
};
|
66
modules/ppcp-settings/resources/js/data/tracking/actions.js
Normal file
66
modules/ppcp-settings/resources/js/data/tracking/actions.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* Action Creators: Define functions to create action objects.
|
||||
*
|
||||
* These functions update state or trigger side effects (e.g., async operations).
|
||||
* Two main actions: updateSources and clearSources.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
/**
|
||||
* Updates the source tracking information for a specific field.
|
||||
*
|
||||
* Records when and from where a field value was changed, enabling.
|
||||
* audit trails and debugging capabilities for state modifications.
|
||||
*
|
||||
* @param {string} storeName - Name of the store containing the field.
|
||||
* @param {string} fieldName - Name of the field being tracked.
|
||||
* @param {string} source - Source identifier for the change (e.g., 'user', 'system').
|
||||
* @return {Object} Action object with type UPDATE_SOURCES and tracking payload.
|
||||
*/
|
||||
export const updateSources = ( storeName, fieldName, source ) => ( {
|
||||
type: ACTION_TYPES.UPDATE_SOURCES,
|
||||
payload: {
|
||||
storeName,
|
||||
fieldName,
|
||||
source,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
} );
|
||||
|
||||
/**
|
||||
* Clears source tracking information for fields or stores.
|
||||
*
|
||||
* Can clear tracking for a specific field, all fields in a store,
|
||||
* or all tracking data.
|
||||
*
|
||||
* @param {string|null} storeName - Name of the store (optional, null clears all stores).
|
||||
* @param {string|null} fieldName - Name of the field (optional, null clears all fields in store).
|
||||
* @return {Object} Action object with appropriate clear type and payload.
|
||||
*/
|
||||
export const clearSources = ( storeName = null, fieldName = null ) => {
|
||||
if ( fieldName ) {
|
||||
return {
|
||||
type: ACTION_TYPES.CLEAR_FIELD_SOURCE,
|
||||
payload: { storeName, fieldName },
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: ACTION_TYPES.CLEAR_SOURCES,
|
||||
payload: { storeName },
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Resets all source tracking data.
|
||||
*
|
||||
* Clears all stored tracking information across all stores and fields,
|
||||
* returning the tracking store to its initial state.
|
||||
*
|
||||
* @return {Object} Action object with type RESET.
|
||||
*/
|
||||
export const reset = () => ( {
|
||||
type: ACTION_TYPES.RESET,
|
||||
} );
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Constants: Field source tracking store.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Name of the field source tracking store.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const STORE_NAME = 'wc/paypal/tracking';
|
33
modules/ppcp-settings/resources/js/data/tracking/index.js
Normal file
33
modules/ppcp-settings/resources/js/data/tracking/index.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* Store setup: Field source tracking store.
|
||||
*
|
||||
* Initializes and registers the field source tracking store.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { createReduxStore, register } from '@wordpress/data';
|
||||
|
||||
import { STORE_NAME } from './constants';
|
||||
import reducer from './reducer';
|
||||
import * as selectors from './selectors';
|
||||
import * as actions from './actions';
|
||||
|
||||
/**
|
||||
* Initialize the field source tracking store.
|
||||
*
|
||||
* @return {boolean} True if initialization succeeded
|
||||
*/
|
||||
export const initStore = () => {
|
||||
const store = createReduxStore( STORE_NAME, {
|
||||
reducer,
|
||||
actions,
|
||||
selectors,
|
||||
} );
|
||||
|
||||
register( store );
|
||||
|
||||
return Boolean( wp.data.select( STORE_NAME ) );
|
||||
};
|
||||
|
||||
export { selectors, STORE_NAME };
|
64
modules/ppcp-settings/resources/js/data/tracking/reducer.js
Normal file
64
modules/ppcp-settings/resources/js/data/tracking/reducer.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Reducer: Field source tracking store.
|
||||
*
|
||||
* State structure: { storeName: { fieldName: { source, timestamp } } }
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import ACTION_TYPES from './action-types';
|
||||
|
||||
const initialState = {};
|
||||
|
||||
const trackingReducer = ( state = initialState, action ) => {
|
||||
switch ( action.type ) {
|
||||
case ACTION_TYPES.UPDATE_SOURCES: {
|
||||
const { storeName, fieldName, source, timestamp } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
[ storeName ]: {
|
||||
...( state[ storeName ] || {} ),
|
||||
[ fieldName ]: { source, timestamp },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ACTION_TYPES.CLEAR_FIELD_SOURCE: {
|
||||
const { storeName, fieldName } = action.payload;
|
||||
const storeData = state[ storeName ];
|
||||
|
||||
if ( ! storeData ) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newStoreData = { ...storeData };
|
||||
delete newStoreData[ fieldName ];
|
||||
|
||||
return {
|
||||
...state,
|
||||
[ storeName ]: newStoreData,
|
||||
};
|
||||
}
|
||||
|
||||
case ACTION_TYPES.CLEAR_SOURCES: {
|
||||
const { storeName } = action.payload;
|
||||
|
||||
if ( storeName ) {
|
||||
const newState = { ...state };
|
||||
delete newState[ storeName ];
|
||||
return newState;
|
||||
}
|
||||
|
||||
return initialState;
|
||||
}
|
||||
|
||||
case ACTION_TYPES.RESET:
|
||||
return initialState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default trackingReducer;
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Selectors: Field source tracking store.
|
||||
*
|
||||
* Accessors for field source information.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get field source for specific field.
|
||||
*
|
||||
* @param {Object} state - Store state.
|
||||
* @param {string} storeName - Name of the store.
|
||||
* @param {string} fieldName - Name of the field.
|
||||
* @return {Object|null} Source information or null.
|
||||
*/
|
||||
export const getFieldSource = ( state, storeName, fieldName ) => {
|
||||
return state?.[ storeName ]?.[ fieldName ] || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all field sources for a store.
|
||||
*
|
||||
* @param {Object} state - Store state.
|
||||
* @param {string} storeName - Name of the store.
|
||||
* @return {Object} All field sources for the store.
|
||||
*/
|
||||
export const getStoreFieldSources = ( state, storeName ) => {
|
||||
return state?.[ storeName ] || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all field sources across all stores.
|
||||
*
|
||||
* @param {Object} state - Store state.
|
||||
* @return {Object} All field sources.
|
||||
*/
|
||||
export const getAllFieldSources = ( state ) => {
|
||||
return state || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if field is tracked.
|
||||
*
|
||||
* @param {Object} state - Store state.
|
||||
* @param {string} storeName - Name of the store.
|
||||
* @param {string} fieldName - Name of the field.
|
||||
* @return {boolean} True if field is tracked.
|
||||
*/
|
||||
export const isFieldTracked = ( state, storeName, fieldName ) => {
|
||||
return !! getFieldSource( state, storeName, fieldName );
|
||||
};
|
|
@ -1,6 +1,17 @@
|
|||
/**
|
||||
* Utility functions for store management and hooks.
|
||||
*
|
||||
* Provides core functionality for creating reducers, managing state updates,
|
||||
* and implementing custom React hooks for store interaction.
|
||||
*
|
||||
* @file
|
||||
*/
|
||||
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { useCallback } from '@wordpress/element';
|
||||
|
||||
import { STORE_NAME as TRACKING_STORE } from '../data/tracking/constants';
|
||||
|
||||
/**
|
||||
* Updates an object with new values, filtering based on allowed keys.
|
||||
*
|
||||
|
@ -86,6 +97,8 @@ export const createReducer = (
|
|||
};
|
||||
|
||||
/**
|
||||
* Creates custom React hooks for accessing store state.
|
||||
*
|
||||
* Returns an object with two hooks:
|
||||
* - useTransient( prop )
|
||||
* - usePersistent( prop )
|
||||
|
@ -93,13 +106,13 @@ export const createReducer = (
|
|||
* Both hooks have a similar syntax to the native "useState( prop )" hook, but provide access to
|
||||
* a transient or persistent property in the relevant Redux store.
|
||||
*
|
||||
* Sample:
|
||||
*
|
||||
* @example
|
||||
* const { useTransient } = createHooksForStore( STORE_NAME );
|
||||
* const [ isReady, setIsReady ] = useTransient( 'isReady' );
|
||||
* setIsReady( true, 'user' ); // Optional source tracking
|
||||
*
|
||||
* @param {string} storeName Store name.
|
||||
* @return {{useTransient, usePersistent}} Store hooks.
|
||||
* @return {{useTransient: Function, usePersistent: Function}} Store hooks.
|
||||
*/
|
||||
export const createHooksForStore = ( storeName ) => {
|
||||
const createHook = ( selector, dispatcher ) => ( key ) => {
|
||||
|
@ -123,17 +136,32 @@ export const createHooksForStore = ( storeName ) => {
|
|||
);
|
||||
|
||||
const actions = useDispatch( storeName );
|
||||
const trackingActions = useDispatch( TRACKING_STORE );
|
||||
|
||||
const setValue = useCallback(
|
||||
( newValue, source ) => {
|
||||
if ( ! actions?.[ dispatcher ] ) {
|
||||
throw new Error(
|
||||
`Please create the action "${ dispatcher }" for store "${ storeName }"`
|
||||
( newValue, source = null ) => {
|
||||
try {
|
||||
// Record field source before updating the store.
|
||||
if ( source && trackingActions?.updateSources ) {
|
||||
trackingActions.updateSources( storeName, key, source );
|
||||
}
|
||||
|
||||
// Update the store state (triggers subscription manager).
|
||||
if ( ! actions?.[ dispatcher ] ) {
|
||||
throw new Error(
|
||||
`Please create the action "${ dispatcher }" for store "${ storeName }"`
|
||||
);
|
||||
}
|
||||
|
||||
actions[ dispatcher ]( key, newValue );
|
||||
} catch ( error ) {
|
||||
console.error(
|
||||
`Error updating ${ key } in ${ storeName }:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
actions[ dispatcher ]( key, newValue, source );
|
||||
},
|
||||
[ actions, key ]
|
||||
[ actions, key, trackingActions ]
|
||||
);
|
||||
|
||||
return [ value, setValue ];
|
||||
|
|
|
@ -216,11 +216,18 @@ const createAccountTypeTrackingConfig = () => {
|
|||
transform: ( value ) => ( {
|
||||
selected_value: value === true ? 'personal' : 'business',
|
||||
} ),
|
||||
rules: {
|
||||
allowedSources: [ 'user' ],
|
||||
},
|
||||
} );
|
||||
};
|
||||
|
||||
const createProductsTrackingConfig = () => {
|
||||
return createArrayFieldTrackingConfig( 'products', 'persistent' );
|
||||
return createArrayFieldTrackingConfig( 'products', 'persistent', {
|
||||
rules: {
|
||||
allowedSources: [ 'user' ],
|
||||
},
|
||||
} );
|
||||
};
|
||||
|
||||
const createPaymentOptionsTrackingConfig = () => {
|
||||
|
@ -231,6 +238,9 @@ const createPaymentOptionsTrackingConfig = () => {
|
|||
transform: ( value ) => ( {
|
||||
selected_value: value === true ? 'expanded' : 'no_cards',
|
||||
} ),
|
||||
rules: {
|
||||
allowedSources: [ 'user' ],
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -240,6 +250,9 @@ const createCompletedTrackingConfig = () => {
|
|||
transform: ( value ) => ( {
|
||||
completed: value === true,
|
||||
} ),
|
||||
rules: {
|
||||
allowedSources: [ 'system' ],
|
||||
},
|
||||
} );
|
||||
};
|
||||
|
||||
|
@ -248,13 +261,20 @@ const createConnectionButtonTrackingConfig = () => {
|
|||
};
|
||||
|
||||
const createSandboxTrackingConfig = () => {
|
||||
return createBooleanFieldTrackingConfig( 'useSandbox', 'persistent' );
|
||||
return createBooleanFieldTrackingConfig(
|
||||
'useSandbox',
|
||||
'persistent',
|
||||
'enabled',
|
||||
'disabled'
|
||||
);
|
||||
};
|
||||
|
||||
const createManualConnectionTrackingConfig = () => {
|
||||
return createBooleanFieldTrackingConfig(
|
||||
'useManualConnection',
|
||||
'persistent'
|
||||
'persistent',
|
||||
'enabled',
|
||||
'disabled'
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -152,9 +152,8 @@ export class FunnelTrackingService {
|
|||
if ( fieldRules ) {
|
||||
return fieldRules.allowedSources.includes( source );
|
||||
}
|
||||
|
||||
// Unknown fields are not tracked.
|
||||
return false;
|
||||
// No rules = accept all sources.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
import { shouldTrackFieldSource, getFieldValue } from './utils';
|
||||
import { getFieldValue } from './utils';
|
||||
|
||||
/**
|
||||
* Manages unified subscriptions for stores that are tracked by multiple funnels.
|
||||
|
@ -156,9 +156,6 @@ export class SubscriptionManager {
|
|||
);
|
||||
}
|
||||
} );
|
||||
|
||||
// Handle field source clearing AFTER all funnels have processed.
|
||||
this.handleFieldSourceClearing( storeName, store, registrations );
|
||||
} catch ( error ) {
|
||||
console.error(
|
||||
`[SubscriptionManager] Error handling store change for ${ storeName }:`,
|
||||
|
@ -175,13 +172,8 @@ export class SubscriptionManager {
|
|||
* @param {Object} store - Store object.
|
||||
*/
|
||||
processFunnelForStore( storeName, registration, select, store ) {
|
||||
const {
|
||||
funnelId,
|
||||
trackingService,
|
||||
fieldRules,
|
||||
fieldConfigs,
|
||||
trackingCondition,
|
||||
} = registration;
|
||||
const { trackingService, fieldRules, fieldConfigs, trackingCondition } =
|
||||
registration;
|
||||
|
||||
// Step 1: Evaluate tracking condition for this funnel.
|
||||
const conditionMet = this.evaluateTrackingCondition(
|
||||
|
@ -243,6 +235,86 @@ export class SubscriptionManager {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process field changes for a specific funnel.
|
||||
* Retrieves field sources from the tracking store to determine tracking eligibility.
|
||||
* @param {Function} select - WordPress data select function.
|
||||
* @param {Object} store - Store object.
|
||||
* @param {string} storeName - Store name.
|
||||
* @param {Object} registration - Funnel registration.
|
||||
* @param {Array} fieldConfigs - Field configurations.
|
||||
* @param {Object} fieldRules - Field rules.
|
||||
* @param {Object} trackingService - Tracking service.
|
||||
*/
|
||||
processFieldChangesForFunnel(
|
||||
select,
|
||||
store,
|
||||
storeName,
|
||||
registration,
|
||||
fieldConfigs,
|
||||
fieldRules,
|
||||
trackingService
|
||||
) {
|
||||
fieldConfigs.forEach( ( fieldConfig ) => {
|
||||
try {
|
||||
const currentValue = getFieldValue(
|
||||
select,
|
||||
storeName,
|
||||
fieldConfig
|
||||
);
|
||||
const previousValue =
|
||||
registration.previousValues[ fieldConfig.fieldName ];
|
||||
|
||||
// Skip if no change.
|
||||
if ( currentValue === previousValue ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get field source from the tracking store.
|
||||
const trackingStore = select( 'wc/paypal/tracking' );
|
||||
const fieldSource =
|
||||
trackingStore?.getFieldSource?.(
|
||||
storeName,
|
||||
fieldConfig.fieldName
|
||||
)?.source || '';
|
||||
|
||||
// Check if this source should be tracked for this funnel.
|
||||
const shouldTrack = trackingService.shouldTrackFieldSource(
|
||||
fieldConfig.fieldName,
|
||||
fieldSource
|
||||
);
|
||||
|
||||
if ( ! shouldTrack ) {
|
||||
// Update previous value but don't track.
|
||||
registration.previousValues[ fieldConfig.fieldName ] =
|
||||
currentValue;
|
||||
return;
|
||||
}
|
||||
|
||||
// Process tracked change for this funnel.
|
||||
this.processTrackedChangeForFunnel(
|
||||
fieldConfig,
|
||||
previousValue,
|
||||
currentValue,
|
||||
fieldSource,
|
||||
trackingService,
|
||||
select,
|
||||
storeName,
|
||||
registration
|
||||
);
|
||||
|
||||
// Update previous value.
|
||||
registration.previousValues[ fieldConfig.fieldName ] =
|
||||
currentValue;
|
||||
} catch ( error ) {
|
||||
console.error(
|
||||
`[SubscriptionManager] Error processing field ${ fieldConfig.fieldName } for funnel ${ registration.funnelId }:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate tracking condition for a specific funnel.
|
||||
* @param {Function} select - WordPress data select function.
|
||||
|
@ -338,24 +410,12 @@ export class SubscriptionManager {
|
|||
*/
|
||||
isStoreReadyForTracking( store, registration ) {
|
||||
const isReady = store.transientData?.()?.isReady;
|
||||
const hasHydrationSource = store.getAllFieldSources?.()?.hydrate;
|
||||
|
||||
const hasFieldSourceMethods =
|
||||
typeof store.getFieldSource === 'function' &&
|
||||
typeof store.getAllFieldSources === 'function';
|
||||
|
||||
if ( ! hasFieldSourceMethods ) {
|
||||
return isReady || registration.initializationAttempts > 50;
|
||||
}
|
||||
|
||||
if ( isReady ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( hasHydrationSource ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback for stores that take time to initialize.
|
||||
return registration.initializationAttempts > 50;
|
||||
}
|
||||
|
||||
|
@ -438,83 +498,6 @@ export class SubscriptionManager {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process field changes for a specific funnel.
|
||||
* @param {Function} select - WordPress data select function.
|
||||
* @param {Object} store - Store object.
|
||||
* @param {string} storeName - Store name.
|
||||
* @param {Object} registration - Funnel registration.
|
||||
* @param {Array} fieldConfigs - Field configurations.
|
||||
* @param {Object} fieldRules - Field rules.
|
||||
* @param {Object} trackingService - Tracking service.
|
||||
*/
|
||||
processFieldChangesForFunnel(
|
||||
select,
|
||||
store,
|
||||
storeName,
|
||||
registration,
|
||||
fieldConfigs,
|
||||
fieldRules,
|
||||
trackingService
|
||||
) {
|
||||
fieldConfigs.forEach( ( fieldConfig ) => {
|
||||
try {
|
||||
const currentValue = getFieldValue(
|
||||
select,
|
||||
storeName,
|
||||
fieldConfig
|
||||
);
|
||||
const previousValue =
|
||||
registration.previousValues[ fieldConfig.fieldName ];
|
||||
|
||||
// Skip if no change.
|
||||
if ( currentValue === previousValue ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get field source.
|
||||
const fieldSource =
|
||||
store.getFieldSource?.( fieldConfig.fieldName )?.source ||
|
||||
'';
|
||||
|
||||
// Check if this source should be tracked for this funnel.
|
||||
const shouldTrack = shouldTrackFieldSource(
|
||||
fieldConfig.fieldName,
|
||||
fieldSource,
|
||||
fieldRules
|
||||
);
|
||||
|
||||
if ( ! shouldTrack ) {
|
||||
// Update previous value but don't track.
|
||||
registration.previousValues[ fieldConfig.fieldName ] =
|
||||
currentValue;
|
||||
return;
|
||||
}
|
||||
|
||||
// Process tracked change for this funnel.
|
||||
this.processTrackedChangeForFunnel(
|
||||
fieldConfig,
|
||||
previousValue,
|
||||
currentValue,
|
||||
fieldSource,
|
||||
trackingService,
|
||||
select,
|
||||
storeName,
|
||||
registration
|
||||
);
|
||||
|
||||
// Update previous value.
|
||||
registration.previousValues[ fieldConfig.fieldName ] =
|
||||
currentValue;
|
||||
} catch ( error ) {
|
||||
console.error(
|
||||
`[SubscriptionManager] Error processing field ${ fieldConfig.fieldName } for funnel ${ registration.funnelId }:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a tracked change for a specific funnel.
|
||||
* @param {Object} fieldConfig - Field configuration.
|
||||
|
@ -620,7 +603,7 @@ export class SubscriptionManager {
|
|||
}
|
||||
} );
|
||||
|
||||
// Add step information after aggregation
|
||||
// Add step information after aggregation.
|
||||
this.enhanceMetadataWithStepInfo( metadata, registration );
|
||||
|
||||
return metadata;
|
||||
|
@ -639,48 +622,33 @@ export class SubscriptionManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Enhance metadata with step information from funnel configuration
|
||||
* @param {Object} metadata - The metadata object to enhance
|
||||
* @param {Object} registration - Funnel registration object
|
||||
* Enhance metadata with step information from funnel configuration.
|
||||
* @param {Object} metadata - The metadata object to enhance.
|
||||
* @param {Object} registration - Funnel registration object.
|
||||
*/
|
||||
enhanceMetadataWithStepInfo( metadata, registration ) {
|
||||
try {
|
||||
// Get the step number from aggregated data
|
||||
// Get the step number from aggregated data.
|
||||
const stepNumber = metadata.step;
|
||||
|
||||
// Get step info directly from registration
|
||||
// Get step info directly from registration.
|
||||
const stepInfo = registration.stepInfo || {};
|
||||
|
||||
// Add step name if we can map it
|
||||
// Add step name if we can map it.
|
||||
if ( typeof stepNumber === 'number' && stepInfo[ stepNumber ] ) {
|
||||
const stepData = stepInfo[ stepNumber ];
|
||||
metadata.stepName =
|
||||
typeof stepData === 'string' ? stepData : stepData.name;
|
||||
}
|
||||
|
||||
// Add currentStep alias for backward compatibility with existing translations
|
||||
// Add currentStep alias for backward compatibility with existing translations.
|
||||
metadata.currentStep = stepNumber;
|
||||
|
||||
// Ensure step fields are properly typed (not string 'null')
|
||||
// Ensure step fields are properly typed (not string 'null').
|
||||
if ( stepNumber === null || stepNumber === undefined ) {
|
||||
metadata.step = null;
|
||||
metadata.currentStep = null;
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log(
|
||||
`[SubscriptionManager] Step enhancement debug for ${ registration.funnelId }:`,
|
||||
{
|
||||
stepNumber,
|
||||
stepNumberType: typeof stepNumber,
|
||||
currentStep: metadata.currentStep,
|
||||
stepName: metadata.stepName,
|
||||
stepInfo,
|
||||
stepInfoKeys: Object.keys( stepInfo ),
|
||||
allMetadataKeys: Object.keys( metadata ),
|
||||
contributingStores: metadata.contributingStores,
|
||||
}
|
||||
);
|
||||
} catch ( error ) {
|
||||
console.warn(
|
||||
`[SubscriptionManager] Error enhancing metadata with step info:`,
|
||||
|
@ -709,7 +677,7 @@ export class SubscriptionManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Safely call a store method with fallback (moved from utils).
|
||||
* Safely call a store method with fallback.
|
||||
* @param {Object} store - The store object.
|
||||
* @param {string} method - The method name to call.
|
||||
* @param {*} fallback - Fallback value if method fails.
|
||||
|
@ -727,45 +695,6 @@ export class SubscriptionManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle field source clearing after all funnels have processed.
|
||||
* @param {string} storeName - Store name.
|
||||
* @param {Object} store - Store object.
|
||||
* @param {Array} registrations - All funnel registrations for this store.
|
||||
*/
|
||||
handleFieldSourceClearing( storeName, store, registrations ) {
|
||||
if ( ! store.clearFieldSource || ! store.getAllFieldSources ) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all field sources that were processed.
|
||||
const allFieldSources = store.getAllFieldSources() || {};
|
||||
|
||||
// Collect all field names that any active funnel is tracking.
|
||||
const trackedFields = new Set();
|
||||
registrations.forEach( ( registration ) => {
|
||||
if ( registration.isActive ) {
|
||||
registration.fieldConfigs.forEach( ( fieldConfig ) => {
|
||||
trackedFields.add( fieldConfig.fieldName );
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
// Clear sources for fields that were tracked.
|
||||
Object.keys( allFieldSources ).forEach( ( fieldName ) => {
|
||||
if ( trackedFields.has( fieldName ) ) {
|
||||
store.clearFieldSource( fieldName );
|
||||
}
|
||||
} );
|
||||
} catch ( error ) {
|
||||
console.error(
|
||||
`[SubscriptionManager] Error clearing field sources for ${ storeName }:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a funnel from a store.
|
||||
* @param {string} storeName - Store name.
|
||||
|
|
|
@ -7,47 +7,6 @@
|
|||
* @file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines if a field with a given source should be tracked.
|
||||
*
|
||||
* @param {string} fieldName - The name of the field.
|
||||
* @param {string} source - The source of the field change.
|
||||
* @param {Object} storeRules - Rules for the specific store.
|
||||
* @return {boolean} Whether the field should be tracked.
|
||||
*/
|
||||
export function shouldTrackFieldSource( fieldName, source, storeRules ) {
|
||||
if ( ! source ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! storeRules ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! storeRules[ fieldName ] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get and validate field rules.
|
||||
const fieldRule = storeRules[ fieldName ];
|
||||
const allowedSources = fieldRule.allowedSources;
|
||||
|
||||
if ( ! Array.isArray( allowedSources ) ) {
|
||||
console.warn(
|
||||
`[TRACK CHECK] Invalid allowedSources for ${ fieldName }:`,
|
||||
{ allowedSources, type: typeof allowedSources }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( allowedSources.length === 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if source is allowed.
|
||||
return allowedSources.includes( source );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of a field from the store with comprehensive error handling.
|
||||
*
|
||||
|
|
|
@ -31,10 +31,7 @@ export const createFieldTrackingConfig = (
|
|||
: select( storeName ).transientData();
|
||||
return data?.[ fieldName ];
|
||||
} ),
|
||||
rules: {
|
||||
allowedSources: [ 'user' ], // Default to user-only.
|
||||
...options.rules,
|
||||
},
|
||||
...( options.rules && { rules: options.rules } ),
|
||||
...options,
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue