* Add monthly pricing toggle

* Move product type label to its own folder

* Add popover to product type label

* Add card help text

* Add product type component tests

* Add tests for product types step

* Refactor validation in product types

* Export ProductTypes component for testing
This commit is contained in:
Joshua T Flowers 2020-08-28 15:39:24 +03:00 committed by GitHub
parent dbec08f7dc
commit 94db7f7f76
9 changed files with 776 additions and 124 deletions

View File

@ -1,18 +0,0 @@
.woocommerce-profile-wizard__product-types {
.components-base-control__field {
display: flex;
}
.woocommerce-product-wizard__product-types__label {
display: inline-block;
margin-right: 12px;
}
.woocommerce-profile-wizard__checkbox {
border-bottom: 1px solid $gray-100;
}
.woocommerce-profile-wizard__card-actions .components-button.is-primary {
margin-top: $gap;
}
}

View File

@ -1,60 +1,24 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { Button, CheckboxControl, Tooltip } from '@wordpress/components';
import { Button, CheckboxControl, FormToggle } from '@wordpress/components';
import { includes, filter, get } from 'lodash';
import interpolateComponents from 'interpolate-components';
import { withDispatch, withSelect } from '@wordpress/data';
import { getSetting } from '@woocommerce/wc-admin-settings';
import { H, Card, Link, Pill } from '@woocommerce/components';
import { H, Card } from '@woocommerce/components';
import { ONBOARDING_STORE_NAME } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './product-types.scss';
import ProductType from './product-type';
import './style.scss';
function getLabel( description, yearlyPrice ) {
if ( ! yearlyPrice ) {
return description;
}
const monthlyPrice = ( yearlyPrice / 12.0 ).toFixed( 2 );
const priceDescription = sprintf(
__( '$%f per month, billed annually', 'woocommerce-admin' ),
monthlyPrice
);
/* eslint-disable @wordpress/i18n-no-collapsible-whitespace */
const toolTipText = __(
"This product type requires a paid extension.\nWe'll add this to a cart so that\nyou can purchase and install it later.",
'woocommerce-admin'
);
/* eslint-enable @wordpress/i18n-no-collapsible-whitespace */
return (
<Fragment>
<span className="woocommerce-product-wizard__product-types__label">
{ description }
</span>
<Tooltip text={ toolTipText } position="bottom center">
<span>
<Pill>
<span className="screen-reader-text">
{ toolTipText }
</span>
{ priceDescription }
</Pill>
</span>
</Tooltip>
</Fragment>
);
}
class ProductTypes extends Component {
export class ProductTypes extends Component {
constructor( props ) {
super();
const profileItems = get( props, 'profileItems', {} );
@ -66,6 +30,7 @@ class ProductTypes extends Component {
this.state = {
error: null,
isMonthlyPricing: true,
selected: profileItems.product_types || defaultProductTypes,
};
@ -73,7 +38,7 @@ class ProductTypes extends Component {
this.onChange = this.onChange.bind( this );
}
async validateField() {
validateField() {
const error = this.state.selected.length
? null
: __(
@ -81,37 +46,31 @@ class ProductTypes extends Component {
'woocommerce-admin'
);
this.setState( { error } );
return ! error;
}
async onContinue() {
await this.validateField();
if ( this.state.error ) {
onContinue() {
if ( ! this.validateField() ) {
return;
}
const {
createNotice,
goToNextStep,
isError,
updateProfileItems,
} = this.props;
const { createNotice, goToNextStep, updateProfileItems } = this.props;
recordEvent( 'storeprofiler_store_product_type_continue', {
product_type: this.state.selected,
} );
await updateProfileItems( { product_types: this.state.selected } );
if ( ! isError ) {
goToNextStep();
} else {
createNotice(
'error',
__(
'There was a problem updating your product types.',
'woocommerce-admin'
updateProfileItems( { product_types: this.state.selected } )
.then( () => goToNextStep() )
.catch( () =>
createNotice(
'error',
__(
'There was a problem updating your product types.',
'woocommerce-admin'
)
)
);
}
}
onChange( slug ) {
@ -135,15 +94,9 @@ class ProductTypes extends Component {
);
}
onLearnMore( slug ) {
recordEvent( 'storeprofiler_store_product_type_learn_more', {
product_type: slug,
} );
}
render() {
const { productTypes = {} } = getSetting( 'onboarding', {} );
const { error, selected } = this.state;
const { error, isMonthlyPricing, selected } = this.state;
return (
<div className="woocommerce-profile-wizard__product-types">
@ -160,51 +113,51 @@ class ProductTypes extends Component {
<Card>
<div className="woocommerce-profile-wizard__checkbox-group">
{ Object.keys( productTypes ).map( ( slug ) => {
const label = getLabel(
productTypes[ slug ].label,
productTypes[ slug ].yearly_price
);
const moreUrl = productTypes[ slug ].more_url;
const helpText =
productTypes[ slug ].description &&
interpolateComponents( {
mixedString:
productTypes[ slug ].description +
( productTypes[ slug ].more_url
? ' {{moreLink/}}'
: '' ),
components: {
moreLink: moreUrl ? (
<Link
href={ moreUrl }
target="_blank"
type="external"
onClick={ () =>
this.onLearnMore( slug )
}
>
{ __(
'Learn more',
'woocommerce-admin'
) }
</Link>
) : (
''
),
},
} );
return (
<CheckboxControl
key={ slug }
label={ label }
help={ helpText }
label={
<ProductType
description={
productTypes[ slug ].description
}
label={ productTypes[ slug ].label }
annualPrice={
productTypes[ slug ]
.yearly_price
}
isMonthlyPricing={
isMonthlyPricing
}
moreUrl={
productTypes[ slug ].more_url
}
slug={ slug }
/>
}
onChange={ () => this.onChange( slug ) }
checked={ selected.includes( slug ) }
className="woocommerce-profile-wizard__checkbox"
/>
);
} ) }
<div className="woocommerce-profile-wizard__product-types-pricing-toggle woocommerce-profile-wizard__checkbox">
<label htmlFor="woocommerce-product-types__pricing-toggle">
{ __(
'Display monthly prices',
'woocommerce-admin'
) }
<FormToggle
id="woocommerce-product-types__pricing-toggle"
checked={ isMonthlyPricing }
onChange={ () =>
this.setState( {
isMonthlyPricing: ! isMonthlyPricing,
} )
}
/>
</label>
</div>
{ error && (
<span className="woocommerce-profile-wizard__error">
{ error }
@ -222,6 +175,12 @@ class ProductTypes extends Component {
</Button>
</div>
</Card>
<div className="woocommerce-profile-wizard__card-help-text">
{ __(
'Billing is annual. All purchases are covered by our 30 day money back guarantee and include access to support and updates. Extensions will be added to a cart for you to purchase later.',
'woocommerce-admin'
) }
</div>
</div>
);
}

View File

@ -0,0 +1,99 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Button, Popover, Tooltip } from '@wordpress/components';
import { useState } from '@wordpress/element';
import interpolateComponents from 'interpolate-components';
import { Link, Pill } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
export default function ProductType( {
annualPrice,
description,
isMonthlyPricing,
label,
moreUrl,
slug,
} ) {
const [ isPopoverVisible, setIsPopoverVisible ] = useState( '' );
if ( ! annualPrice ) {
return label;
}
/* eslint-disable @wordpress/i18n-no-collapsible-whitespace */
const toolTipText = __(
"This product type requires a paid extension.\nWe'll add this to a cart so that\nyou can purchase and install it later.",
'woocommerce-admin'
);
/* eslint-enable @wordpress/i18n-no-collapsible-whitespace */
return (
<div className="woocommerce-product-type">
<span className="woocommerce-product-type__label">{ label }</span>
<Button
isTertiary
label={ __(
'Learn more about recommended free business features',
'woocommerce-admin'
) }
onClick={ () => {
setIsPopoverVisible( true );
} }
>
<i className="material-icons-outlined" aria-hidden="true">
info
</i>
</Button>
{ isPopoverVisible && (
<Popover
focusOnMount="container"
position="top center"
onClose={ () => setIsPopoverVisible( false ) }
>
{ interpolateComponents( {
mixedString:
description + ( moreUrl ? ' {{moreLink/}}' : '' ),
components: {
moreLink: moreUrl ? (
<Link
href={ moreUrl }
target="_blank"
type="external"
onClick={ () =>
recordEvent(
'storeprofiler_store_product_type_learn_more',
{
product_type: slug,
}
)
}
>
{ __( 'Learn more', 'woocommerce-admin' ) }
</Link>
) : (
''
),
},
} ) }
</Popover>
) }
<Tooltip text={ toolTipText } position="bottom center">
<Pill>
<span className="screen-reader-text">{ toolTipText }</span>
{ isMonthlyPricing
? sprintf(
/* translators: Dollar amount (example: $4.08 ) */
__( '$%f per month', 'woocommerce-admin' ),
( annualPrice / 12.0 ).toFixed( 2 )
)
: sprintf(
/* translators: Dollar amount (example: $49.00 ) */
__( '$%f per year', 'woocommerce-admin' ),
annualPrice
) }
</Pill>
</Tooltip>
</div>
);
}

View File

@ -0,0 +1,69 @@
.woocommerce-profile-wizard__product-types {
.components-base-control__field {
display: flex;
}
.woocommerce-product-type {
align-items: center;
display: flex;
}
.woocommerce-product-type__label {
display: inline-block;
margin-right: 12px;
}
.woocommerce-profile-wizard__checkbox {
display: flex;
align-items: center;
border-bottom: 1px solid $gray-100;
.components-button {
padding-top: 0;
padding-bottom: 0;
height: auto;
}
}
.components-base-control__field {
display: flex;
width: 100%;
align-items: center;
}
.components-checkbox-control__label {
display: flex;
align-items: center;
width: 100%;
.woocommerce-pill {
margin-left: auto;
}
}
.components-popover .components-popover__content {
min-width: 360px;
}
.woocommerce-profile-wizard__product-types-pricing-toggle.woocommerce-profile-wizard__checkbox {
display: flex;
align-items: center;
justify-content: flex-end;
color: $gray-600;
font-size: 14px;
label {
display: inline-flex;
align-items: center;
}
.components-form-toggle {
display: inline-flex;
margin-left: $gap;
}
}
.woocommerce-profile-wizard__card-actions .components-button.is-primary {
margin-top: $gap;
}
}

View File

@ -0,0 +1,307 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProductTypes should render product types 1`] = `
<div>
<div
class="woocommerce-profile-wizard__product-types"
>
<h2
class="woocommerce-profile-wizard__header-title"
>
What type of products will be listed?
</h2>
<h2
class="woocommerce-profile-wizard__header-subtitle"
>
Choose any that apply
</h2>
<div
class="woocommerce-card"
>
<div
class="woocommerce-card__body"
>
<div
class="woocommerce-profile-wizard__checkbox-group"
>
<div
class="components-base-control woocommerce-profile-wizard__checkbox"
>
<div
class="components-base-control__field"
>
<span
class="components-checkbox-control__input-container"
>
<input
class="components-checkbox-control__input"
id="inspector-checkbox-control-0"
type="checkbox"
value="1"
/>
</span>
<label
class="components-checkbox-control__label"
for="inspector-checkbox-control-0"
>
<div
class="woocommerce-product-type"
>
<span
class="woocommerce-product-type__label"
>
Paid product
</span>
<button
aria-label="Learn more about recommended free business features"
class="components-button is-tertiary"
type="button"
>
<i
aria-hidden="true"
class="material-icons-outlined"
>
info
</i>
</button>
<span
class="woocommerce-pill"
>
<span
class="screen-reader-text"
>
This product type requires a paid extension.
We'll add this to a cart so that
you can purchase and install it later.
</span>
$10 per month
</span>
</div>
</label>
</div>
</div>
<div
class="components-base-control woocommerce-profile-wizard__checkbox"
>
<div
class="components-base-control__field"
>
<span
class="components-checkbox-control__input-container"
>
<input
class="components-checkbox-control__input"
id="inspector-checkbox-control-1"
type="checkbox"
value="1"
/>
</span>
<label
class="components-checkbox-control__label"
for="inspector-checkbox-control-1"
>
Free product
</label>
</div>
</div>
<div
class="woocommerce-profile-wizard__product-types-pricing-toggle woocommerce-profile-wizard__checkbox"
>
<label
for="woocommerce-product-types__pricing-toggle"
>
Display monthly prices
<span
class="components-form-toggle is-checked"
>
<input
checked=""
class="components-form-toggle__input"
id="woocommerce-product-types__pricing-toggle"
type="checkbox"
/>
<span
class="components-form-toggle__track"
/>
<span
class="components-form-toggle__thumb"
/>
</span>
</label>
</div>
</div>
<div
class="woocommerce-profile-wizard__card-actions"
>
<button
class="components-button is-primary"
disabled=""
type="button"
>
Continue
</button>
</div>
</div>
</div>
<div
class="woocommerce-profile-wizard__card-help-text"
>
Billing is annual. All purchases are covered by our 30 day money back guarantee and include access to support and updates. Extensions will be added to a cart for you to purchase later.
</div>
</div>
</div>
`;
exports[`ProductTypes should show annual prices on toggle 1`] = `
<div>
<div
class="woocommerce-profile-wizard__product-types"
>
<h2
class="woocommerce-profile-wizard__header-title"
>
What type of products will be listed?
</h2>
<h2
class="woocommerce-profile-wizard__header-subtitle"
>
Choose any that apply
</h2>
<div
class="woocommerce-card"
>
<div
class="woocommerce-card__body"
>
<div
class="woocommerce-profile-wizard__checkbox-group"
>
<div
class="components-base-control woocommerce-profile-wizard__checkbox"
>
<div
class="components-base-control__field"
>
<span
class="components-checkbox-control__input-container"
>
<input
class="components-checkbox-control__input"
id="inspector-checkbox-control-2"
type="checkbox"
value="1"
/>
</span>
<label
class="components-checkbox-control__label"
for="inspector-checkbox-control-2"
>
<div
class="woocommerce-product-type"
>
<span
class="woocommerce-product-type__label"
>
Paid product
</span>
<button
aria-label="Learn more about recommended free business features"
class="components-button is-tertiary"
type="button"
>
<i
aria-hidden="true"
class="material-icons-outlined"
>
info
</i>
</button>
<span
class="woocommerce-pill"
>
<span
class="screen-reader-text"
>
This product type requires a paid extension.
We'll add this to a cart so that
you can purchase and install it later.
</span>
$120 per year
</span>
</div>
</label>
</div>
</div>
<div
class="components-base-control woocommerce-profile-wizard__checkbox"
>
<div
class="components-base-control__field"
>
<span
class="components-checkbox-control__input-container"
>
<input
class="components-checkbox-control__input"
id="inspector-checkbox-control-3"
type="checkbox"
value="1"
/>
</span>
<label
class="components-checkbox-control__label"
for="inspector-checkbox-control-3"
>
Free product
</label>
</div>
</div>
<div
class="woocommerce-profile-wizard__product-types-pricing-toggle woocommerce-profile-wizard__checkbox"
>
<label
for="woocommerce-product-types__pricing-toggle"
>
Display monthly prices
<span
class="components-form-toggle"
>
<input
checked=""
class="components-form-toggle__input"
id="woocommerce-product-types__pricing-toggle"
type="checkbox"
/>
<span
class="components-form-toggle__track"
/>
<span
class="components-form-toggle__thumb"
/>
</span>
</label>
</div>
</div>
<div
class="woocommerce-profile-wizard__card-actions"
>
<button
class="components-button is-primary"
disabled=""
type="button"
>
Continue
</button>
</div>
</div>
</div>
<div
class="woocommerce-profile-wizard__card-help-text"
>
Billing is annual. All purchases are covered by our 30 day money back guarantee and include access to support and updates. Extensions will be added to a cart for you to purchase later.
</div>
</div>
</div>
`;

View File

@ -0,0 +1,83 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ProductType should render the product type 1`] = `
<div
className="woocommerce-product-type"
>
<span
className="woocommerce-product-type__label"
>
Product type label
</span>
<ForwardRef(Button)
isTertiary={true}
label="Learn more about recommended free business features"
onClick={[Function]}
>
<i
aria-hidden="true"
className="material-icons-outlined"
>
info
</i>
</ForwardRef(Button)>
<Tooltip
position="bottom center"
text="This product type requires a paid extension.
We'll add this to a cart so that
you can purchase and install it later."
>
<Pill>
<span
className="screen-reader-text"
>
This product type requires a paid extension.
We'll add this to a cart so that
you can purchase and install it later.
</span>
$120 per year
</Pill>
</Tooltip>
</div>
`;
exports[`ProductType should render the product type with monthly prices 1`] = `
<div
className="woocommerce-product-type"
>
<span
className="woocommerce-product-type__label"
>
Product type label
</span>
<ForwardRef(Button)
isTertiary={true}
label="Learn more about recommended free business features"
onClick={[Function]}
>
<i
aria-hidden="true"
className="material-icons-outlined"
>
info
</i>
</ForwardRef(Button)>
<Tooltip
position="bottom center"
text="This product type requires a paid extension.
We'll add this to a cart so that
you can purchase and install it later."
>
<Pill>
<span
className="screen-reader-text"
>
This product type requires a paid extension.
We'll add this to a cart so that
you can purchase and install it later.
</span>
$10 per month
</Pill>
</Tooltip>
</div>
`;

View File

@ -0,0 +1,88 @@
/**
* External dependencies
*/
import { render, screen, waitFor } from '@testing-library/react';
import { setSetting } from '@woocommerce/wc-admin-settings';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import { ProductTypes } from '../';
describe( 'ProductTypes', () => {
beforeEach( () => {
setSetting( 'onboarding', {
productTypes: {
paidProduct: {
description: 'Paid product type',
label: 'Paid product',
more_url: 'https://woocommerce.com/paid-product',
product: 100,
slug: 'paid-product',
yearly_price: 120,
},
freeProduct: {
label: 'Free product',
},
},
} );
} );
afterEach( () => {
setSetting( 'onboarding', {} );
} );
test( 'should render product types', () => {
const { container } = render( <ProductTypes /> );
expect( container ).toMatchSnapshot();
} );
test( 'should show annual prices on toggle', () => {
const { container } = render( <ProductTypes /> );
const toggle = screen.getByLabelText( 'Display monthly prices', {
selector: 'input',
} );
userEvent.click( toggle );
expect( container ).toMatchSnapshot();
} );
test( 'should validate on continue', async () => {
const mockCreateNotice = jest.fn();
const mockGoToNextStep = jest.fn();
const mockUpdateProfileItems = jest.fn().mockResolvedValue();
render(
<ProductTypes
createNotice={ mockCreateNotice }
goToNextStep={ mockGoToNextStep }
updateProfileItems={ mockUpdateProfileItems }
/>
);
const continueButton = screen.getByText( 'Continue', {
selector: 'button',
} );
const productType = screen.getByText( 'Free product', {
selector: 'label',
} );
// Validation should fail since no product types are selected.
userEvent.click( continueButton );
await waitFor( () => {
expect( mockGoToNextStep ).not.toHaveBeenCalled();
expect( mockUpdateProfileItems ).not.toHaveBeenCalled();
} );
// Click on a product type to pass validation.
userEvent.click( productType );
userEvent.click( continueButton );
await waitFor( () => {
expect( mockUpdateProfileItems ).toHaveBeenCalled();
expect( mockGoToNextStep ).toHaveBeenCalled();
} );
} );
} );

View File

@ -0,0 +1,58 @@
/**
* External dependencies
*/
import { render, screen } from '@testing-library/react';
import { shallow } from 'enzyme';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import ProductType from '../product-type';
const defaultProps = {
annualPrice: 120,
label: 'Product type label',
description: 'Product type description',
moreUrl: 'https://woocommerce.com/my-product-type',
slug: 'my-product-type',
};
describe( 'ProductType', () => {
test( 'should render the product type', () => {
const productType = shallow(
<ProductType { ...defaultProps } isMonthlyPricing={ false } />
);
expect( productType ).toMatchSnapshot();
} );
test( 'should render the product type with monthly prices', () => {
const productType = shallow(
<ProductType { ...defaultProps } isMonthlyPricing={ true } />
);
expect( productType ).toMatchSnapshot();
} );
test( 'should show Popover on click', () => {
const { container } = render(
<ProductType { ...defaultProps } isMonthlyPricing={ true } />
);
const infoButton = screen.getByLabelText(
'Learn more about recommended free business features',
{
selector: 'button',
}
);
userEvent.click( infoButton );
const popover = container.querySelector( '.components-popover' );
const learnMoreLink = popover.querySelector( 'a' );
expect( popover ).not.toBeNull();
expect( popover.textContent ).toBe(
defaultProps.description + ' Learn more'
);
expect( learnMoreLink.href ).toBe( defaultProps.moreUrl );
} );
} );

View File

@ -35,6 +35,13 @@
}
}
.woocommerce-card + .woocommerce-profile-wizard__card-help-text {
font-size: 14px;
color: $gray-600;
text-align: center;
margin-top: $gap;
}
.woocommerce-profile-wizard__header {
height: 56px;
border-bottom: 1px solid $studio-gray-5;