add: CYS task-completed intro flow (#40616)

* add: UI work for task completed flow

* added spinner for intro page loading

* add: save ai generated theme id to options

* resolve rebase conflict

* fixed tests
This commit is contained in:
RJ 2023-10-09 13:19:08 +08:00 committed by GitHub
parent 60b4502e40
commit b436d40be3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 532 additions and 104 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 MiB

View File

@ -1,3 +1,6 @@
// @ts-expect-error -- No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { store as coreStore } from '@wordpress/core-data';
/**
* External dependencies
*/
@ -10,7 +13,8 @@ import {
updateQueryString,
} from '@woocommerce/navigation';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
import { dispatch } from '@wordpress/data';
import { dispatch, resolveSelect } from '@wordpress/data';
import { Spinner } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/settings';
/**
@ -71,8 +75,15 @@ const redirectToThemes = ( _context: customizeStoreStateMachineContext ) => {
};
const markTaskComplete = async () => {
const currentTemplate = await resolveSelect(
coreStore
// @ts-expect-error No types for this exist yet.
).__experimentalGetTemplateForLink( '/' );
return dispatch( OPTIONS_STORE_NAME ).updateOptions( {
woocommerce_admin_customize_store_completed: 'yes',
// we use this on the intro page to determine if this same theme was used in the last customization
woocommerce_admin_customize_store_completed_theme_id:
currentTemplate.id ?? undefined,
} );
};
@ -129,6 +140,7 @@ export const customizeStoreStateMachineDefinition = createMachine( {
activeTheme: '',
activeThemeHasMods: false,
customizeStoreTaskCompleted: false,
currentThemeIsAiGenerated: false,
},
} as customizeStoreStateMachineContext,
invoke: {
@ -196,6 +208,7 @@ export const customizeStoreStateMachineDefinition = createMachine( {
'assignThemeData',
'assignActiveThemeHasMods',
'assignCustomizeStoreCompleted',
'assignCurrentThemeIsAiGenerated',
],
},
},
@ -365,7 +378,9 @@ export const CustomizeStoreController = ( {
currentState={ state.value }
/>
) : (
<div />
<div className="woocommerce-customize-store__loading">
<Spinner />
</div>
) }
</div>
</>

View File

@ -69,16 +69,15 @@ export const assignActiveThemeHasMods = assign<
export const assignCustomizeStoreCompleted = assign<
customizeStoreStateMachineContext,
customizeStoreStateMachineEvents // this is actually the wrong type for the event but I still don't know how to type this properly
customizeStoreStateMachineEvents
>( {
intro: ( context, event ) => {
const customizeStoreCompleted = (
const customizeStoreTaskCompleted = (
event as DoneInvokeEvent< {
assignCustomizeStoreCompleted: boolean;
customizeStoreTaskCompleted: boolean;
} >
).data.assignCustomizeStoreCompleted;
// type coercion workaround for now
return { ...context.intro, customizeStoreCompleted };
).data.customizeStoreTaskCompleted;
return { ...context.intro, customizeStoreTaskCompleted };
},
} );
@ -90,3 +89,17 @@ export const assignFetchIntroDataError = assign<
return { ...context.intro, hasErrors: true };
},
} );
export const assignCurrentThemeIsAiGenerated = assign<
customizeStoreStateMachineContext,
customizeStoreStateMachineEvents
>( {
intro: ( context, event ) => {
const currentThemeIsAiGenerated = (
event as DoneInvokeEvent< {
currentThemeIsAiGenerated: boolean;
} >
).data.currentThemeIsAiGenerated;
return { ...context.intro, currentThemeIsAiGenerated };
},
} );

View File

@ -1,79 +0,0 @@
/**
* External dependencies
*/
import { Button, Modal } from '@wordpress/components';
import { Sender } from 'xstate';
import { __ } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { customizeStoreStateMachineEvents } from '..';
import { ADMIN_URL } from '~/utils/admin-settings';
export const DesignChangeWarningModal = ( {
isOpen = false,
setOpenDesignChangeWarningModal,
sendEvent,
classname = 'woocommerce-customize-store__design-change-warning-modal',
}: {
isOpen?: boolean;
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
sendEvent: Sender< customizeStoreStateMachineEvents >;
classname?: string;
} ) => {
if ( ! isOpen ) {
return null;
}
return (
<Modal
className={ classname }
title={ __(
'Are you sure you want to start a new design?',
'woocommerce'
) }
onRequestClose={ () => setOpenDesignChangeWarningModal( false ) }
shouldCloseOnClickOutside={ false }
>
<p>
{ createInterpolateElement(
__(
"The [AI designer*] will create a new store design for you, and you'll lose any changes you've made to your active theme. If you'd prefer to continue editing your theme, you can do so via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
onClick={ () => {
window.open(
`${ ADMIN_URL }site-editor.php`,
'_blank'
);
return false;
} }
href=""
/>
),
}
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button
onClick={ () => setOpenDesignChangeWarningModal( false ) }
variant="link"
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }
variant="primary"
>
{ __( 'Design with AI', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
};

View File

@ -16,7 +16,11 @@ import {
import { CustomizeStoreComponent } from '../types';
import { SiteHub } from '../assembler-hub/site-hub';
import { ThemeCard } from './theme-card';
import { DesignChangeWarningModal } from './design-change-warning-modal';
import {
DesignChangeWarningModal,
StartNewDesignWarningModal,
StartOverWarningModal,
} from './warning-modals';
import { useNetworkStatus } from '~/utils/react-hooks/use-network-status';
import './intro.scss';
import {
@ -24,6 +28,8 @@ import {
ThemeHasModsBanner,
JetpackOfflineBanner,
DefaultBanner,
ExistingAiThemeBanner,
ExistingThemeBanner,
} from './intro-banners';
export type events =
@ -43,22 +49,28 @@ const BANNER_COMPONENTS = {
'network-offline': NetworkOfflineBanner,
'task-incomplete-active-theme-has-mods': ThemeHasModsBanner,
'jetpack-offline': JetpackOfflineBanner,
'existing-ai-theme': DefaultBanner,
'existing-ai-theme': ExistingAiThemeBanner,
'existing-theme': ExistingThemeBanner,
default: DefaultBanner,
};
const MODAL_COMPONENTS = {
'no-modal': null,
'task-incomplete-override-design-changes': DesignChangeWarningModal,
'task-complete-with-ai-theme': null,
'task-complete-without-ai-theme': null,
'task-complete-with-ai-theme': StartOverWarningModal,
'task-complete-without-ai-theme': StartNewDesignWarningModal,
};
type ModalStatus = keyof typeof MODAL_COMPONENTS;
export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
const {
intro: { themeData, activeThemeHasMods, customizeStoreTaskCompleted },
intro: {
themeData,
activeThemeHasMods,
customizeStoreTaskCompleted,
currentThemeIsAiGenerated,
},
} = context;
const isJetpackOffline = false;
@ -78,12 +90,15 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
case isJetpackOffline as boolean:
bannerStatus = 'jetpack-offline';
break;
case activeThemeHasMods && ! customizeStoreTaskCompleted:
case ! customizeStoreTaskCompleted && activeThemeHasMods:
bannerStatus = 'task-incomplete-active-theme-has-mods';
break;
case context.intro.currentThemeIsAiGenerated:
case customizeStoreTaskCompleted && currentThemeIsAiGenerated:
bannerStatus = 'existing-ai-theme';
break;
case customizeStoreTaskCompleted && ! currentThemeIsAiGenerated:
bannerStatus = 'existing-theme';
break;
}
switch ( true ) {
@ -93,6 +108,12 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
case bannerStatus === 'task-incomplete-active-theme-has-mods':
modalStatus = 'task-incomplete-override-design-changes';
break;
case bannerStatus === 'existing-ai-theme':
modalStatus = 'task-complete-with-ai-theme';
break;
case bannerStatus === 'existing-theme':
modalStatus = 'task-complete-without-ai-theme';
break;
}
const ModalComponent = MODAL_COMPONENTS[ modalStatus ];
@ -103,7 +124,6 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
<>
{ ModalComponent && (
<ModalComponent
isOpen={ openDesignChangeWarningModal }
sendEvent={ sendEvent }
setOpenDesignChangeWarningModal={
setOpenDesignChangeWarningModal

View File

@ -4,6 +4,7 @@
import { __ } from '@wordpress/i18n';
import classNames from 'classnames';
import { Button } from '@wordpress/components';
import { getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -17,6 +18,7 @@ export const BaseIntroBanner = ( {
buttonIsLink,
bannerButtonOnClick,
bannerButtonText,
children,
}: {
bannerTitle: string;
bannerText: string;
@ -24,6 +26,7 @@ export const BaseIntroBanner = ( {
buttonIsLink: boolean;
bannerButtonOnClick: () => void;
bannerButtonText: string;
children?: React.ReactNode;
} ) => {
return (
<div
@ -41,6 +44,7 @@ export const BaseIntroBanner = ( {
>
{ bannerButtonText }
</Button>
{ children }
</div>
</div>
);
@ -92,6 +96,31 @@ export const JetpackOfflineBanner = ( {
);
};
export const ExistingThemeBanner = ( {
setOpenDesignChangeWarningModal,
}: {
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
} ) => {
return (
<BaseIntroBanner
bannerTitle={ __(
'Use the power of AI to design your store',
'woocommerce'
) }
bannerText={ __(
'Design the look of your store, create pages, and generate copy using our built-in AI tools.',
'woocommerce'
) }
bannerClass=""
buttonIsLink={ false }
bannerButtonOnClick={ () => {
setOpenDesignChangeWarningModal( true );
} }
bannerButtonText={ __( 'Design with AI', 'woocommerce' ) }
/>
);
};
export const DefaultBanner = ( {
sendEvent,
}: {
@ -143,3 +172,39 @@ export const ThemeHasModsBanner = ( {
/>
);
};
export const ExistingAiThemeBanner = ( {
setOpenDesignChangeWarningModal,
}: {
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
} ) => {
return (
<BaseIntroBanner
bannerTitle={ __( 'Customize your custom theme', 'woocommerce' ) }
bannerText={ __(
'Keep customizing the look of your AI-generated store, or start over and create a new one.',
'woocommerce'
) }
bannerClass="existing-ai-theme-banner"
buttonIsLink={ false }
bannerButtonOnClick={ () => {
window.location.href = getNewPath(
{},
'/customize-store/assembler-hub',
{}
);
} }
bannerButtonText={ __( 'Customize', 'woocommerce' ) }
>
<Button
className=""
onClick={ () => {
setOpenDesignChangeWarningModal( true );
} }
variant={ 'secondary' }
>
{ __( 'Create a new one', 'woocommerce' ) }
</Button>
</BaseIntroBanner>
);
};

View File

@ -113,6 +113,13 @@
background-position-y: 29px;
}
&.existing-ai-theme-banner {
background: rgba(246, 247, 247, 1) url(../assets/images/intro-banner-existing-ai.svg) no-repeat center right;
background-size: auto 218px;
background-position-y: 29px;
background-position-x: 91%;
}
.woocommerce-customize-store-banner-content {
width: 375px;
margin-left: 50px;
@ -123,6 +130,10 @@
font-weight: 500;
}
button.components-button + button.components-button { // add left margin for all buttons with another button to its left
margin-left: 12px;
}
h1 {
font-size: 1.25rem;
line-height: 23.87px;
@ -236,7 +247,6 @@
text-decoration: none !important;
}
h1 {
width: 320px;
line-height: 28px;
font-size: 20px;
color: #1e1e1e;

View File

@ -1,15 +1,13 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-expect-error -- No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { store as coreStore } from '@wordpress/core-data';
/**
* External dependencies
*/
import { resolveSelect } from '@wordpress/data';
import { ONBOARDING_STORE_NAME } from '@woocommerce/data';
// @ts-ignore No types for this exist yet.
import { store as coreStore } from '@wordpress/core-data';
import { ONBOARDING_STORE_NAME, OPTIONS_STORE_NAME } from '@woocommerce/data';
import apiFetch from '@wordpress/api-fetch';
// placeholder xstate async service that returns a set of theme cards
export const fetchThemeCards = async () => {
const themes = await apiFetch( {
path: '/wc-admin/onboarding/themes/recommended',
@ -20,10 +18,21 @@ export const fetchThemeCards = async () => {
};
export const fetchIntroData = async () => {
let currentThemeIsAiGenerated = false;
const currentTemplate = await resolveSelect(
coreStore
// @ts-expect-error No types for this exist yet.
).__experimentalGetTemplateForLink( '/' );
const maybePreviousTemplate = await resolveSelect(
OPTIONS_STORE_NAME
).getOption( 'woocommerce_admin_customize_store_completed_theme_id' );
if (
maybePreviousTemplate &&
currentTemplate?.id === maybePreviousTemplate
) {
currentThemeIsAiGenerated = true;
}
const styleRevs = await resolveSelect(
coreStore
@ -57,5 +66,6 @@ export const fetchIntroData = async () => {
activeThemeHasMods,
customizeStoreTaskCompleted,
themeData,
currentThemeIsAiGenerated,
};
};

View File

@ -93,4 +93,37 @@ describe( 'Intro Banners', () => {
type: 'DESIGN_WITH_AI',
} );
} );
it( 'should display the existing ai theme banner when customizeStoreTaskCompleted and currentThemeIsAiGenerated', () => {
const sendEventMock = jest.fn();
( useNetworkStatus as jest.Mock ).mockImplementation( () => false );
render(
<Intro
sendEvent={ sendEventMock }
context={ {
intro: {
hasErrors: false,
activeTheme: '',
themeData: {
themes: [],
_links: {
browse_all: {
href: '',
},
},
},
activeThemeHasMods: false,
customizeStoreTaskCompleted: true,
currentThemeIsAiGenerated: true,
},
themeConfiguration: {},
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
/>
);
expect(
screen.getByText( /Customize your custom theme/i )
).toBeInTheDocument();
} );
} );

View File

@ -67,4 +67,104 @@ describe( 'Intro Modals', () => {
type: 'DESIGN_WITH_AI',
} );
} );
it( 'should display StartOverWarningModal when customizeStoreTaskCompleted and currentThemeIsAiGenerated and button is clicked', async () => {
const sendEventMock = jest.fn();
render(
<Intro
sendEvent={ sendEventMock }
context={ {
intro: {
hasErrors: false,
activeTheme: '',
themeData: {
themes: [],
_links: {
browse_all: {
href: '',
},
},
},
activeThemeHasMods: false,
customizeStoreTaskCompleted: true,
currentThemeIsAiGenerated: true,
},
themeConfiguration: {},
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
/>
);
const bannerButton = screen.getByRole( 'button', {
name: /Create a new one/i,
} );
fireEvent.click( bannerButton );
await waitFor( () => {
expect(
screen.getByText( /Are you sure you want to start over?/i )
).toBeInTheDocument();
} );
const modalButton = screen.getByRole( 'button', {
name: /Start again/i,
} );
fireEvent.click( modalButton );
expect( sendEventMock ).toHaveBeenCalledWith( {
type: 'DESIGN_WITH_AI',
} );
} );
it( 'should display StartNewDesignWarningModal when customizeStoreTaskCompleted and not currentThemeIsAiGenerated and button is clicked', async () => {
const sendEventMock = jest.fn();
render(
<Intro
sendEvent={ sendEventMock }
context={ {
intro: {
hasErrors: false,
activeTheme: '',
themeData: {
themes: [],
_links: {
browse_all: {
href: '',
},
},
},
activeThemeHasMods: false,
customizeStoreTaskCompleted: true,
currentThemeIsAiGenerated: false,
},
themeConfiguration: {},
} }
currentState={ 'intro' }
parentMachine={ null as unknown as AnyInterpreter }
/>
);
const bannerButton = screen.getByRole( 'button', {
name: /Design with AI/i,
} );
fireEvent.click( bannerButton );
await waitFor( () => {
expect(
screen.getByText(
/Are you sure you want to start a new design?/i
)
).toBeInTheDocument();
} );
const modalButton = screen.getByRole( 'button', {
name: /Design with AI/i,
} );
fireEvent.click( modalButton );
expect( sendEventMock ).toHaveBeenCalledWith( {
type: 'DESIGN_WITH_AI',
} );
} );
} );

View File

@ -0,0 +1,191 @@
/**
* External dependencies
*/
import { Button, Modal } from '@wordpress/components';
import { Sender } from 'xstate';
import { __ } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { customizeStoreStateMachineEvents } from '..';
import { ADMIN_URL } from '~/utils/admin-settings';
export const DesignChangeWarningModal = ( {
setOpenDesignChangeWarningModal,
sendEvent,
classname = 'woocommerce-customize-store__design-change-warning-modal',
}: {
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
sendEvent: Sender< customizeStoreStateMachineEvents >;
classname?: string;
} ) => {
return (
<Modal
className={ classname }
title={ __(
'Are you sure you want to start a new design?',
'woocommerce'
) }
onRequestClose={ () => setOpenDesignChangeWarningModal( false ) }
shouldCloseOnClickOutside={ false }
>
<p>
{ createInterpolateElement(
__(
"The [AI designer*] will create a new store design for you, and you'll lose any changes you've made to your active theme. If you'd prefer to continue editing your theme, you can do so via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
onClick={ () => {
window.open(
`${ ADMIN_URL }site-editor.php`,
'_blank'
);
return false;
} }
href=""
/>
),
}
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button
onClick={ () => setOpenDesignChangeWarningModal( false ) }
variant="link"
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }
variant="primary"
>
{ __( 'Design with AI', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
};
export const StartNewDesignWarningModal = ( {
setOpenDesignChangeWarningModal,
sendEvent,
classname = 'woocommerce-customize-store__design-change-warning-modal',
}: {
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
sendEvent: Sender< customizeStoreStateMachineEvents >;
classname?: string;
} ) => {
return (
<Modal
className={ classname }
title={ __(
'Are you sure you want to start a new design?',
'woocommerce'
) }
onRequestClose={ () => setOpenDesignChangeWarningModal( false ) }
shouldCloseOnClickOutside={ false }
>
<p>
{ createInterpolateElement(
__(
"The [AI designer*] will create a new store design for you, and you'll lose any changes you've made to your active theme. If you'd prefer to continue editing your theme, you can do so via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
onClick={ () => {
window.open(
`${ ADMIN_URL }site-editor.php`,
'_blank'
);
return false;
} }
href=""
/>
),
}
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button
onClick={ () => setOpenDesignChangeWarningModal( false ) }
variant="link"
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }
variant="primary"
>
{ __( 'Design with AI', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
};
export const StartOverWarningModal = ( {
setOpenDesignChangeWarningModal,
sendEvent,
classname = 'woocommerce-customize-store__design-change-warning-modal',
}: {
setOpenDesignChangeWarningModal: ( arg0: boolean ) => void;
sendEvent: Sender< customizeStoreStateMachineEvents >;
classname?: string;
} ) => {
return (
<Modal
className={ classname }
title={ __(
'Are you sure you want to start over?',
'woocommerce'
) }
onRequestClose={ () => setOpenDesignChangeWarningModal( false ) }
shouldCloseOnClickOutside={ false }
>
<p>
{ createInterpolateElement(
__(
"You'll be asked to provide your business info again, and will lose your existing AI design. If you want to customize your existing design, you can do so via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
onClick={ () => {
window.open(
`${ ADMIN_URL }site-editor.php`,
'_blank'
);
return false;
} }
href=""
/>
),
}
) }
</p>
<div className="woocommerce-customize-store__design-change-warning-modal-footer">
<Button
onClick={ () => setOpenDesignChangeWarningModal( false ) }
variant="link"
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
<Button
onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }
variant="primary"
>
{ __( 'Start again', 'woocommerce' ) }
</Button>
</div>
</Modal>
);
};

View File

@ -1,7 +1,13 @@
.woocommerce-layout .woocommerce-layout__main {
@include breakpoint( '<782px' ) {
@include breakpoint('<782px') {
padding-top: 0 !important;
}
.woocommerce-customize-store__loading {
display: grid;
place-items: center;
height: 100vh;
}
}
.woocommerce-customize-store {
@ -71,7 +77,6 @@ body.woocommerce-customize-store.js.is-fullscreen-mode {
}
.woocommerce-cys-design-with-ai {
.components-base-control {
width: 404px;
textarea {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Save ai generated theme ID to options and use it to determine if the intro page should warn about existing AI theme