Display CES modal once per action per store (https://github.com/woocommerce/woocommerce-admin/pull/5352)
* Spike out customer effort score * Refactor CustomerEffortScore as Package (https://github.com/woocommerce/woocommerce-admin/pull/5342) * Tidy up _webpack.config.js * Fix linter issues * refactor setting up CES tracking, add try..catch around loading from localStorage * Add CES feature toggle (https://github.com/woocommerce/woocommerce-admin/pull/5387) * Add feature toggle to only enable the customer effort score in development * Move check into Loader * fix logic 🙄 Co-authored-by: Rebecca Scott <me@becdetat.com> * Add client-side check of the feature flag * fix tabs in config * Fix comment * Use product lifecycle and options * Add product count to tracks props * Display CES modal once per event per store * Remove TODO * appease the formatting gods * Use Loader::load_features instead of DIY * Drop CustomerEffortScoreTracks singleton * Fix tracks event name * bump ci * Pull package-lock.json from main * Refactor to remove some duplication Co-authored-by: Rebecca Scott <me@becdetat.com> Co-authored-by: Adrian Duffell <9312929+adrianduffell@users.noreply.github.com>
This commit is contained in:
parent
15b45c9db4
commit
00eeede732
|
@ -47,7 +47,7 @@ function CustomerEffortScoreTracksContainer( {
|
||||||
<CustomerEffortScoreTracks
|
<CustomerEffortScoreTracks
|
||||||
key={ index }
|
key={ index }
|
||||||
initiallyVisible={ true }
|
initiallyVisible={ true }
|
||||||
trackName={ item.track_name }
|
action={ item.action }
|
||||||
label={ item.label }
|
label={ item.label }
|
||||||
trackProps={ item.props || {} }
|
trackProps={ item.props || {} }
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,9 +6,10 @@ import PropTypes from 'prop-types';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import CustomerEffortScore from '@woocommerce/customer-effort-score';
|
import CustomerEffortScore from '@woocommerce/customer-effort-score';
|
||||||
import { compose } from '@wordpress/compose';
|
import { compose } from '@wordpress/compose';
|
||||||
import { withSelect } from '@wordpress/data';
|
import { withSelect, withDispatch } from '@wordpress/data';
|
||||||
import { OPTIONS_STORE_NAME, MONTH } from '@woocommerce/data';
|
import { OPTIONS_STORE_NAME, MONTH } from '@woocommerce/data';
|
||||||
|
|
||||||
|
const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';
|
||||||
const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
||||||
'woocommerce_admin_install_timestamp';
|
'woocommerce_admin_install_timestamp';
|
||||||
|
|
||||||
|
@ -18,28 +19,50 @@ const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME =
|
||||||
*
|
*
|
||||||
* @param {Object} props Component props.
|
* @param {Object} props Component props.
|
||||||
* @param {boolean} props.initiallyVisible Whether or not the tracks modal is initially visible.
|
* @param {boolean} props.initiallyVisible Whether or not the tracks modal is initially visible.
|
||||||
* @param {string} props.trackName The name sent to Tracks.
|
* @param {string} props.action The action name sent to Tracks.
|
||||||
* @param {Object} props.trackProps Additional props sent to Tracks.
|
* @param {Object} props.trackProps Additional props sent to Tracks.
|
||||||
* @param {string} props.label The label displayed in the modal.
|
* @param {string} props.label The label displayed in the modal.
|
||||||
* @param {boolean} props.resolving Are values still being resolving.
|
* @param {Array} props.cesShownForActions The array of actions that the CES modal has been shown for.
|
||||||
|
* @param {boolean} props.resolving Are values still being resolved.
|
||||||
* @param {number} props.storeAge The age of the store in months.
|
* @param {number} props.storeAge The age of the store in months.
|
||||||
|
* @param {Function} props.updateOptions Function to update options.
|
||||||
*/
|
*/
|
||||||
function CustomerEffortScoreTracks( {
|
function CustomerEffortScoreTracks( {
|
||||||
initiallyVisible,
|
initiallyVisible,
|
||||||
trackName,
|
action,
|
||||||
trackProps,
|
trackProps,
|
||||||
label,
|
label,
|
||||||
|
cesShownForActions,
|
||||||
resolving,
|
resolving,
|
||||||
storeAge,
|
storeAge,
|
||||||
|
updateOptions,
|
||||||
} ) {
|
} ) {
|
||||||
const [ visible, setVisible ] = useState( initiallyVisible );
|
const [ visible, setVisible ] = useState( initiallyVisible );
|
||||||
|
const [ shown, setShown ] = useState( false );
|
||||||
|
|
||||||
if ( resolving ) {
|
if ( resolving ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( cesShownForActions.indexOf( action ) !== -1 && ! shown ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the `shown` state value to only update the shown_for_actions option
|
||||||
|
// once.
|
||||||
|
if ( visible && ! shown ) {
|
||||||
|
setShown( true );
|
||||||
|
updateOptions( {
|
||||||
|
[ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [
|
||||||
|
action,
|
||||||
|
...cesShownForActions,
|
||||||
|
],
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
const trackCallback = ( score ) => {
|
const trackCallback = ( score ) => {
|
||||||
recordEvent( trackName, {
|
recordEvent( 'ces_feedback', {
|
||||||
|
action,
|
||||||
score,
|
score,
|
||||||
store_age: storeAge,
|
store_age: storeAge,
|
||||||
...trackProps,
|
...trackProps,
|
||||||
|
@ -65,9 +88,9 @@ CustomerEffortScoreTracks.propTypes = {
|
||||||
*/
|
*/
|
||||||
initiallyVisible: PropTypes.bool,
|
initiallyVisible: PropTypes.bool,
|
||||||
/**
|
/**
|
||||||
* The name sent to Tracks.
|
* The action name sent to Tracks.
|
||||||
*/
|
*/
|
||||||
trackName: PropTypes.string.isRequired,
|
action: PropTypes.string.isRequired,
|
||||||
/**
|
/**
|
||||||
* Additional props sent to Tracks.
|
* Additional props sent to Tracks.
|
||||||
*/
|
*/
|
||||||
|
@ -76,6 +99,10 @@ CustomerEffortScoreTracks.propTypes = {
|
||||||
* The label displayed in the modal.
|
* The label displayed in the modal.
|
||||||
*/
|
*/
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* The array of actions that the CES modal has been shown for.
|
||||||
|
*/
|
||||||
|
cesShownForActions: PropTypes.arrayOf( PropTypes.string ).isRequired,
|
||||||
/**
|
/**
|
||||||
* Whether props are still being resolved.
|
* Whether props are still being resolved.
|
||||||
*/
|
*/
|
||||||
|
@ -90,20 +117,31 @@ export default compose(
|
||||||
withSelect( ( select ) => {
|
withSelect( ( select ) => {
|
||||||
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
|
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
|
||||||
|
|
||||||
|
const cesShownForActions =
|
||||||
|
getOption( SHOWN_FOR_ACTIONS_OPTION_NAME ) || [];
|
||||||
|
|
||||||
const adminInstallTimestamp =
|
const adminInstallTimestamp =
|
||||||
getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0;
|
getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0;
|
||||||
// Date.now() is ms since Unix epoch, adminInstallTimestamp is in
|
// Date.now() is ms since Unix epoch, adminInstallTimestamp is in
|
||||||
// seconds since Unix epoch.
|
// seconds since Unix epoch.
|
||||||
const storeAgeInSeconds = Date.now() - adminInstallTimestamp * 1000;
|
const storeAgeInMs = Date.now() - adminInstallTimestamp * 1000;
|
||||||
const storeAge = Math.round( storeAgeInSeconds / MONTH );
|
const storeAge = Math.round( storeAgeInMs / MONTH );
|
||||||
|
|
||||||
const resolving = isResolving( 'getOption', [
|
const resolving =
|
||||||
ADMIN_INSTALL_TIMESTAMP_OPTION_NAME,
|
isResolving( 'getOption', [ SHOWN_FOR_ACTIONS_OPTION_NAME ] ) ||
|
||||||
] );
|
isResolving( 'getOption', [ ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ] );
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
cesShownForActions,
|
||||||
storeAge,
|
storeAge,
|
||||||
resolving,
|
resolving,
|
||||||
};
|
};
|
||||||
|
} ),
|
||||||
|
withDispatch( ( dispatch ) => {
|
||||||
|
const { updateOptions } = dispatch( OPTIONS_STORE_NAME );
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateOptions,
|
||||||
|
};
|
||||||
} )
|
} )
|
||||||
)( CustomerEffortScoreTracks );
|
)( CustomerEffortScoreTracks );
|
||||||
|
|
|
@ -13,10 +13,32 @@ defined( 'ABSPATH' ) || exit;
|
||||||
* Triggers customer effort score on several different actions.
|
* Triggers customer effort score on several different actions.
|
||||||
*/
|
*/
|
||||||
class CustomerEffortScoreTracks {
|
class CustomerEffortScoreTracks {
|
||||||
const CES_TRACKS_QUEUE_OPTION_NAME = 'woocommerce_ces_tracks_queue';
|
/**
|
||||||
|
* Option name for the CES Tracks queue.
|
||||||
|
*/
|
||||||
|
const CES_TRACKS_QUEUE_OPTION_NAME
|
||||||
|
= 'woocommerce_ces_tracks_queue';
|
||||||
|
|
||||||
const CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME
|
/**
|
||||||
= 'woocommerce_clear_ces_tracks_queue_for_page';
|
* Option name for the clear CES Tracks queue for page.
|
||||||
|
*/
|
||||||
|
const CLEAR_CES_TRACKS_QUEUE_FOR_PAGE_OPTION_NAME =
|
||||||
|
'woocommerce_clear_ces_tracks_queue_for_page';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option name for the set of actions that have been shown.
|
||||||
|
*/
|
||||||
|
const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action name for product add/publish.
|
||||||
|
*/
|
||||||
|
const PRODUCT_ADD_PUBLISH_ACTION_NAME = 'product_add_publish';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action name for product update.
|
||||||
|
*/
|
||||||
|
const PRODUCT_UPDATE_ACTION_NAME = 'product_update';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor. Sets up filters to hook into WooCommerce.
|
* Constructor. Sets up filters to hook into WooCommerce.
|
||||||
|
@ -88,14 +110,50 @@ class CustomerEffortScoreTracks {
|
||||||
return $product_count;
|
return $product_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether the action has already been shown.
|
||||||
|
*
|
||||||
|
* @param string $action The action to check.
|
||||||
|
*
|
||||||
|
* @return bool Whether the action has already been shown.
|
||||||
|
*/
|
||||||
|
private function has_been_shown( $action ) {
|
||||||
|
$shown_for_features = get_option( self::SHOWN_FOR_ACTIONS_OPTION_NAME, array() );
|
||||||
|
$has_been_shown = in_array( $action, $shown_for_features, true );
|
||||||
|
|
||||||
|
return $has_been_shown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue the item to the CES tracks queue.
|
||||||
|
*
|
||||||
|
* @param object $item The item to enqueue.
|
||||||
|
*/
|
||||||
|
private function enqueue_to_ces_tracks( $item ) {
|
||||||
|
$queue = get_option(
|
||||||
|
self::CES_TRACKS_QUEUE_OPTION_NAME,
|
||||||
|
array()
|
||||||
|
);
|
||||||
|
|
||||||
|
$queue[] = $item;
|
||||||
|
|
||||||
|
update_option(
|
||||||
|
self::CES_TRACKS_QUEUE_OPTION_NAME,
|
||||||
|
$queue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue the CES survey trigger for a new product.
|
* Enqueue the CES survey trigger for a new product.
|
||||||
*/
|
*/
|
||||||
private function enqueue_ces_survey_for_new_product() {
|
private function enqueue_ces_survey_for_new_product() {
|
||||||
$queue = get_option( self::CES_TRACKS_QUEUE_OPTION_NAME, array() );
|
if ( $this->has_been_shown( self::PRODUCT_ADD_PUBLISH_ACTION_NAME ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$queue[] = array(
|
$this->enqueue_to_ces_tracks(
|
||||||
'track_name' => 'product_add_publish_effort_score',
|
array(
|
||||||
|
'action' => self::PRODUCT_ADD_PUBLISH_ACTION_NAME,
|
||||||
'label' => __(
|
'label' => __(
|
||||||
'How easy was it to add a product?',
|
'How easy was it to add a product?',
|
||||||
'woocommerce-admin'
|
'woocommerce-admin'
|
||||||
|
@ -105,19 +163,21 @@ class CustomerEffortScoreTracks {
|
||||||
'props' => array(
|
'props' => array(
|
||||||
'product_count' => $this->get_product_count(),
|
'product_count' => $this->get_product_count(),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
update_option( self::CES_TRACKS_QUEUE_OPTION_NAME, $queue );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enqueue the CES survey trigger for an existing product.
|
* Enqueue the CES survey trigger for an existing product.
|
||||||
*/
|
*/
|
||||||
private function enqueue_ces_survey_for_edited_product() {
|
private function enqueue_ces_survey_for_edited_product() {
|
||||||
$queue = get_option( self::CES_TRACKS_QUEUE_OPTION_NAME, array() );
|
if ( $this->has_been_shown( self::PRODUCT_UPDATE_ACTION_NAME ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$queue[] = array(
|
$this->enqueue_to_ces_tracks(
|
||||||
'track_name' => 'product_update_effort_score',
|
array(
|
||||||
|
'action' => self::PRODUCT_UPDATE_ACTION_NAME,
|
||||||
'label' => __(
|
'label' => __(
|
||||||
'How easy was it to edit your product?',
|
'How easy was it to edit your product?',
|
||||||
'woocommerce-admin'
|
'woocommerce-admin'
|
||||||
|
@ -127,9 +187,8 @@ class CustomerEffortScoreTracks {
|
||||||
'props' => array(
|
'props' => array(
|
||||||
'product_count' => $this->get_product_count(),
|
'product_count' => $this->get_product_count(),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
update_option( self::CES_TRACKS_QUEUE_OPTION_NAME, $queue );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in New Issue