diff --git a/packages/js/customer-effort-score/src/customer-effort-score.js b/packages/js/customer-effort-score/src/customer-effort-score.js index 6563247067d..50b8d9fc0fe 100644 --- a/packages/js/customer-effort-score/src/customer-effort-score.js +++ b/packages/js/customer-effort-score/src/customer-effort-score.js @@ -10,7 +10,7 @@ import { withDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import CustomerFeedbackModal from './customer-feedback-modal'; +import { CustomerFeedbackModal } from './customer-feedback-modal'; const noop = () => {}; @@ -29,7 +29,7 @@ const noop = () => {}; * @param {Function} props.onModalShownCallback Function to call when the modal is shown. * @param {Object} props.icon Icon (React component) to be shown on the notice. */ -export function CustomerEffortScore( { +function CustomerEffortScoreComponent( { recordScoreCallback, label, createNotice, @@ -82,7 +82,7 @@ export function CustomerEffortScore( { ); } -CustomerEffortScore.propTypes = { +CustomerEffortScoreComponent.propTypes = { /** * The function to call to record the score. */ @@ -113,7 +113,7 @@ CustomerEffortScore.propTypes = { icon: PropTypes.element, }; -export default compose( +const CustomerEffortScore = compose( withDispatch( ( dispatch ) => { const { createNotice } = dispatch( 'core/notices2' ); @@ -121,4 +121,6 @@ export default compose( createNotice, }; } ) -)( CustomerEffortScore ); +)( CustomerEffortScoreComponent ); + +export { CustomerEffortScore }; diff --git a/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx b/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx index 109febcf7cd..6cd4541bee4 100644 --- a/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx +++ b/packages/js/customer-effort-score/src/customer-feedback-modal/index.tsx @@ -30,9 +30,11 @@ import { __ } from '@wordpress/i18n'; function CustomerFeedbackModal( { recordScoreCallback, label, + defaultScore = NaN, }: { recordScoreCallback: ( score: number, comments: string ) => void; label: string; + defaultScore?: number; } ): JSX.Element | null { const options = [ { @@ -57,7 +59,7 @@ function CustomerFeedbackModal( { }, ]; - const [ score, setScore ] = useState( NaN ); + const [ score, setScore ] = useState( defaultScore || NaN ); const [ comments, setComments ] = useState( '' ); const [ showNoScoreMessage, setShowNoScoreMessage ] = useState( false ); const [ isOpen, setOpen ] = useState( true ); @@ -152,6 +154,7 @@ function CustomerFeedbackModal( { CustomerFeedbackModal.propTypes = { recordScoreCallback: PropTypes.func.isRequired, label: PropTypes.string.isRequired, + score: PropTypes.number, }; -export default CustomerFeedbackModal; +export { CustomerFeedbackModal }; diff --git a/packages/js/customer-effort-score/src/customer-feedback-simple/customer-feedback-simple.tsx b/packages/js/customer-effort-score/src/customer-feedback-simple/customer-feedback-simple.tsx index 698fce1cd2d..ed162706871 100644 --- a/packages/js/customer-effort-score/src/customer-feedback-simple/customer-feedback-simple.tsx +++ b/packages/js/customer-effort-score/src/customer-feedback-simple/customer-feedback-simple.tsx @@ -15,6 +15,7 @@ import { __ } from '@wordpress/i18n'; type CustomerFeedbackSimpleProps = { recordScoreCallback: ( score: number ) => void; label: string; + showFeedback?: boolean; }; /** @@ -31,10 +32,12 @@ type CustomerFeedbackSimpleProps = { * @param {Object} props Component props. * @param {Function} props.recordScoreCallback Function to call when the results are sent. * @param {string} props.label Question to ask the customer. + * @param {string} props.showFeedback To show feedback message. */ const CustomerFeedbackSimple: React.FC< CustomerFeedbackSimpleProps > = ( { recordScoreCallback, label, + showFeedback = false, } ) => { const options = [ { @@ -67,14 +70,14 @@ const CustomerFeedbackSimple: React.FC< CustomerFeedbackSimpleProps > = ( { const [ score, setScore ] = useState( NaN ); useEffect( () => { - if ( score !== NaN ) { + if ( ! isNaN( score ) ) { recordScoreCallback( score ); } }, [ score ] ); return (
- { isNaN( score ) ? ( + { isNaN( score ) && ! showFeedback ? ( = ( { ) ) }
- ) : null } - - { score >= 3 && ( + ) : (
= ( { CustomerFeedbackSimple.propTypes = { recordScoreCallback: PropTypes.func.isRequired, label: PropTypes.string.isRequired, + showFeedback: PropTypes.bool, }; export { CustomerFeedbackSimple }; diff --git a/packages/js/customer-effort-score/src/index.js b/packages/js/customer-effort-score/src/index.js index a89db2c591c..8699d826dac 100644 --- a/packages/js/customer-effort-score/src/index.js +++ b/packages/js/customer-effort-score/src/index.js @@ -1,2 +1,3 @@ export * from './customer-effort-score'; export * from './customer-feedback-simple'; +export * from './customer-feedback-modal'; diff --git a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js index 3d37a61d4fa..b55b39548e3 100644 --- a/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js +++ b/plugins/woocommerce-admin/client/customer-effort-score-tracks/customer-effort-score-tracks.js @@ -4,7 +4,7 @@ import { useState } from '@wordpress/element'; import PropTypes from 'prop-types'; 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 { withSelect, withDispatch } from '@wordpress/data'; import { OPTIONS_STORE_NAME, WEEK } from '@woocommerce/data'; diff --git a/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.scss b/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.scss index 428843d1fb1..e784d47201e 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.scss +++ b/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.scss @@ -1,4 +1,4 @@ -.wooocommerce-task-card__header-subtitle { +.wooocommerce-task-card__header .wooocommerce-task-card__header-subtitle { color: $gray-700; margin-bottom: $gap-large; } diff --git a/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.tsx b/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.tsx index 00c007d33b4..a322354f10a 100644 --- a/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.tsx +++ b/plugins/woocommerce-admin/client/two-column-tasks/completed-header-with-ces.tsx @@ -2,9 +2,17 @@ * External dependencies */ import classnames from 'classnames'; +import { useState } from '@wordpress/element'; +import { EllipsisMenu } from '@woocommerce/components'; +import { recordEvent } from '@woocommerce/tracks'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { OPTIONS_STORE_NAME, WEEK } from '@woocommerce/data'; import { Button, Card, CardHeader } from '@wordpress/components'; import { Text } from '@woocommerce/experimental'; -import { CustomerFeedbackSimple } from '@woocommerce/customer-effort-score'; +import { + CustomerFeedbackModal, + CustomerFeedbackSimple, +} from '@woocommerce/customer-effort-score'; import { __ } from '@wordpress/i18n'; /** @@ -12,21 +20,87 @@ import { __ } from '@wordpress/i18n'; */ import './completed-header-with-ces.scss'; import HeaderImage from './completed-celebration-header.svg'; -import React from 'react'; -import { EllipsisMenu } from '@woocommerce/components'; type TaskListCompletedHeaderProps = { - hideTasks: ( event: string ) => void; + hideTasks: () => void; keepTasks: () => void; allowTracking: boolean; }; +const ADMIN_INSTALL_TIMESTAMP_OPTION_NAME = + 'woocommerce_admin_install_timestamp'; +const SHOWN_FOR_ACTIONS_OPTION_NAME = 'woocommerce_ces_shown_for_actions'; +const CES_ACTION = 'store_setup'; + +function getStoreAgeInWeeks( adminInstallTimestamp: number ) { + if ( adminInstallTimestamp === 0 ) { + return null; + } + + // Date.now() is ms since Unix epoch, adminInstallTimestamp is in + // seconds since Unix epoch. + const storeAgeInMs = Date.now() - adminInstallTimestamp * 1000; + const storeAgeInWeeks = Math.round( storeAgeInMs / WEEK ); + + return storeAgeInWeeks; +} + export const TaskListCompletedHeaderWithCES: React.FC< TaskListCompletedHeaderProps > = ( { hideTasks, keepTasks, allowTracking, } ) => { - const recordScoreCallback = ( score: number ) => {}; + const { updateOptions } = useDispatch( OPTIONS_STORE_NAME ); + const [ showCesModal, setShowCesModal ] = useState( false ); + const [ score, setScore ] = useState( NaN ); + const [ showFeedback, setShowFeedback ] = useState( false ); + const { storeAgeInWeeks, cesShownForActions } = useSelect( ( select ) => { + const { getOption } = select( OPTIONS_STORE_NAME ); + + const adminInstallTimestamp: number = + getOption( ADMIN_INSTALL_TIMESTAMP_OPTION_NAME ) || 0; + return { + storeAgeInWeeks: getStoreAgeInWeeks( adminInstallTimestamp ), + cesShownForActions: + getOption( SHOWN_FOR_ACTIONS_OPTION_NAME ) || [], + }; + } ); + + const recordScore = ( score: number ) => { + if ( score > 2 ) { + recordEvent( 'ces_feedback', { + action: CES_ACTION, + score, + store_age: storeAgeInWeeks, + } ); + updateOptions( { + [ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [ + CES_ACTION, + ...cesShownForActions, + ], + } ); + } else { + setScore( score ); + setShowCesModal( true ); + } + }; + + const recordModalScore = ( score: number, comments: string ) => { + setShowCesModal( false ); + recordEvent( 'ces_feedback', { + action: 'store_setup', + score, + comments: comments || '', + store_age: storeAgeInWeeks, + } ); + updateOptions( { + [ SHOWN_FOR_ACTIONS_OPTION_NAME ]: [ + CES_ACTION, + ...cesShownForActions, + ], + } ); + setShowFeedback( true ); + }; return ( <> @@ -97,11 +171,19 @@ export const TaskListCompletedHeaderWithCES: React.FC< TaskListCompletedHeaderPr 'How was your experience?', 'woocommerce' ) } - recordScoreCallback={ recordScoreCallback } + recordScoreCallback={ recordScore } + showFeedback={ showFeedback } /> ) }
+ { showCesModal ? ( + + ) : null } ); };