Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search: Add pricing and tier information to Plans page #15125

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions _inc/client/components/data/query-site-products/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* External dependencies
*/
import { useEffect } from 'react';
import { connect } from 'react-redux';

/**
* Internal dependencies
*/
import { fetchSiteProducts, isFetchingSiteProducts } from 'state/site-products';

export function QuerySiteProducts( props ) {
useEffect( () => ! props.isFetchingSiteProducts && props.fetchSiteProducts(), [] );
return null;
}

export default connect(
state => ( { isFetchingSiteProducts: isFetchingSiteProducts( state ) } ),
dispatch => ( { fetchSiteProducts: () => dispatch( fetchSiteProducts() ) } )
)( QuerySiteProducts );
8 changes: 8 additions & 0 deletions _inc/client/lib/plans/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ export const FEATURE_WORDADS_JETPACK = 'wordads-jetpack';
export const FEATURE_GOOGLE_ANALYTICS_JETPACK = 'google-analytics-jetpack';
export const FEATURE_SEARCH_JETPACK = 'search-jetpack';

// Jetpack Search Tiers
export const JETPACK_SEARCH_TIER_UP_TO_100_RECORDS = 'up_to_100_records';
export const JETPACK_SEARCH_TIER_UP_TO_1K_RECORDS = 'up_to_1k_records';
export const JETPACK_SEARCH_TIER_UP_TO_10K_RECORDS = 'up_to_10k_records';
export const JETPACK_SEARCH_TIER_UP_TO_100K_RECORDS = 'up_to_100k_records';
export const JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS = 'up_to_1m_records';
export const JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS = 'more_than_1m_records';

export function isMonthly( plan ) {
return includes( JETPACK_MONTHLY_PLANS, plan );
}
Expand Down
2 changes: 2 additions & 0 deletions _inc/client/plans/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, { Fragment } from 'react';
* Internal dependencies
*/
import QueryProducts from 'components/data/query-products';
import QuerySiteProducts from 'components/data/query-site-products';
import QuerySite from 'components/data/query-site';
import PlanGrid from './plan-grid';
import ProductSelector from './product-selector';
Expand All @@ -16,6 +17,7 @@ export class Plans extends React.Component {
return (
<Fragment>
<QueryProducts />
<QuerySiteProducts />
<QuerySite />
<PlanGrid />
<ProductSelector />
Expand Down
11 changes: 7 additions & 4 deletions _inc/client/plans/product-selector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
import { getSiteRawUrl, getUpgradeUrl, isMultisite } from '../state/initial-state';
import { getProducts, isFetchingProducts } from '../state/products';
import './single-products.scss';
import { isFetchingSiteProducts, getSiteProducts } from '../state/site-products';

class ProductSelector extends Component {
state = {
Expand Down Expand Up @@ -233,8 +234,7 @@ class ProductSelector extends Component {
return (
<SingleProductSearch
isFetching={ this.props.isFetchingData }
products={ this.props.products }
searchUpgradeUrl={ this.props.searchUpgradeUrl }
siteProducts={ this.props.siteProducts }
/>
);
}
Expand All @@ -256,11 +256,14 @@ export default connect( state => {
multisite: isMultisite( state ),
products: getProducts( state ),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in one of the earlier comments you mentioned this component got slower to load (due to the extra API endpoint)...

I think you can actually eliminate this products property from the component (and the isFetchingProducts property below) and just pass siteProducts in to SingleProductBackup instead -- this should work fine since the two have the same structure (and for Jetpack Backup, the exact same data).

In that case, you can probably also avoid calling QueryProducts on this page entirely, which would avoid having to hit that API endpoint at all.

realtimeBackupUpgradeUrl: getUpgradeUrl( state, 'jetpack-backup-realtime' ),
searchUpgradeUrl: getUpgradeUrl( state, 'jetpack-search' ),
sitePlan: getSitePlan( state ),
siteProducts: getSiteProducts( state ),
siteRawlUrl: getSiteRawUrl( state ),
isFetchingData:
isFetchingSiteData( state ) || isFetchingProducts( state ) || ! getAvailablePlans( state ),
isFetchingSiteData( state ) ||
isFetchingProducts( state ) ||
! getAvailablePlans( state ) ||
isFetchingSiteProducts( state ),
backupInfoUrl: getUpgradeUrl( state, 'aag-backups' ), // Redirect to https://jetpack.com/upgrade/backup/
isInstantSearchEnabled: !! get( state, 'jetpack.initialState.isInstantSearchEnabled', false ),
};
Expand Down
2 changes: 1 addition & 1 deletion _inc/client/plans/single-product-backup/body.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ class SingleProductBackupBody extends React.Component {
) ) }
</div>
<ProductSavings
selectedBackup={ selectedBackup }
billingTimeFrame={ billingTimeFrame }
currencyCode={ currencyCode }
potentialSavings={ selectedBackup.potentialSavings }
/>
<UpgradeButton
selectedUpgrade={ selectedBackup }
Expand Down
19 changes: 13 additions & 6 deletions _inc/client/plans/single-product-components/product-savings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,30 @@ import { translate as __ } from 'i18n-calypso';
* Internal dependencies
*/
import PlanPrice from 'components/plans/plan-price';
import './product-savings.scss';

export default function ProductSavings( { selectedBackup, currencyCode } ) {
if ( ! selectedBackup || ! selectedBackup.potentialSavings ) {
export default function ProductSavings( {
billingTimeframe = 'yearly',
potentialSavings,
currencyCode,
} ) {
if ( ! potentialSavings ) {
return null;
}
const savings = (
<PlanPrice
className="single-product-backup__annual-savings"
rawPrice={ selectedBackup.potentialSavings }
className="single-product__annual-savings"
rawPrice={ potentialSavings }
currencyCode={ currencyCode }
inline
/>
);

return (
<p className="single-product-backup__savings">
{ __( 'You are saving {{savings /}} by paying yearly', { components: { savings } } ) }
<p className="single-product__savings">
{ billingTimeframe === 'monthly'
? __( 'You would save {{savings /}} by paying yearly', { components: { savings } } )
: __( 'You are saving {{savings /}} by paying yearly', { components: { savings } } ) }
</p>
);
}
13 changes: 13 additions & 0 deletions _inc/client/plans/single-product-components/product-savings.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@import 'scss/variables/_colors.scss';
@import 'scss/calypso-colors.scss';

.single-product__savings {
font-style: italic;
}

.single-product__annual-savings {
&,
.plan-price__integer {
color: $gray-text-min;
}
}
102 changes: 84 additions & 18 deletions _inc/client/plans/single-product-search/index.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,60 @@
/**
* External dependencies
*/
import React from 'react';
import { translate as __ } from 'i18n-calypso';
import { get, noop } from 'lodash';
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { translate as __, numberFormat } from 'i18n-calypso';
import { get } from 'lodash';

/**
* Internal dependencies
*/
import Button from 'components/button';
import PlanRadioButton from '../single-product-components/plan-radio-button';
import ProductSavings from '../single-product-components/product-savings';
import {
JETPACK_SEARCH_TIER_UP_TO_100_RECORDS,
JETPACK_SEARCH_TIER_UP_TO_1K_RECORDS,
JETPACK_SEARCH_TIER_UP_TO_10K_RECORDS,
JETPACK_SEARCH_TIER_UP_TO_100K_RECORDS,
JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS,
JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS,
} from '../../lib/plans/constants';
import { getUpgradeUrl } from '../../state/initial-state';

const PLACEHOLDER_RECORD_COUNT = 53;
const PLACEHOLDER_PRICE = 50;
const PLACEHOLDER_LABEL = 'Tier 1: Up to 100 records';
function getTierLabel( priceTierSlug, recordCount ) {
switch ( priceTierSlug ) {
case JETPACK_SEARCH_TIER_UP_TO_100_RECORDS:
return __( 'Up to 100 records' );
case JETPACK_SEARCH_TIER_UP_TO_1K_RECORDS:
return __( 'Up to 1,000 records' );
case JETPACK_SEARCH_TIER_UP_TO_10K_RECORDS:
return __( 'Up to 10,000 records' );
case JETPACK_SEARCH_TIER_UP_TO_100K_RECORDS:
return __( 'Up to 100,000 records' );
case JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS:
return __( 'Up to 1,000,000 records' );
case JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS:
const tierMaximumRecords = 1000000 * Math.ceil( recordCount / 1000000 );
return __( 'Up to %(tierMaximumRecords)s records', {
args: { tierMaximumRecords: numberFormat( tierMaximumRecords ) },
} );
default:
return null;
}
}

function handleSelectedTimeframeChangeFactory( setTimeframe ) {
return event => setTimeframe( event.target.value );
}

export default function SingleProductSearchCard( props ) {
const currencyCode = get( props.products, 'jetpack_backup_daily.currency_code', '' );
export function SingleProductSearchCard( props ) {
const [ timeframe, setTimeframe ] = useState( 'yearly' );
const handleSelectedTimeframeChange = handleSelectedTimeframeChangeFactory( setTimeframe );
const currencyCode = get( props.siteProducts, 'jetpack_search.currency_code', '' );
const monthlyPrice = get( props.siteProducts, 'jetpack_search_monthly.cost', '' );
const yearlyPrice = get( props.siteProducts, 'jetpack_search.cost', '' );
const recordCount = get( props.siteProducts, 'jetpack_search.price_tier_usage_quantity', '0' );

return props.isFetching ? (
<div className="plans-section__single-product-skeleton is-placeholder" />
Expand All @@ -36,29 +74,57 @@ export default function SingleProductSearchCard( props ) {
</a>
</p>
<h4 className="single-product-backup__options-header">
{ __( 'Eligible Tier: ' ) }
{ getTierLabel(
get( props.siteProducts, 'jetpack_search.price_tier_slug' ),
recordCount
) }
<br />
{ __(
'Your current site record size: %d record',
'Your current site record size: %d records',
{ args: PLACEHOLDER_RECORD_COUNT, count: PLACEHOLDER_RECORD_COUNT }
'Your current site record size: %s record',
'Your current site record size: %s records',
{ args: recordCount, count: recordCount }
) }
</h4>
<div className="single-product-search__radio-buttons-container">
<PlanRadioButton
billingTimeFrame="monthly"
checked={ timeframe === 'monthly' }
currencyCode={ currencyCode }
fullPrice={ monthlyPrice }
onChange={ handleSelectedTimeframeChange }
planName="Monthly"
radioValue="monthly"
/>
<PlanRadioButton
billingTimeFrame="yearly"
checked={ true }
checked={ timeframe === 'yearly' }
currencyCode={ currencyCode }
fullPrice={ PLACEHOLDER_PRICE }
onChange={ noop }
planName={ PLACEHOLDER_LABEL }
radioValue={ PLACEHOLDER_PRICE }
fullPrice={ yearlyPrice }
onChange={ handleSelectedTimeframeChange }
planName="Annual"
radioValue="yearly"
/>
</div>
<ProductSavings
billingTimeframe={ timeframe }
currencyCode={ currencyCode }
potentialSavings={ 12 * monthlyPrice - yearlyPrice }
/>
<div className="single-product-search__upgrade-button-container">
<Button href={ props.searchUpgradeUrl } primary>
{ __( 'Get Jetpack Search' ) }
<Button
href={ timeframe === 'yearly' ? props.searchUpgradeUrl : props.searchUpgradeMonthlyUrl }
primary
>
{ __( 'Upgrade to Jetpack Search' ) }
</Button>
</div>
</div>
</div>
);
}

export default connect( state => ( {
searchUpgradeUrl: getUpgradeUrl( state, 'jetpack-search' ),
searchUpgradeMonthlyUrl: getUpgradeUrl( state, 'jetpack-search-monthly' ),
} ) )( SingleProductSearchCard );
11 changes: 0 additions & 11 deletions _inc/client/plans/single-products.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,6 @@
}
}

.single-product-backup__savings {
font-style: italic;
}

.single-product-backup__annual-savings {
&,
.plan-price__integer {
color: $gray-text-min;
}
}

.single-product-search__radio-buttons-container,
.single-product-backup__radio-buttons-container {
display: flex;
Expand Down
7 changes: 6 additions & 1 deletion _inc/client/rest-api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function JetpackRestApiClient( root, nonce ) {
},
getParams = {
credentials: 'same-origin',
headers: headers,
headers,
},
postParams = {
method: 'post',
Expand Down Expand Up @@ -219,6 +219,11 @@ function JetpackRestApiClient( root, nonce ) {
.then( parseJsonResponse )
.then( body => JSON.parse( body.data ) ),

fetchSiteProducts: () =>
getRequest( `${ apiRoot }jetpack/v4/site/products`, getParams )
.then( checkStatus )
.then( parseJsonResponse ),

fetchSitePurchases: () =>
getRequest( `${ apiRoot }jetpack/v4/site/purchases`, getParams )
.then( checkStatus )
Expand Down
4 changes: 4 additions & 0 deletions _inc/client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export const JETPACK_PRODUCTS_FETCH = 'JETPACK_PRODUCTS_FETCH';
export const JETPACK_PRODUCTS_FETCH_RECEIVE = 'JETPACK_PRODUCTS_FETCH_RECEIVE';
export const JETPACK_PRODUCTS_FETCH_FAIL = 'JETPACK_PRODUCTS_FETCH_FAIL';

export const JETPACK_SITE_PRODUCTS_FETCH = 'JETPACK_SITE_PRODUCTS_FETCH';
export const JETPACK_SITE_PRODUCTS_FETCH_FAIL = 'JETPACK_SITE_PRODUCTS_FETCH_FAIL';
export const JETPACK_SITE_PRODUCTS_FETCH_RECEIVE = 'JETPACK_SITE_PRODUCTS_FETCH_RECEIVE';

export const JETPACK_SETTINGS_FETCH = 'JETPACK_SETTINGS_FETCH';
export const JETPACK_SETTINGS_FETCH_RECEIVE = 'JETPACK_SETTINGS_FETCH_RECEIVE';
export const JETPACK_SETTINGS_FETCH_FAIL = 'JETPACK_SETTINGS_FETCH_FAIL';
Expand Down
2 changes: 2 additions & 0 deletions _inc/client/state/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { reducer as rewind } from 'state/rewind/reducer';
import { reducer as search } from 'state/search/reducer';
import { reducer as settings } from 'state/settings/reducer';
import { reducer as siteData } from 'state/site/reducer';
import { reducer as siteProducts } from 'state/site-products/reducer';
import { reducer as siteVerify } from 'state/site-verify/reducer';
import { reducer as disconnectSurvey } from 'state/disconnect-survey/reducer';
import { reducer as trackingSettings } from 'state/tracking/reducer';
Expand All @@ -42,6 +43,7 @@ const jetpackReducer = combineReducers( {
search,
settings,
siteData,
siteProducts,
siteVerify,
disconnectSurvey,
trackingSettings,
Expand Down
31 changes: 31 additions & 0 deletions _inc/client/state/site-products/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Internal dependencies
*/
import {
JETPACK_SITE_PRODUCTS_FETCH,
JETPACK_SITE_PRODUCTS_FETCH_FAIL,
JETPACK_SITE_PRODUCTS_FETCH_RECEIVE,
} from 'state/action-types';
import restApi from 'rest-api';

export const fetchSiteProducts = () => {
return dispatch => {
dispatch( {
type: JETPACK_SITE_PRODUCTS_FETCH,
} );
return restApi
.fetchSiteProducts()
.then( response => {
dispatch( {
type: JETPACK_SITE_PRODUCTS_FETCH_RECEIVE,
siteProducts: response.data,
} );
} )
.catch( error => {
dispatch( {
type: JETPACK_SITE_PRODUCTS_FETCH_FAIL,
error,
} );
} );
};
};
2 changes: 2 additions & 0 deletions _inc/client/state/site-products/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './reducer';
export * from './actions';
Loading