-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Blocks: Introduce a ProductPlanOverlapNotices block (#37513)
* 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
Showing
5 changed files
with
273 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
58
client/blocks/product-plan-overlap-notices/docs/example.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) ); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters