Skip to content

Commit

Permalink
Blocks: Introduce a ProductPlanOverlapNotices block (#37513)
Browse files Browse the repository at this point in the history
* Blocks: Introduce a ProductPlanOverlapNotices block

* Blocks: Introduce a ProductPlanOverlapNotices block example

* Blocks: Introduce a ProductPlanOverlapNotices block README

* Products: Introduce a generic list of product short names

* Devdocs: Add ProductPlanOverlapNotices example

* Ensure overlap only with current product

* Address feedback in docs and example
  • Loading branch information
tyxla authored Nov 13, 2019
1 parent f19008f commit fd19feb
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 0 deletions.
45 changes: 45 additions & 0 deletions client/blocks/product-plan-overlap-notices/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Product Plan Overlap Notices
=======

Product Plan Overlap Notices is a React component for rendering a block with notices.

Those notices will appear when there is a feature overlap between the current plan and the current product, when they are within the provided list of products and plans.

## Usage

```jsx
import React from 'react';
import ProductPlanOverlapNotices from 'blocks/product-plan-overlap-notices';

const jetpackPlans = [
'jetpack_personal',
'jetpack_personal_monthly',
'jetpack_premium',
'jetpack_premium_monthly',
'jetpack_business',
'jetpack_business_monthly',
];

const jetpackProducts = [
'jetpack_backup_daily',
'jetpack_backup_daily_monthly',
'jetpack_backup_realtime',
'jetpack_backup_realtime_monthly',
];

export default class extends React.Component {
render() {
return (
<ProductPlanOverlapNotices plans={ jetpackPlans } products={ jetpackProducts } />
);
}
}
```

## Props

The following props can be passed to the Product Plan Overlap Notices block:

* `plans`: ( array ) Array of plan slugs that we consider as possibly overlapping with products.
* `products`: ( array ) Array of product slugs that we consider as possibly overlapping with plans.
* `siteId`: ( number ) ID of the site we're fetching purchases and plans for. Optional - currently selected site will be used by default.
58 changes: 58 additions & 0 deletions client/blocks/product-plan-overlap-notices/docs/example.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* External dependencies
*/
import React, { Component } from 'react';

/**
* Internal dependencies
*/
import SitesDropdown from 'components/sites-dropdown';
import ProductPlanOverlapNotices from '../';

const jetpackPlans = [
'jetpack_personal',
'jetpack_personal_monthly',
'jetpack_premium',
'jetpack_premium_monthly',
'jetpack_business',
'jetpack_business_monthly',
];

const jetpackProducts = [
'jetpack_backup_daily',
'jetpack_backup_daily_monthly',
'jetpack_backup_realtime',
'jetpack_backup_realtime_monthly',
];

class ProductPlanOverlapNoticesExample extends Component {
state = {
siteId: 0,
};

render() {
return (
<div style={ { maxWidth: 520, margin: '0 auto' } }>
<div style={ { maxWidth: 300, margin: '0 auto 10px' } }>
<SitesDropdown onSiteSelect={ siteId => this.setState( { siteId } ) } />
</div>

{ this.state.siteId ? (
<ProductPlanOverlapNotices
plans={ jetpackPlans }
products={ jetpackProducts }
siteId={ this.state.siteId }
/>
) : (
<p style={ { textAlign: 'center' } }>
Please, select a Jetpack site to experience the full demo.
</p>
) }
</div>
);
}
}

ProductPlanOverlapNoticesExample.displayName = 'ProductPlanOverlapNotices';

export default ProductPlanOverlapNoticesExample;
163 changes: 163 additions & 0 deletions client/blocks/product-plan-overlap-notices/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* External dependencies
*/
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { find, includes, some } from 'lodash';
import { localize } from 'i18n-calypso';

/**
* Internal dependencies
*/
import Notice from 'components/notice';
import QueryProductsList from 'components/data/query-products-list';
import QuerySitePlans from 'components/data/query-site-plans';
import QuerySitePurchases from 'components/data/query-site-purchases';
import { getAvailableProductsList } from 'state/products-list/selectors';
import { getSelectedSiteId } from 'state/ui/selectors';
import { getSitePlanSlug } from 'state/sites/plans/selectors';
import { getSitePurchases } from 'state/purchases/selectors';
import { planHasFeature } from 'lib/plans';
import { PRODUCT_SHORT_NAMES } from 'lib/products-values/constants';

class ProductPlanOverlapNotices extends Component {
static propTypes = {
plans: PropTypes.arrayOf( PropTypes.string ).isRequired,
products: PropTypes.arrayOf( PropTypes.string ).isRequired,
siteId: PropTypes.number,

// Connected props
availableProducts: PropTypes.object,
currentPlanSlug: PropTypes.string,
purchases: PropTypes.array,
selectedSiteId: PropTypes.number,

// From localize() HoC
translate: PropTypes.func.isRequired,
};

hasOverlap() {
const { availableProducts, currentPlanSlug, plans, products, purchases } = this.props;

if ( ! currentPlanSlug || ! purchases || ! availableProducts ) {
return false;
}

// Is the current plan among the plans we're interested in?
if ( ! includes( plans, currentPlanSlug ) ) {
return false;
}

// Is the current product among the products we're interested in?
const currentProductSlug = this.getCurrentProductSlug();
if ( ! currentProductSlug ) {
return false;
}

// Does the current plan include the current product as a feature?
return some(
products,
productSlug =>
productSlug === currentProductSlug && planHasFeature( currentPlanSlug, productSlug )
);
}

getCurrentProductSlug() {
const { products, purchases } = this.props;

const currentProduct = find( purchases, purchase =>
includes( products, purchase.productSlug )
);
if ( ! currentProduct ) {
return null;
}

return currentProduct.productSlug;
}

getCurrentProductName() {
const { availableProducts } = this.props;
const currentProductSlug = this.getCurrentProductSlug();

if ( ! currentProductSlug || ! availableProducts[ currentProductSlug ] ) {
return '';
}

return availableProducts[ currentProductSlug ].product_name;
}

getCurrentPlanName() {
const { availableProducts, currentPlanSlug } = this.props;

if ( ! availableProducts[ currentPlanSlug ] ) {
return '';
}

return availableProducts[ currentPlanSlug ].product_name;
}

getOverlappingFeatureName() {
const { availableProducts } = this.props;

if ( ! this.hasOverlap() ) {
return null;
}

const currentProductSlug = this.getCurrentProductSlug();
if ( ! currentProductSlug ) {
return null;
}

if ( PRODUCT_SHORT_NAMES[ currentProductSlug ] ) {
return PRODUCT_SHORT_NAMES[ currentProductSlug ].toLowerCase();
}

if ( availableProducts[ currentProductSlug ] ) {
return availableProducts[ currentProductSlug ].product_name;
}

return null;
}

render() {
const { selectedSiteId, translate } = this.props;

return (
<Fragment>
<QuerySitePlans siteId={ selectedSiteId } />
<QuerySitePurchases siteId={ selectedSiteId } />
<QueryProductsList />

{ this.hasOverlap() && (
<Notice
status="is-warning"
text={ translate(
'Your %(planName)s Plan includes %(featureName)s. ' +
'Looks like you also purchased the %(productName)s product. ' +
'Consider removing %(productName)s.',
{
args: {
featureName: this.getOverlappingFeatureName(),
planName: this.getCurrentPlanName(),
productName: this.getCurrentProductName(),
},
}
) }
/>
) }
</Fragment>
);
}
}

export default connect( ( state, { siteId } ) => {
const selectedSiteId = siteId || getSelectedSiteId( state );

return {
availableProducts: getAvailableProductsList( state ),
currentPlanSlug: getSitePlanSlug( state, selectedSiteId ),
purchases: getSitePurchases( state, selectedSiteId ),
selectedSiteId,
};
} )( localize( ProductPlanOverlapNotices ) );
2 changes: 2 additions & 0 deletions client/devdocs/design/blocks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import FollowButton from 'blocks/follow-button/docs/example';
import LikeButtons from 'blocks/like-button/docs/example';
import PostSchedule from 'components/post-schedule/docs/example';
import PostSelector from 'my-sites/post-selector/docs/example';
import ProductPlanOverlapNotices from 'blocks/product-plan-overlap-notices/docs/example';
import ProductSelector from 'blocks/product-selector/docs/example';
import Site from 'blocks/site/docs/example';
import SitePlaceholder from 'blocks/site/docs/placeholder-example';
Expand Down Expand Up @@ -155,6 +156,7 @@ export default class AppComponents extends React.Component {
<PlanStorage readmeFilePath="plan-storage" />
<PostSchedule />
<PostSelector />
<ProductPlanOverlapNotices readmeFilePath="product-plan-overlap-notices" />
<ProductSelector readmeFilePath="product-selector" />
<Site readmeFilePath="site" />
<SitePlaceholder />
Expand Down
5 changes: 5 additions & 0 deletions client/lib/products-values/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export const JETPACK_BACKUP_PRODUCT_SHORT_NAMES = {
[ PRODUCT_JETPACK_BACKUP_REALTIME ]: translate( 'Real-Time Backups' ),
[ PRODUCT_JETPACK_BACKUP_REALTIME_MONTHLY ]: translate( 'Real-Time Backups' ),
};

export const PRODUCT_SHORT_NAMES = {
...JETPACK_BACKUP_PRODUCT_SHORT_NAMES,
};

export const JETPACK_BACKUP_PRODUCT_DAILY_DISPLAY_NAME = (
<Fragment>
{ translate( 'Jetpack Backup {{em}}Daily{{/em}}', {
Expand Down

0 comments on commit fd19feb

Please sign in to comment.