-
Notifications
You must be signed in to change notification settings - Fork 22
Add CES prompts for initial setup and campaign creation #1152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
36ce432
de2d18e
05c5459
e8ef37c
0ff1b0f
83ed21c
346880a
70e8d5a
bd41bcf
9b6a811
5e0fefb
064000c
388c8ec
74cc674
fd51468
2932545
006b2ac
19ff496
3c9b2f6
07d85e4
739e897
ab165c7
bc62333
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { __ } from '@wordpress/i18n'; | ||
import CustomerEffortScore from '@woocommerce/customer-effort-score'; | ||
import { recordEvent } from '@woocommerce/tracks'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { LOCAL_STORAGE_KEYS } from '.~/constants'; | ||
import localStorage from '.~/utils/localStorage'; | ||
|
||
/** | ||
* CES prompt snackbar open | ||
* | ||
* @event gla_ces_snackbar_open | ||
*/ | ||
/** | ||
* CES prompt snackbar closed | ||
* | ||
* @event gla_ces_snackbar_closed | ||
*/ | ||
/** | ||
* CES modal open | ||
* | ||
* @event gla_ces_modal_open | ||
*/ | ||
/** | ||
* CES feedback recorded | ||
* | ||
* @event gla_ces_feedback | ||
*/ | ||
|
||
/** | ||
* A CustomerEffortScore wrapper that uses tracks to track the selected | ||
* customer effort score. | ||
* | ||
* @fires gla_ces_snackbar_open whenever the CES snackbar (notice) is open | ||
* @fires gla_ces_snackbar_closed whenever the CES snackbar (notice) is closed | ||
* @fires gla_ces_modal_open whenever the CES modal is open | ||
* @fires gla_ces_feedback whenever the CES feedback is recorded | ||
* | ||
* @param {Object} props React component props. | ||
* @param {string} props.eventContext Context to be used in the CES wrapper events. | ||
* @param {string} props.label Text to be displayed in the CES notice and modal. | ||
* @return {JSX.Element} Rendered element. | ||
*/ | ||
const CustomerEffortScorePrompt = ( { eventContext, label } ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💅 Non blocking I guess we can add here
And then remove them in |
||
const removeCESPromptFlagFromLocal = () => { | ||
localStorage.remove( | ||
LOCAL_STORAGE_KEYS.CAN_ONBOARDING_SETUP_CES_PROMPT_OPEN | ||
); | ||
}; | ||
|
||
const onNoticeShown = () => { | ||
removeCESPromptFlagFromLocal(); | ||
recordEvent( 'gla_ces_snackbar_open', { | ||
context: eventContext, | ||
} ); | ||
}; | ||
|
||
const onNoticeDismissed = () => { | ||
recordEvent( 'gla_ces_snackbar_closed', { | ||
context: eventContext, | ||
} ); | ||
}; | ||
|
||
const onModalShown = () => { | ||
recordEvent( 'gla_ces_modal_open', { | ||
context: eventContext, | ||
} ); | ||
}; | ||
|
||
const recordScore = ( score, comments ) => { | ||
recordEvent( 'gla_ces_feedback', { | ||
context: eventContext, | ||
score, | ||
comments: comments || '', | ||
} ); | ||
}; | ||
|
||
return ( | ||
<CustomerEffortScore | ||
label={ label } | ||
recordScoreCallback={ recordScore } | ||
onNoticeShownCallback={ onNoticeShown } | ||
onNoticeDismissedCallback={ onNoticeDismissed } | ||
onModalShownCallback={ onModalShown } | ||
icon={ | ||
<span | ||
style={ { height: 21, width: 21 } } | ||
role="img" | ||
aria-label={ __( | ||
'Pencil icon', | ||
'google-listings-and-ads' | ||
) } | ||
> | ||
✏️ | ||
</span> | ||
} | ||
/> | ||
); | ||
}; | ||
|
||
export default CustomerEffortScorePrompt; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,12 @@ | |
* External dependencies | ||
*/ | ||
import { getHistory, getNewPath, getQuery } from '@woocommerce/navigation'; | ||
import { createInterpolateElement, useEffect } from '@wordpress/element'; | ||
import { | ||
createInterpolateElement, | ||
useEffect, | ||
useCallback, | ||
useState, | ||
} from '@wordpress/element'; | ||
import { __ } from '@wordpress/i18n'; | ||
import { Button } from '@wordpress/components'; | ||
import { recordEvent } from '@woocommerce/tracks'; | ||
|
@@ -11,14 +16,16 @@ import { recordEvent } from '@woocommerce/tracks'; | |
* Internal dependencies | ||
*/ | ||
import { getCreateCampaignUrl } from '.~/utils/urls'; | ||
import isWCTracksEnabled from '.~/utils/isWCTracksEnabled'; | ||
import AppModal from '.~/components/app-modal'; | ||
import GuidePageContent, { | ||
ContentLink, | ||
} from '.~/components/guide-page-content'; | ||
import CustomerEffortScorePrompt from '.~/components/customer-effort-score-prompt'; | ||
import { GUIDE_NAMES } from '.~/constants'; | ||
import headerImageURL from './header.svg'; | ||
import './index.scss'; | ||
|
||
const GUIDE_NAME = 'campaign-creation-success'; | ||
const CTA_CREATE_ANOTHER_CAMPAIGN = 'create-another-campaign'; | ||
|
||
const handleCloseWithAction = ( e, specifiedAction ) => { | ||
|
@@ -34,17 +41,33 @@ const handleCloseWithAction = ( e, specifiedAction ) => { | |
} | ||
|
||
recordEvent( 'gla_modal_closed', { | ||
context: GUIDE_NAME, | ||
context: GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS, | ||
action, | ||
} ); | ||
}; | ||
|
||
const handleRequestClose = ( e ) => handleCloseWithAction( e, 'dismiss' ); | ||
|
||
const GuideImplementation = () => { | ||
const GuideImplementation = ( { isOpen, setCESPromptOpen } ) => { | ||
const handleGotItClick = useCallback( | ||
( e ) => { | ||
handleCloseWithAction( e ); | ||
setCESPromptOpen( true ); | ||
}, | ||
[ setCESPromptOpen ] | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ / 💅 What i the rationale behind using
I guess is something related to the UseCallback? Not sure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think originally my thought was: the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ohhh, understood, didn't show that one. Thanks for the explanation.
Would be interesting to know why it is outside, wondering if there is an important reason for it, maybe @eason9487 knows. I would go ahead with your current solution and the maybe refactor after knowing that. It's 100% a non blocking issue. Since I don't want to block the PR I rely on you for the final decision regarding this. Again, looks good now as it's. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Those click handlers were not dependent on props and states, so to avoid their references changing all the time, I put them outside then. It's ok to move them inside the component. 💅 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eason9487 Great suggestion. Added in |
||
|
||
useEffect( () => { | ||
recordEvent( 'gla_modal_open', { context: GUIDE_NAME } ); | ||
}, [] ); | ||
if ( isOpen ) { | ||
recordEvent( 'gla_modal_open', { | ||
context: GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS, | ||
} ); | ||
} | ||
}, [ isOpen ] ); | ||
|
||
if ( ! isOpen ) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<AppModal | ||
|
@@ -66,7 +89,7 @@ const GuideImplementation = () => { | |
key="1" | ||
isPrimary | ||
data-action="confirm" | ||
onClick={ handleCloseWithAction } | ||
onClick={ handleGotItClick } | ||
> | ||
{ __( 'Got it', 'google-listings-and-ads' ) } | ||
</Button>, | ||
|
@@ -115,10 +138,26 @@ const GuideImplementation = () => { | |
* For example: `/wp-admin/admin.php?page=wc-admin&path=%2Fgoogle%2Fdashboard&guide=campaign-creation-success`. | ||
*/ | ||
export default function CampaignCreationSuccessGuide() { | ||
const isOpen = getQuery().guide === GUIDE_NAME; | ||
const [ isCESPromptOpen, setCESPromptOpen ] = useState( false ); | ||
|
||
if ( ! isOpen ) { | ||
return null; | ||
} | ||
return <GuideImplementation />; | ||
const isOpen = getQuery()?.guide === GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS; | ||
const wcTracksEnabled = isWCTracksEnabled(); | ||
|
||
return ( | ||
<> | ||
<GuideImplementation | ||
isOpen={ isOpen } | ||
setCESPromptOpen={ setCESPromptOpen } | ||
/> | ||
{ isCESPromptOpen && wcTracksEnabled && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💅 See my previous comment regarding |
||
<CustomerEffortScorePrompt | ||
label={ __( | ||
'How easy was it to create a Google Ad campaign?', | ||
'google-listings-and-ads' | ||
) } | ||
eventContext={ GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS } | ||
/> | ||
) } | ||
</> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import '@testing-library/jest-dom'; | ||
import { render, screen } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
import { getQuery } from '@woocommerce/navigation'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import CampaignCreationSuccessGuide from './index'; | ||
import isWCTracksEnabled from '.~/utils/isWCTracksEnabled'; | ||
import { GUIDE_NAMES } from '.~/constants'; | ||
|
||
jest.mock( '@woocommerce/navigation', () => { | ||
return { | ||
...jest.requireActual( '@woocommerce/navigation' ), | ||
getQuery: jest.fn(), | ||
}; | ||
} ); | ||
|
||
jest.mock( '.~/utils/isWCTracksEnabled', () => jest.fn() ); | ||
|
||
const CAMPAIGN_CREATION_SUCCESS_GUIDE_TEXT = | ||
"You've set up a paid Smart Shopping Campaign!"; | ||
const CES_PROMPT_TEXT = 'How easy was it to create a Google Ad campaign?'; | ||
|
||
jest.mock( '.~/components/customer-effort-score-prompt', () => () => ( | ||
<div>{ CES_PROMPT_TEXT }</div> | ||
) ); | ||
|
||
describe( 'CampaignCreationSuccessGuide', () => { | ||
describe( `When the query string "guide" equals to ${ GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS }`, () => { | ||
beforeAll( () => { | ||
getQuery.mockImplementation( () => { | ||
return { | ||
guide: GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS, | ||
}; | ||
} ); | ||
} ); | ||
|
||
afterAll( () => { | ||
getQuery.mockReset(); | ||
} ); | ||
|
||
test( 'Should render CampaignCreationSuccessGuide', () => { | ||
const { queryByText } = render( <CampaignCreationSuccessGuide /> ); | ||
expect( | ||
queryByText( CAMPAIGN_CREATION_SUCCESS_GUIDE_TEXT ) | ||
).toBeInTheDocument(); | ||
} ); | ||
|
||
describe( 'And wcTracksEnabled is false', () => { | ||
beforeAll( () => { | ||
isWCTracksEnabled.mockImplementation( () => { | ||
return false; | ||
} ); | ||
} ); | ||
|
||
afterAll( () => { | ||
isWCTracksEnabled.mockReset(); | ||
} ); | ||
|
||
test( 'Should not render CustomerEffortScorePrompt when user does not click "Got it"', () => { | ||
const { queryByText } = render( | ||
<CampaignCreationSuccessGuide /> | ||
); | ||
expect( | ||
queryByText( CES_PROMPT_TEXT ) | ||
).not.toBeInTheDocument(); | ||
} ); | ||
|
||
test( 'Should not render CustomerEffortScorePrompt when user clicks "Got it"', () => { | ||
const { queryByText } = render( | ||
<CampaignCreationSuccessGuide /> | ||
); | ||
userEvent.click( screen.getByText( 'Got it' ) ); | ||
|
||
expect( | ||
queryByText( CES_PROMPT_TEXT ) | ||
).not.toBeInTheDocument(); | ||
} ); | ||
} ); | ||
|
||
describe( 'And wcTracksEnabled is true', () => { | ||
beforeAll( () => { | ||
isWCTracksEnabled.mockImplementation( () => { | ||
return true; | ||
} ); | ||
} ); | ||
|
||
afterAll( () => { | ||
isWCTracksEnabled.mockReset(); | ||
} ); | ||
|
||
test( 'Should not render CustomerEffortScorePrompt when user does not click "Got it"', () => { | ||
const { queryByText } = render( | ||
<CampaignCreationSuccessGuide /> | ||
); | ||
expect( | ||
queryByText( CES_PROMPT_TEXT ) | ||
).not.toBeInTheDocument(); | ||
} ); | ||
|
||
test( 'Should render CustomerEffortScorePrompt when user clicks "Got it"', () => { | ||
const { queryByText } = render( | ||
<CampaignCreationSuccessGuide /> | ||
); | ||
userEvent.click( screen.getByText( 'Got it' ) ); | ||
|
||
expect( queryByText( CES_PROMPT_TEXT ) ).toBeInTheDocument(); | ||
} ); | ||
} ); | ||
} ); | ||
|
||
describe( `When the query string "guide" does not equals to ${ GUIDE_NAMES.CAMPAIGN_CREATION_SUCCESS }`, () => { | ||
beforeAll( () => { | ||
getQuery.mockImplementation( () => { | ||
return {}; | ||
} ); | ||
} ); | ||
|
||
afterAll( () => { | ||
getQuery.mockReset(); | ||
} ); | ||
|
||
test( 'Should not render CampaignCreationSuccessGuide', () => { | ||
const { queryByText } = render( <CampaignCreationSuccessGuide /> ); | ||
expect( | ||
queryByText( CAMPAIGN_CREATION_SUCCESS_GUIDE_TEXT ) | ||
).not.toBeInTheDocument(); | ||
} ); | ||
} ); | ||
} ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💅