Cleanup product task experiment (#35950)

* Cleanup product task experiment

* Rename experimental-products -> products

* Add changelog

* Clean up product task experiment in woo/onboarding

Remove product variant

* Rename experimental-import-products -> import-products

* Clean up OnboardingTasks.php

* Add changelog

* Remove experimental-products-task feature flag
This commit is contained in:
Chi-Hsuan Huang 2022-12-28 16:08:54 +08:00 committed by GitHub
parent b133ad98c7
commit 9f0d718a33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 79 additions and 835 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Cleanup product task experiment

View File

@ -34,7 +34,6 @@ export const trackView = async ( taskId: string, variant?: string ) => {
} );
};
let experimentalVariant: string | undefined;
type WooOnboardingTaskProps = {
id: string;
variant?: string;
@ -56,37 +55,15 @@ type WooOnboardingTaskSlotProps = Slot.Props & {
*/
const WooOnboardingTask: React.FC< WooOnboardingTaskProps > & {
Slot: React.VFC< WooOnboardingTaskSlotProps >;
} = ( { id, variant, ...props } ) => {
useEffect( () => {
if ( id === 'products' ) {
experimentalVariant = variant;
}
}, [ id, variant ] );
} = ( { id, ...props } ) => {
return <Fill name={ 'woocommerce_onboarding_task_' + id } { ...props } />;
};
// We need this here just in case the experiment assignment takes awhile to load, so that we don't fire trackView with a blank experimentalVariant
// Remove all of the code in this file related to experiments and variants when the product task experiment concludes and never speak of the existence of this code to anyone
const pollForExperimentalVariant = ( id: string, count: number ) => {
if ( count > 20 ) {
trackView( id, 'experiment_timed_out' ); // if we can't fetch experiment after 4 seconds, give up
} else if ( experimentalVariant ) {
trackView( id, experimentalVariant );
} else {
setTimeout( () => pollForExperimentalVariant( id, count + 1 ), 200 );
}
};
WooOnboardingTask.Slot = ( { id, fillProps } ) => {
// The Slot is a React component and this hook works as expected.
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect( () => {
if ( id === 'products' ) {
pollForExperimentalVariant( id, 0 );
} else {
trackView( id );
}
trackView( id );
}, [ id ] );
return (

View File

@ -1,40 +0,0 @@
.woocommerce-products-card-layout {
margin-top: 8px;
display: flex;
flex-direction: column;
align-items: center;
.woocommerce-products-card-layout__description {
text-align: center;
}
.woocommerce-products-card-list {
margin-top: 32px;
.woocommerce-list {
grid-template-columns: 217px 217px 217px;
}
.woocommerce-list__item {
width: 217px;
height: 200px;
align-items: stretch;
}
.woocommerce-list__item-after {
opacity: 0;
}
.woocommerce-list__item-inner {
padding: 40px 24px;
}
}
.woocommerce-products-list__item-load-sample-product {
border: 1.5px dashed #dcdcde;
.woocommerce-list__item-before {
background-color: $gray-100;
}
}
}

View File

@ -1,63 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Text } from '@woocommerce/experimental';
import { Link } from '@woocommerce/components';
import interpolateComponents from '@automattic/interpolate-components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import { ProductType } from './constants';
import CardList from '../experimental-import-products/CardList';
import './card-layout.scss';
import useRecordCompletionTime from '../use-record-completion-time';
type CardProps = {
items: ( ProductType & {
onClick: () => void;
} )[];
};
const CardLayout: React.FC< CardProps > = ( { items } ) => {
const { recordCompletionTime } = useRecordCompletionTime( 'products' );
return (
<div className="woocommerce-products-card-layout">
<Text className="woocommerce-products-card-layout__description">
{ interpolateComponents( {
mixedString: __(
'{{sbLink}}Start blank{{/sbLink}} or select a product type:',
'woocommerce'
),
components: {
sbLink: (
<Link
onClick={ () => {
recordEvent( 'tasklist_add_product', {
method: 'manually',
} );
recordCompletionTime();
window.location.href = getAdminLink(
'post-new.php?post_type=product&wc_onboarding_active_task=products&tutorial=true'
);
return false;
} }
href=""
type="wc-admin"
>
<></>
</Link>
),
},
} ) }
</Text>
<CardList items={ items } />
</div>
);
};
export default CardLayout;

View File

@ -1,58 +0,0 @@
/**
* External dependencies
*/
import { fireEvent, render } from '@testing-library/react';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import CardLayout from '../card-layout';
import { productTypes } from '../constants';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );
describe( 'CardLayout', () => {
beforeEach( () => {
( recordEvent as jest.Mock ).mockClear();
} );
it( 'should render all products types in CardLayout', () => {
const { queryByText, queryAllByRole } = render(
<CardLayout
items={ [
{
...productTypes[ 0 ],
onClick: () => {},
},
] }
/>
);
expect( queryByText( productTypes[ 0 ].title ) ).toBeInTheDocument();
expect( queryAllByRole( 'link' ) ).toHaveLength( 1 );
} );
it( 'start blank link should fire the tasklist_add_product and completion events', () => {
const { getByText } = render(
<CardLayout
items={ [
{
...productTypes[ 0 ],
onClick: () => {},
},
] }
/>
);
fireEvent.click( getByText( 'Start blank' ) );
expect( recordEvent ).toHaveBeenNthCalledWith(
1,
'tasklist_add_product',
{ method: 'manually' }
);
expect( recordEvent ).toHaveBeenNthCalledWith(
2,
'task_completion_time',
{ task_name: 'products', time: '0-2s' }
);
} );
} );

View File

@ -1,33 +0,0 @@
/**
* External dependencies
*/
import { useState, useEffect } from '@wordpress/element';
type Layout = 'control' | 'card' | 'stacked';
export const getProductLayoutExperiment = async (): Promise< Layout > => {
// Deploy the stacked layout. Todo: cleanup the experiment code.
return 'stacked';
};
export const isProductTaskExperimentTreatment =
async (): Promise< boolean > => {
return ( await getProductLayoutExperiment() ) !== 'control';
};
export const useProductTaskExperiment = () => {
const [ isLoading, setIsLoading ] = useState< boolean >( true );
const [ experimentLayout, setExperimentLayout ] =
useState< Layout >( 'control' );
useEffect( () => {
getProductLayoutExperiment().then( ( layout ) => {
setExperimentLayout( layout );
setIsLoading( false );
} );
}, [ setExperimentLayout ] );
return { isLoading, experimentLayout };
};
export default useProductTaskExperiment;

View File

@ -13,12 +13,12 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import Stacks from '../experimental-products/stack';
import Stacks from '../products/stack';
import CardList from './CardList';
import { importTypes } from './importTypes';
import './style.scss';
import useProductTypeListItems from '../experimental-products/use-product-types-list-items';
import { getProductTypes } from '../experimental-products/utils';
import useProductTypeListItems from '../products/use-product-types-list-items';
import { getProductTypes } from '../products/utils';
import LoadSampleProductModal from '../components/load-sample-product-modal';
import useLoadSampleProducts from '../components/use-load-sample-products';
import LoadSampleProductConfirmModal from '../components/load-sample-product-confirm-modal';
@ -114,7 +114,7 @@ registerPlugin( 'wc-admin-onboarding-task-products', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-tasks',
render: () => (
<WooOnboardingTask id="products" variant="import">
<WooOnboardingTask id="products">
<Products />
</WooOnboardingTask>
),

View File

@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { Products } from '../';
import { Products } from '..';
jest.mock( '@woocommerce/tracks', () => ( { recordEvent: jest.fn() } ) );

View File

@ -1,8 +1,7 @@
/**
* Internal dependencies
*/
import { isProductTaskExperimentTreatment } from './experimental-products/use-product-layout-experiment';
import { isImportProductExperiment } from './product-task-experiment';
import { isImportProduct } from './utils';
import './PaymentGatewaySuggestions';
import './shipping';
import './Marketing';
@ -12,28 +11,15 @@ import './tax';
import './woocommerce-payments';
import './purchase';
const possiblyImportProductTaskExperiment = async () => {
const isExperiment = await isProductTaskExperimentTreatment();
if ( isExperiment ) {
if ( isImportProductExperiment() ) {
import( './experimental-import-products' );
} else {
import( './experimental-products' );
}
const possiblyImportProductTask = async () => {
if ( isImportProduct() ) {
import( './import-products' );
} else {
import( './products' );
}
};
if (
window.wcAdminFeatures &&
( window.wcAdminFeatures[ 'experimental-import-products-task' ] ||
window.wcAdminFeatures[ 'experimental-products-task' ] )
) {
possiblyImportProductTaskExperiment();
} else {
import( './products' );
}
possiblyImportProductTask();
if (
window.wcAdminFeatures &&

View File

@ -1,7 +0,0 @@
/**
* Internal dependencies
*/
import './products';
import ProductTemplateModal from './product-template-modal';
export { ProductTemplateModal };

View File

@ -6,7 +6,7 @@ import { WooOnboardingTask } from '@woocommerce/onboarding';
import { Text } from '@woocommerce/experimental';
import { registerPlugin } from '@wordpress/plugins';
import { useMemo, useState } from '@wordpress/element';
import { Button, Spinner } from '@wordpress/components';
import { Button } from '@wordpress/components';
import { getAdminLink } from '@woocommerce/settings';
import { Icon, chevronDown, chevronUp } from '@wordpress/icons';
import { recordEvent } from '@woocommerce/tracks';
@ -21,14 +21,11 @@ import { getSurfacedProductTypeKeys, getProductTypes } from './utils';
import useProductTypeListItems from './use-product-types-list-items';
import Stack from './stack';
import Footer from './footer';
import CardLayout from './card-layout';
import { LoadSampleProductType } from './constants';
import LoadSampleProductModal from '../components/load-sample-product-modal';
import useLoadSampleProducts from '../components/use-load-sample-products';
import LoadSampleProductConfirmModal from '../components/load-sample-product-confirm-modal';
import useRecordCompletionTime from '../use-record-completion-time';
import { getCountryCode } from '~/dashboard/utils';
import { useProductTaskExperiment } from './use-product-layout-experiment';
const getOnboardingProductType = (): string[] => {
const onboardingData = getAdminSetting( 'onboarding' );
@ -59,8 +56,6 @@ export const Products = () => {
isConfirmingLoadSampleProducts,
setIsConfirmingLoadSampleProducts,
] = useState( false );
const { isLoading: isLoadingExperiment, experimentLayout } =
useProductTaskExperiment();
const { isStoreInUS } = useSelect( ( select ) => {
const { getSettings } = select( SETTINGS_STORE_NAME );
@ -120,93 +115,66 @@ export const Products = () => {
! surfacedProductTypes.includes( productType ) &&
surfacedProductTypes.push( productType )
);
if ( experimentLayout === 'card' ) {
surfacedProductTypes.push( {
...LoadSampleProductType,
onClick: () => setIsConfirmingLoadSampleProducts( true ),
} );
}
}
return surfacedProductTypes;
}, [
surfacedProductTypeKeys,
isExpanded,
productTypesWithTimeRecord,
experimentLayout,
] );
}, [ surfacedProductTypeKeys, isExpanded, productTypesWithTimeRecord ] );
return (
<div className="woocommerce-task-products">
{ isLoadingExperiment ? (
<Spinner />
) : (
<>
<Text
variant="title"
as="h2"
className="woocommerce-task-products__title"
>
{ __(
'What product do you want to add?',
'woocommerce'
) }
</Text>
<Text
variant="title"
as="h2"
className="woocommerce-task-products__title"
>
{ __( 'What product do you want to add?', 'woocommerce' ) }
</Text>
<div className="woocommerce-product-content">
{ experimentLayout === 'stacked' ? (
<Stack
items={ visibleProductTypes }
onClickLoadSampleProduct={ () =>
setIsConfirmingLoadSampleProducts( true )
}
showOtherOptions={ isExpanded }
/>
) : (
<CardLayout items={ visibleProductTypes } />
) }
<ViewControlButton
isExpanded={ isExpanded }
onClick={ () => {
if ( ! isExpanded ) {
recordEvent(
'tasklist_view_more_product_types_click'
);
}
setIsExpanded( ! isExpanded );
} }
/>
<Footer />
</div>
{ isLoadingSampleProducts ? (
<LoadSampleProductModal />
) : (
isConfirmingLoadSampleProducts && (
<LoadSampleProductConfirmModal
onCancel={ () => {
setIsConfirmingLoadSampleProducts( false );
recordEvent(
'tasklist_cancel_load_sample_products_click'
);
} }
onImport={ () => {
setIsConfirmingLoadSampleProducts( false );
loadSampleProduct();
} }
/>
)
) }
</>
<div className="woocommerce-product-content">
<Stack
items={ visibleProductTypes }
onClickLoadSampleProduct={ () =>
setIsConfirmingLoadSampleProducts( true )
}
showOtherOptions={ isExpanded }
/>
<ViewControlButton
isExpanded={ isExpanded }
onClick={ () => {
if ( ! isExpanded ) {
recordEvent(
'tasklist_view_more_product_types_click'
);
}
setIsExpanded( ! isExpanded );
} }
/>
<Footer />
</div>
{ isLoadingSampleProducts ? (
<LoadSampleProductModal />
) : (
isConfirmingLoadSampleProducts && (
<LoadSampleProductConfirmModal
onCancel={ () => {
setIsConfirmingLoadSampleProducts( false );
recordEvent(
'tasklist_cancel_load_sample_products_click'
);
} }
onImport={ () => {
setIsConfirmingLoadSampleProducts( false );
loadSampleProduct();
} }
/>
)
) }
</div>
);
};
const ExperimentalProductsFill = () => {
const { isLoading, experimentLayout } = useProductTaskExperiment();
return isLoading ? null : (
<WooOnboardingTask id="products" variant={ experimentLayout }>
const ProductsFill = () => {
return (
<WooOnboardingTask id="products">
<Products />
</WooOnboardingTask>
);
@ -215,5 +183,5 @@ const ExperimentalProductsFill = () => {
registerPlugin( 'wc-admin-onboarding-task-products', {
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
scope: 'woocommerce-tasks',
render: () => <ExperimentalProductsFill />,
render: () => <ProductsFill />,
} );

View File

@ -1,198 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Modal, RadioControl } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { applyFilters } from '@wordpress/hooks';
import {
ITEMS_STORE_NAME,
ONBOARDING_STORE_NAME,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './product-template-modal.scss';
import { createNoticesFromResponse } from '../../../lib/notices';
import { getCountryCode } from '../../../dashboard/utils';
export const ONBOARDING_PRODUCT_TEMPLATES_FILTER =
'woocommerce_admin_onboarding_product_templates';
const getProductTemplates = () => [
{
key: 'physical',
title: __( 'Physical product', 'woocommerce' ),
subtitle: __(
'Tangible items that get delivered to customers',
'woocommerce'
),
},
{
key: 'digital',
title: __( 'Digital product', 'woocommerce' ),
subtitle: __(
'Items that customers download or access through your website',
'woocommerce'
),
},
{
key: 'variable',
title: __( 'Variable product', 'woocommerce' ),
subtitle: __(
'Products with several versions that customers can choose from',
'woocommerce'
),
},
{
key: 'subscription',
title: __( 'Subscription product', 'woocommerce' ),
subtitle: __(
'Products that customers receive or gain access to regularly by paying in advance',
'woocommerce'
),
},
];
export default function ProductTemplateModal( { onClose } ) {
const [ selectedTemplate, setSelectedTemplate ] = useState( null );
const [ isRedirecting, setIsRedirecting ] = useState( false );
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );
const { countryCode, profileItems } = useSelect( ( select ) => {
const { getProfileItems } = select( ONBOARDING_STORE_NAME );
const { getSettings } = select( SETTINGS_STORE_NAME );
const { general: settings = {} } = getSettings( 'general' );
return {
countryCode: getCountryCode( settings.woocommerce_default_country ),
profileItems: getProfileItems(),
};
} );
const { installedPlugins } = useSelect( ( select ) => {
const { getInstalledPlugins } = select( PLUGINS_STORE_NAME );
return {
installedPlugins: getInstalledPlugins(),
};
} );
const createTemplate = () => {
setIsRedirecting( true );
recordEvent( 'tasklist_product_template_selection', {
product_type: selectedTemplate,
} );
if ( selectedTemplate === 'subscription' ) {
window.location.href = getAdminLink(
'post-new.php?post_type=product&subscription_pointers=true'
);
return;
}
if ( selectedTemplate ) {
createProductFromTemplate(
{
template_name: selectedTemplate,
status: 'draft',
},
{ _fields: [ 'id' ] }
).then(
( data ) => {
if ( data && data.id ) {
const link = getAdminLink(
`post.php?post=${ data.id }&action=edit&wc_onboarding_active_task=products&tutorial=true`
);
window.location = link;
}
},
( error ) => {
// failed creating product with template
createNoticesFromResponse( error );
setIsRedirecting( false );
}
);
} else if ( onClose ) {
recordEvent( 'tasklist_product_template_dismiss' );
onClose();
}
};
const removeSubscriptions =
( window.wcAdminFeatures && ! window.wcAdminFeatures.subscriptions ) ||
countryCode !== 'US' ||
! profileItems.product_types?.includes( 'subscriptions' ) ||
! installedPlugins.includes( 'woocommerce-payments' );
const productTemplates = removeSubscriptions
? getProductTemplates().filter(
( template ) => template.key !== 'subscription'
)
: getProductTemplates();
/**
* An object defining a product template.
*
* @typedef {Object} template
* @property {string} key Icon to render.
* @property {string} title Url.
* @property {string} subtitle Link title.
*/
/**
* Store product templates.
*
* @filter woocommerce_admin_onboarding_product_templates
* @param {Array.<template>} templates Array of product templates.
*/
const templates = applyFilters(
ONBOARDING_PRODUCT_TEMPLATES_FILTER,
productTemplates
);
return (
<Modal
title={ __( 'Start with a template', 'woocommerce' ) }
isDismissible={ true }
onRequestClose={ () => onClose() }
className="woocommerce-product-template-modal"
>
<div className="woocommerce-product-template-modal__wrapper">
<div className="woocommerce-product-template-modal__list">
<RadioControl
selected={ selectedTemplate }
options={ templates.map( ( item ) => {
return {
label: (
<>
<span className="woocommerce-product-template-modal__list-title">
{ item.title }
</span>
<span className="woocommerce-product-template-modal__list-subtitle">
{ item.subtitle }
</span>
</>
),
value: item.key,
};
} ) }
onChange={ setSelectedTemplate }
/>
</div>
<div className="woocommerce-product-template-modal__actions">
<Button
isPrimary
isBusy={ isRedirecting }
disabled={ ! selectedTemplate || isRedirecting }
onClick={ createTemplate }
>
{ __( 'Go', 'woocommerce' ) }
</Button>
</div>
</div>
</Modal>
);
}

View File

@ -1,53 +0,0 @@
$border-color: $gray-100;
.woocommerce-product-template-modal {
@include breakpoint( '>600px' ) {
min-width: 565px;
}
.woocommerce-product-template-modal__actions {
padding-top: $gap-large;
}
}
.woocommerce-product-template-modal__list {
.components-base-control {
margin: 0 -#{$gap-large};
.components-base-control__field {
.components-radio-control__option {
display: flex;
text-decoration: none;
width: 100%;
align-items: center;
padding: $gap $gap-large;
margin-bottom: 0;
box-sizing: border-box;
border-bottom: 1px solid $gray-100;
&:first-child {
border-top: 1px solid $gray-100;
}
.components-radio-control__input {
margin-right: $gap;
flex: none;
&:checked::before {
box-sizing: border-box;
}
&:focus {
box-shadow: none;
}
}
}
}
}
.woocommerce-product-template-modal__list-title {
color: $gray-900;
display: block;
}
.woocommerce-product-template-modal__list-subtitle {
color: $gray-700;
margin-top: 4px;
display: block;
}
}
.woocommerce-product-template-modal__actions {
text-align: right;
}

View File

@ -1,173 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Fragment, useState } from '@wordpress/element';
import { Card, CardBody } from '@wordpress/components';
import {
Icon,
sidebar,
chevronRight,
plusCircle,
archive,
download,
} from '@wordpress/icons';
import {
ONBOARDING_STORE_NAME,
PLUGINS_STORE_NAME,
SETTINGS_STORE_NAME,
} from '@woocommerce/data';
import { useSelect } from '@wordpress/data';
import { List, Pill } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/settings';
import { recordEvent } from '@woocommerce/tracks';
import { registerPlugin } from '@wordpress/plugins';
import { WooOnboardingTask } from '@woocommerce/onboarding';
/**
* Internal dependencies
*/
import ProductTemplateModal from './product-template-modal';
import { getCountryCode } from '../../../dashboard/utils';
const getSubTasks = () => [
{
key: 'addProductTemplate',
title: (
<>
{ __( 'Start with a template', 'woocommerce' ) }
<Pill>{ __( 'Recommended', 'woocommerce' ) }</Pill>
</>
),
content: __(
'Use a template to add physical, digital, and variable products',
'woocommerce'
),
before: <Icon icon={ sidebar }></Icon>,
after: <Icon icon={ chevronRight } />,
onClick: () =>
recordEvent( 'tasklist_add_product', {
method: 'product_template',
} ),
},
{
key: 'addProductManually',
title: __( 'Add manually', 'woocommerce' ),
content: __(
'For small stores we recommend adding products manually',
'woocommerce'
),
before: <Icon icon={ plusCircle } />,
after: <Icon icon={ chevronRight } />,
onClick: () =>
recordEvent( 'tasklist_add_product', { method: 'manually' } ),
href: getAdminLink(
'post-new.php?post_type=product&wc_onboarding_active_task=products&tutorial=true'
),
},
{
key: 'importProducts',
title: __( 'Import via CSV', 'woocommerce' ),
content: __(
'For larger stores we recommend importing all products at once via CSV file',
'woocommerce'
),
before: <Icon icon={ archive } />,
after: <Icon icon={ chevronRight } />,
onClick: () =>
recordEvent( 'tasklist_add_product', { method: 'import' } ),
href: getAdminLink(
'edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=products'
),
},
{
key: 'migrateProducts',
title: __( 'Import from another service', 'woocommerce' ),
content: __(
'For stores currently selling elsewhere we suggest using a product migration service',
'woocommerce'
),
before: <Icon icon={ download } />,
after: <Icon icon={ chevronRight } />,
onClick: () =>
recordEvent( 'tasklist_add_product', { method: 'migrate' } ),
// @todo This should be replaced with the in-app purchase iframe when ready.
href: 'https://woocommerce.com/products/cart2cart/?utm_medium=product',
target: '_blank',
},
];
const Products = () => {
const [ selectTemplate, setSelectTemplate ] = useState( null );
const { countryCode, profileItems } = useSelect( ( select ) => {
const { getProfileItems } = select( ONBOARDING_STORE_NAME );
const { getSettings } = select( SETTINGS_STORE_NAME );
const { general: settings = {} } = getSettings( 'general' );
return {
countryCode: getCountryCode( settings.woocommerce_default_country ),
profileItems: getProfileItems(),
};
} );
const { installedPlugins } = useSelect( ( select ) => {
const { getInstalledPlugins } = select( PLUGINS_STORE_NAME );
return {
installedPlugins: getInstalledPlugins(),
};
} );
const subTasks = getSubTasks();
if (
window.wcAdminFeatures &&
window.wcAdminFeatures.subscriptions &&
countryCode === 'US' &&
profileItems.product_types?.includes( 'subscriptions' ) &&
installedPlugins.includes( 'woocommerce-payments' )
) {
const task = subTasks.find(
( { key } ) => key === 'addProductTemplate'
);
task.content = __(
'Use a template to add physical, digital, variable, and subscription products',
'woocommerce'
);
}
const onTaskClick = ( task ) => {
task.onClick();
if ( task.key === 'addProductTemplate' ) {
setSelectTemplate( true );
}
};
const listItems = subTasks.map( ( task ) => ( {
...task,
onClick: () => onTaskClick( task ),
} ) );
return (
<Fragment>
<Card className="woocommerce-task-card">
<CardBody size={ null }>
<List items={ listItems } />
</CardBody>
</Card>
{ selectTemplate ? (
<ProductTemplateModal
onClose={ () => setSelectTemplate( null ) }
/>
) : null }
</Fragment>
);
};
registerPlugin( 'wc-admin-onboarding-task-products', {
scope: 'woocommerce-tasks',
render: () => (
<WooOnboardingTask id="products" variant="control">
<Products />
</WooOnboardingTask>
),
} );

View File

@ -9,10 +9,9 @@ import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { Products } from '../';
import { Products } from '..';
import { defaultSurfacedProductTypes, productTypes } from '../constants';
import { getAdminSetting } from '~/utils/admin-settings';
import { useProductTaskExperiment } from '../use-product-layout-experiment';
jest.mock( '@wordpress/data', () => ( {
...jest.requireActual( '@wordpress/data' ),
@ -23,13 +22,6 @@ jest.mock( '~/utils/admin-settings', () => ( {
getAdminSetting: jest.fn(),
} ) );
jest.mock( '../use-product-layout-experiment', () => ( {
useProductTaskExperiment: jest.fn().mockReturnValue( {
isLoading: false,
experimentLayout: 'stacked',
} ),
} ) );
jest.mock( '../use-create-product-by-type', () => ( {
useCreateProductByType: jest
.fn()
@ -306,41 +298,7 @@ describe( 'Products', () => {
);
} );
it( 'should show spinner when layout experiment is loading', async () => {
( useProductTaskExperiment as jest.Mock ).mockImplementation( () => {
return {
isLoading: true,
experimentLayout: 'card',
};
} );
const { container } = render( <Products /> );
expect(
container.getElementsByClassName( 'components-spinner' )
).toHaveLength( 1 );
} );
it( 'should render card layout when experiment is assigned', async () => {
( useProductTaskExperiment as jest.Mock ).mockImplementation( () => {
return {
isLoading: false,
experimentLayout: 'card',
};
} );
const { container } = render( <Products /> );
expect(
container.getElementsByClassName(
'woocommerce-products-card-layout'
)
).toHaveLength( 1 );
} );
it( 'should render stacked layout when experiment is assigned', async () => {
( useProductTaskExperiment as jest.Mock ).mockImplementation( () => {
return {
isLoading: false,
experimentLayout: 'stacked',
};
} );
it( 'should render stacked layout', async () => {
const { container } = render( <Products /> );
expect(
container.getElementsByClassName( 'woocommerce-products-stack' )

View File

@ -8,9 +8,9 @@ const onboardingData = getAdminSetting( 'onboarding' );
/**
* Returns true if the merchant has indicated that they have another online shop while filling out the OBW
*/
export const isImportProductExperiment = () => {
export const isImportProduct = () => {
return (
window?.wcAdminFeatures?.[ 'experimental-import-products-task' ] &&
window?.wcAdminFeatures?.[ 'import-products-task' ] &&
onboardingData?.profile?.selling_venues &&
onboardingData?.profile?.selling_venues !== 'no'
);

View File

@ -11,7 +11,6 @@ declare global {
analytics: boolean;
coupons: boolean;
'customer-effort-score-tracks': boolean;
'experimental-products-task': boolean;
homescreen: boolean;
marketing: boolean;
'minified-js': boolean;

View File

@ -0,0 +1,4 @@
Significance: patch
Type: dev
Cleanup product task experiment

View File

@ -4,8 +4,7 @@
"analytics": true,
"coupons": true,
"customer-effort-score-tracks": true,
"experimental-products-task": true,
"experimental-import-products-task": true,
"import-products-task": true,
"experimental-fashion-sample-products": true,
"shipping-smart-defaults": true,
"shipping-setting-tour": true,

View File

@ -4,8 +4,7 @@
"analytics": true,
"coupons": true,
"customer-effort-score-tracks": true,
"experimental-products-task": true,
"experimental-import-products-task": true,
"import-products-task": true,
"experimental-fashion-sample-products": true,
"shipping-smart-defaults": true,
"shipping-setting-tour": true,

View File

@ -340,38 +340,13 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
* @return WP_Error|WP_REST_Response
*/
public static function import_sample_products() {
if (
( Features::is_enabled( 'experimental-import-products-task' ) || Features::is_enabled( 'experimental-products-task' ) )
&& static::is_experiment_product_task()
) {
$sample_csv_file = Features::is_enabled( 'experimental-fashion-sample-products' ) ? WC_ABSPATH . 'sample-data/experimental_fashion_sample_9_products.csv' :
WC_ABSPATH . 'sample-data/experimental_sample_9_products.csv';
} else {
$sample_csv_file = WC_ABSPATH . 'sample-data/sample_products.csv';
}
$sample_csv_file = Features::is_enabled( 'experimental-fashion-sample-products' ) ? WC_ABSPATH . 'sample-data/experimental_fashion_sample_9_products.csv' :
WC_ABSPATH . 'sample-data/experimental_sample_9_products.csv';
$import = self::import_sample_products_from_csv( $sample_csv_file );
return rest_ensure_response( $import );
}
/**
* Check if product task experiment is treatment.
*
* @return bool
*/
public static function is_experiment_product_task() {
$anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : '';
$allow_tracking = get_option( 'woocommerce_allow_tracking' ) === 'yes';
$abtest = new \WooCommerce\Admin\Experimental_Abtest(
$anon_id,
'woocommerce',
$allow_tracking
);
return $abtest->get_variation( 'woocommerce_products_task_layout_stacked_v3' ) === 'treatment' ||
$abtest->get_variation( 'woocommerce_products_task_layout_card_v3' ) === 'treatment';
}
/**
* Creates a product from a template name passed in through the template_name param.
*