From f87eb599ba643b940001d09c5f3313cabccf80cf Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Tue, 24 Mar 2020 17:24:48 -0600 Subject: [PATCH 1/6] Add handling for pricing and tier APIs --- .../data/query-site-products/index.js | 20 ++++++ _inc/client/lib/plans/constants.js | 8 +++ _inc/client/plans/index.jsx | 2 + _inc/client/plans/product-selector.jsx | 11 +++- .../plans/single-product-search/index.jsx | 59 +++++++++++++---- _inc/client/rest-api/index.js | 7 +- _inc/client/state/action-types.js | 4 ++ _inc/client/state/reducer.js | 2 + _inc/client/state/site-products/actions.js | 31 +++++++++ _inc/client/state/site-products/index.js | 2 + _inc/client/state/site-products/reducer.js | 58 +++++++++++++++++ _inc/lib/class.core-rest-api-endpoints.php | 10 +++ .../class.jetpack-core-api-site-endpoints.php | 65 ++++++++++++------- 13 files changed, 241 insertions(+), 38 deletions(-) create mode 100644 _inc/client/components/data/query-site-products/index.js create mode 100644 _inc/client/state/site-products/actions.js create mode 100644 _inc/client/state/site-products/index.js create mode 100644 _inc/client/state/site-products/reducer.js diff --git a/_inc/client/components/data/query-site-products/index.js b/_inc/client/components/data/query-site-products/index.js new file mode 100644 index 0000000000000..bc13c6bdb92a6 --- /dev/null +++ b/_inc/client/components/data/query-site-products/index.js @@ -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 ); diff --git a/_inc/client/lib/plans/constants.js b/_inc/client/lib/plans/constants.js index 71043ddd9011b..f4a1ea6dfc0de 100644 --- a/_inc/client/lib/plans/constants.js +++ b/_inc/client/lib/plans/constants.js @@ -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 ); } diff --git a/_inc/client/plans/index.jsx b/_inc/client/plans/index.jsx index 55e38ee694e6b..d413f91295718 100644 --- a/_inc/client/plans/index.jsx +++ b/_inc/client/plans/index.jsx @@ -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'; @@ -16,6 +17,7 @@ export class Plans extends React.Component { return ( + diff --git a/_inc/client/plans/product-selector.jsx b/_inc/client/plans/product-selector.jsx index f71b72baaafaa..af5689809de87 100644 --- a/_inc/client/plans/product-selector.jsx +++ b/_inc/client/plans/product-selector.jsx @@ -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 = { @@ -232,8 +233,9 @@ class ProductSelector extends Component { renderSearchProduct() { return ( ); @@ -250,6 +252,8 @@ class ProductSelector extends Component { } export default connect( state => { + const isFetchingData = + isFetchingSiteData( state ) || isFetchingProducts( state ) || ! getAvailablePlans( state ); return { activeSitePurchases: getActiveSitePurchases( state ), dailyBackupUpgradeUrl: getUpgradeUrl( state, 'jetpack-backup-daily' ), @@ -258,9 +262,10 @@ export default connect( state => { 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 ), + isFetchingData, + isFetchingDataForSearch: isFetchingData || isFetchingSiteProducts( state ), backupInfoUrl: getUpgradeUrl( state, 'aag-backups' ), // Redirect to https://jetpack.com/upgrade/backup/ isInstantSearchEnabled: !! get( state, 'jetpack.initialState.isInstantSearchEnabled', false ), }; diff --git a/_inc/client/plans/single-product-search/index.jsx b/_inc/client/plans/single-product-search/index.jsx index ef809bf1deeb6..fb14c54b1df40 100644 --- a/_inc/client/plans/single-product-search/index.jsx +++ b/_inc/client/plans/single-product-search/index.jsx @@ -2,7 +2,7 @@ * External dependencies */ import React from 'react'; -import { translate as __ } from 'i18n-calypso'; +import { translate as __, numberFormat } from 'i18n-calypso'; import { get, noop } from 'lodash'; /** @@ -10,13 +10,45 @@ import { get, noop } from 'lodash'; */ import Button from 'components/button'; import PlanRadioButton from '../single-product-components/plan-radio-button'; +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'; -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 __( 'Tier 1: Up to 100 records' ); + case JETPACK_SEARCH_TIER_UP_TO_1K_RECORDS: + return __( 'Tier 2: Up to 1,000 records' ); + case JETPACK_SEARCH_TIER_UP_TO_10K_RECORDS: + return __( 'Tier 3: Up to 10,000 records' ); + case JETPACK_SEARCH_TIER_UP_TO_100K_RECORDS: + return __( 'Tier 4: Up to 100,000 records' ); + case JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS: + return __( 'Tier 5: Up to 1,000,000 records' ); + case JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS: + // NOTE: 5 === number of defined tiers + const tierNumber = 5 + Math.floor( recordCount / 1000000 ); + const tierMaximumRecords = 1000000 * Math.ceil( recordCount / 1000000 ); + return __( 'Tier %(tierNumber)d: Up to %(tierMaximumRecords)s records', { + args: { tierNumber, tierMaximumRecords: numberFormat( tierMaximumRecords ) }, + } ); + default: + return null; + } +} + +const BILLING_TIMEFRAME = 'yearly'; export default function SingleProductSearchCard( props ) { - const currencyCode = get( props.products, 'jetpack_backup_daily.currency_code', '' ); + const currencyCode = get( props.products, 'jetpack_search.currency_code', '' ); + const yearlyPrice = get( props.products, 'jetpack_search.cost', '' ); + const recordCount = get( props.siteProducts, 'jetpack_search.price_tier_usage_quantity', '0' ); return props.isFetching ? (
@@ -37,20 +69,23 @@ export default function SingleProductSearchCard( props ) {

{ __( - '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 } ) }

diff --git a/_inc/client/rest-api/index.js b/_inc/client/rest-api/index.js index 0b12ab9b8cb05..4b3125f369bf4 100644 --- a/_inc/client/rest-api/index.js +++ b/_inc/client/rest-api/index.js @@ -31,7 +31,7 @@ function JetpackRestApiClient( root, nonce ) { }, getParams = { credentials: 'same-origin', - headers: headers, + headers, }, postParams = { method: 'post', @@ -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 ) diff --git a/_inc/client/state/action-types.js b/_inc/client/state/action-types.js index 307a8d0f3e1a6..46e7ebb712870 100644 --- a/_inc/client/state/action-types.js +++ b/_inc/client/state/action-types.js @@ -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'; diff --git a/_inc/client/state/reducer.js b/_inc/client/state/reducer.js index 1005746b1e3a4..35c0efdfc00a4 100644 --- a/_inc/client/state/reducer.js +++ b/_inc/client/state/reducer.js @@ -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'; @@ -42,6 +43,7 @@ const jetpackReducer = combineReducers( { search, settings, siteData, + siteProducts, siteVerify, disconnectSurvey, trackingSettings, diff --git a/_inc/client/state/site-products/actions.js b/_inc/client/state/site-products/actions.js new file mode 100644 index 0000000000000..6880673c373bb --- /dev/null +++ b/_inc/client/state/site-products/actions.js @@ -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, + } ); + } ); + }; +}; diff --git a/_inc/client/state/site-products/index.js b/_inc/client/state/site-products/index.js new file mode 100644 index 0000000000000..5e3164b4c9f72 --- /dev/null +++ b/_inc/client/state/site-products/index.js @@ -0,0 +1,2 @@ +export * from './reducer'; +export * from './actions'; diff --git a/_inc/client/state/site-products/reducer.js b/_inc/client/state/site-products/reducer.js new file mode 100644 index 0000000000000..174be6cfb7f2a --- /dev/null +++ b/_inc/client/state/site-products/reducer.js @@ -0,0 +1,58 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import { + JETPACK_SITE_PRODUCTS_FETCH, + JETPACK_SITE_PRODUCTS_FETCH_FAIL, + JETPACK_SITE_PRODUCTS_FETCH_RECEIVE, +} from 'state/action-types'; + +export const items = ( state = {}, action ) => { + switch ( action.type ) { + case JETPACK_SITE_PRODUCTS_FETCH_RECEIVE: + return action.siteProducts; + default: + return state; + } +}; + +export const requests = ( state = {}, action ) => { + switch ( action.type ) { + case JETPACK_SITE_PRODUCTS_FETCH: + return { ...state, isFetching: true }; + case JETPACK_SITE_PRODUCTS_FETCH_RECEIVE: + case JETPACK_SITE_PRODUCTS_FETCH_FAIL: + return { ...state, isFetching: false }; + default: + return state; + } +}; + +export const reducer = combineReducers( { + items, + requests, +} ); + +/** + * Returns true if currently requesting site products. Otherwise false. + * + * @param {Object} state Global state tree + * @return {Boolean} Whether site products are being requested + */ +export function isFetchingSiteProducts( state ) { + return !! state.jetpack.siteProducts.requests.isFetching; +} + +/** + * Returns WP.com site products that are relevant to Jetpack. + * @param {Object} state Global state tree + * @return {Object} Site products + */ +export function getSiteProducts( state ) { + return state.jetpack.siteProducts.items; +} diff --git a/_inc/lib/class.core-rest-api-endpoints.php b/_inc/lib/class.core-rest-api-endpoints.php index 2d655695727a9..62323dfa90597 100644 --- a/_inc/lib/class.core-rest-api-endpoints.php +++ b/_inc/lib/class.core-rest-api-endpoints.php @@ -212,6 +212,16 @@ public static function register_endpoints() { 'permission_callback' => array( $site_endpoint , 'can_request' ), ) ); + register_rest_route( + 'jetpack/v4', + '/site/products', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $site_endpoint, 'get_products' ), + 'permission_callback' => array( $site_endpoint, 'can_request' ), + ) + ); + // Get current site purchases. register_rest_route( 'jetpack/v4', diff --git a/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php b/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php index c321f6ae37594..365692102d86e 100644 --- a/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php +++ b/_inc/lib/core-api/class.jetpack-core-api-site-endpoints.php @@ -11,7 +11,18 @@ * This is the endpoint class for `/site` endpoints. */ class Jetpack_Core_API_Site_Endpoint { - + /** + * Returns commonly used WP_Error indicating failure to fetch data + * + * @return WP_Error that denotes our inability to fetch the requested data + */ + private static function get_failed_fetch_error() { + return new WP_Error( + 'failed_to_fetch_data', + esc_html__( 'Unable to fetch the requested data.', 'jetpack' ), + array( 'status' => 500 ) + ); + } /** * Returns the result of `/sites/%s/features` endpoint call. @@ -22,18 +33,13 @@ class Jetpack_Core_API_Site_Endpoint { * of plan slugs that enable these features */ public static function get_features() { - // Make the API request. $request = sprintf( '/sites/%d/features', Jetpack_Options::get_option( 'id' ) ); $response = Client::wpcom_json_api_request_as_blog( $request, '1.1' ); // Bail if there was an error or malformed response. if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { - return new WP_Error( - 'failed_to_fetch_data', - esc_html__( 'Unable to fetch the requested data.', 'jetpack' ), - array( 'status' => 500 ) - ); + return self::get_failed_fetch_error(); } // Decode the results. @@ -41,11 +47,7 @@ public static function get_features() { // Bail if there were no results or plan details returned. if ( ! is_array( $results ) ) { - return new WP_Error( - 'failed_to_fetch_data', - esc_html__( 'Unable to fetch the requested data.', 'jetpack' ), - array( 'status' => 500 ) - ); + return self::get_failed_fetch_error(); } return rest_ensure_response( @@ -70,11 +72,7 @@ public static function get_purchases() { // Bail if there was an error or malformed response. if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { - return new WP_Error( - 'failed_to_fetch_data', - esc_html__( 'Unable to fetch the requested data.', 'jetpack' ), - array( 'status' => 500 ) - ); + return self::get_failed_fetch_error(); } // Decode the results. @@ -82,11 +80,7 @@ public static function get_purchases() { // Bail if there were no results or purchase details returned. if ( ! is_array( $results ) ) { - return new WP_Error( - 'failed_to_fetch_data', - esc_html__( 'Unable to fetch the requested data.', 'jetpack' ), - array( 'status' => 500 ) - ); + return self::get_failed_fetch_error(); } return rest_ensure_response( @@ -98,6 +92,33 @@ public static function get_purchases() { ); } + /** + * Returns the result of `/sites/%d/products` endpoint call. + * + * @return array of site products. + */ + public static function get_products() { + $url = sprintf( '/sites/%d/products?locale=%s&type=jetpack', Jetpack_Options::get_option( 'id' ), get_user_locale() ); + $response = Client::wpcom_json_api_request_as_blog( $url, '1.1' ); + + if ( is_wp_error( $response ) || ! is_array( $response ) || ! isset( $response['body'] ) ) { + return self::get_failed_fetch_error(); + } + + $results = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( ! is_array( $results ) ) { + return self::get_failed_fetch_error(); + } + + return rest_ensure_response( + array( + 'code' => 'success', + 'message' => esc_html__( 'Site products correctly received.', 'jetpack' ), + 'data' => $results, + ) + ); + } + /** * Check that the current user has permissions to request information about this site. * From 33e1b3b11ecdabd4c1a64850748c009e76b64779 Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Wed, 25 Mar 2020 17:34:29 -0600 Subject: [PATCH 2/6] Remove tier numbers --- .../client/plans/single-product-search/index.jsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/_inc/client/plans/single-product-search/index.jsx b/_inc/client/plans/single-product-search/index.jsx index fb14c54b1df40..c59735123cba9 100644 --- a/_inc/client/plans/single-product-search/index.jsx +++ b/_inc/client/plans/single-product-search/index.jsx @@ -22,21 +22,19 @@ import { function getTierLabel( priceTierSlug, recordCount ) { switch ( priceTierSlug ) { case JETPACK_SEARCH_TIER_UP_TO_100_RECORDS: - return __( 'Tier 1: Up to 100 records' ); + return __( 'Tier: Up to 100 records' ); case JETPACK_SEARCH_TIER_UP_TO_1K_RECORDS: - return __( 'Tier 2: Up to 1,000 records' ); + return __( 'Tier: Up to 1,000 records' ); case JETPACK_SEARCH_TIER_UP_TO_10K_RECORDS: - return __( 'Tier 3: Up to 10,000 records' ); + return __( 'Tier: Up to 10,000 records' ); case JETPACK_SEARCH_TIER_UP_TO_100K_RECORDS: - return __( 'Tier 4: Up to 100,000 records' ); + return __( 'Tier: Up to 100,000 records' ); case JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS: - return __( 'Tier 5: Up to 1,000,000 records' ); + return __( 'Tier: Up to 1,000,000 records' ); case JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS: - // NOTE: 5 === number of defined tiers - const tierNumber = 5 + Math.floor( recordCount / 1000000 ); const tierMaximumRecords = 1000000 * Math.ceil( recordCount / 1000000 ); - return __( 'Tier %(tierNumber)d: Up to %(tierMaximumRecords)s records', { - args: { tierNumber, tierMaximumRecords: numberFormat( tierMaximumRecords ) }, + return __( 'Tier: Up to %(tierMaximumRecords)s records', { + args: { tierMaximumRecords: numberFormat( tierMaximumRecords ) }, } ); default: return null; From 78ec1b883fc89b4241e01afd5419e239a0748f0a Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Wed, 25 Mar 2020 17:51:06 -0600 Subject: [PATCH 3/6] Add monthly and yearly pricing into search plan --- _inc/client/plans/product-selector.jsx | 2 - .../plans/single-product-search/index.jsx | 52 ++++++++++++++----- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/_inc/client/plans/product-selector.jsx b/_inc/client/plans/product-selector.jsx index af5689809de87..07a1fded0c60c 100644 --- a/_inc/client/plans/product-selector.jsx +++ b/_inc/client/plans/product-selector.jsx @@ -236,7 +236,6 @@ class ProductSelector extends Component { isFetching={ this.props.isFetchingDataForSearch } products={ this.props.products } siteProducts={ this.props.siteProducts } - searchUpgradeUrl={ this.props.searchUpgradeUrl } /> ); } @@ -260,7 +259,6 @@ export default connect( state => { multisite: isMultisite( state ), products: getProducts( state ), realtimeBackupUpgradeUrl: getUpgradeUrl( state, 'jetpack-backup-realtime' ), - searchUpgradeUrl: getUpgradeUrl( state, 'jetpack-search' ), sitePlan: getSitePlan( state ), siteProducts: getSiteProducts( state ), siteRawlUrl: getSiteRawUrl( state ), diff --git a/_inc/client/plans/single-product-search/index.jsx b/_inc/client/plans/single-product-search/index.jsx index c59735123cba9..966cf4618dde8 100644 --- a/_inc/client/plans/single-product-search/index.jsx +++ b/_inc/client/plans/single-product-search/index.jsx @@ -1,9 +1,10 @@ /** * External dependencies */ -import React from 'react'; +import React, { useState } from 'react'; +import { connect } from 'react-redux'; import { translate as __, numberFormat } from 'i18n-calypso'; -import { get, noop } from 'lodash'; +import { get } from 'lodash'; /** * Internal dependencies @@ -18,6 +19,7 @@ import { JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS, JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS, } from '../../lib/plans/constants'; +import { getUpgradeUrl } from '../../state/initial-state'; function getTierLabel( priceTierSlug, recordCount ) { switch ( priceTierSlug ) { @@ -41,10 +43,15 @@ function getTierLabel( priceTierSlug, recordCount ) { } } -const BILLING_TIMEFRAME = 'yearly'; +function handleSelectedTimeframeChangeFactory( setTimeframe ) { + return event => setTimeframe( event.target.value ); +} -export default function SingleProductSearchCard( props ) { +export function SingleProductSearchCard( props ) { + const [ timeframe, setTimeframe ] = useState( 'yearly' ); + const handleSelectedTimeframeChange = handleSelectedTimeframeChangeFactory( setTimeframe ); const currencyCode = get( props.products, 'jetpack_search.currency_code', '' ); + const monthlyPrice = get( props.products, 'jetpack_search_monthly.cost', '' ); const yearlyPrice = get( props.products, 'jetpack_search.cost', '' ); const recordCount = get( props.siteProducts, 'jetpack_search.price_tier_usage_quantity', '0' ); @@ -71,23 +78,37 @@ export default function SingleProductSearchCard( props ) { 'Your current site record size: %s records', { args: recordCount, count: recordCount } ) } +
+ { getTierLabel( + get( props.siteProducts, 'jetpack_search.price_tier_slug' ), + recordCount + ) }
+
-
@@ -95,3 +116,8 @@ export default function SingleProductSearchCard( props ) {
); } + +export default connect( state => ( { + searchUpgradeUrl: getUpgradeUrl( state, 'jetpack-search' ), + searchUpgradeMonthlyUrl: getUpgradeUrl( state, 'jetpack-search-monthly' ), +} ) )( SingleProductSearchCard ); From 63a1e0a15e394e3764d4fa0bfa511194d6dec6f5 Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Wed, 25 Mar 2020 18:10:23 -0600 Subject: [PATCH 4/6] Add support for showing ProductSavings component --- .../plans/single-product-backup/body.jsx | 2 +- .../product-savings.jsx | 19 ++++++++---- .../product-savings.scss | 13 ++++++++ .../plans/single-product-search/index.jsx | 31 ++++++++++++------- _inc/client/plans/single-products.scss | 11 ------- 5 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 _inc/client/plans/single-product-components/product-savings.scss diff --git a/_inc/client/plans/single-product-backup/body.jsx b/_inc/client/plans/single-product-backup/body.jsx index 1767529199840..ebd5500d1c0f5 100644 --- a/_inc/client/plans/single-product-backup/body.jsx +++ b/_inc/client/plans/single-product-backup/body.jsx @@ -65,9 +65,9 @@ class SingleProductBackupBody extends React.Component { ) ) }
); return ( -

- { __( 'You are saving {{savings /}} by paying yearly', { components: { savings } } ) } +

+ { billingTimeframe === 'monthly' + ? __( 'You would save {{savings /}} by paying yearly', { components: { savings } } ) + : __( 'You are saving {{savings /}} by paying yearly', { components: { savings } } ) }

); } diff --git a/_inc/client/plans/single-product-components/product-savings.scss b/_inc/client/plans/single-product-components/product-savings.scss new file mode 100644 index 0000000000000..59970c416992a --- /dev/null +++ b/_inc/client/plans/single-product-components/product-savings.scss @@ -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; + } +} diff --git a/_inc/client/plans/single-product-search/index.jsx b/_inc/client/plans/single-product-search/index.jsx index 966cf4618dde8..441d27ac86709 100644 --- a/_inc/client/plans/single-product-search/index.jsx +++ b/_inc/client/plans/single-product-search/index.jsx @@ -11,6 +11,7 @@ import { get } from 'lodash'; */ 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, @@ -24,18 +25,18 @@ import { getUpgradeUrl } from '../../state/initial-state'; function getTierLabel( priceTierSlug, recordCount ) { switch ( priceTierSlug ) { case JETPACK_SEARCH_TIER_UP_TO_100_RECORDS: - return __( 'Tier: Up to 100 records' ); + return __( 'Up to 100 records' ); case JETPACK_SEARCH_TIER_UP_TO_1K_RECORDS: - return __( 'Tier: Up to 1,000 records' ); + return __( 'Up to 1,000 records' ); case JETPACK_SEARCH_TIER_UP_TO_10K_RECORDS: - return __( 'Tier: Up to 10,000 records' ); + return __( 'Up to 10,000 records' ); case JETPACK_SEARCH_TIER_UP_TO_100K_RECORDS: - return __( 'Tier: Up to 100,000 records' ); + return __( 'Up to 100,000 records' ); case JETPACK_SEARCH_TIER_UP_TO_1M_RECORDS: - return __( 'Tier: Up to 1,000,000 records' ); + return __( 'Up to 1,000,000 records' ); case JETPACK_SEARCH_TIER_MORE_THAN_1M_RECORDS: const tierMaximumRecords = 1000000 * Math.ceil( recordCount / 1000000 ); - return __( 'Tier: Up to %(tierMaximumRecords)s records', { + return __( 'Up to %(tierMaximumRecords)s records', { args: { tierMaximumRecords: numberFormat( tierMaximumRecords ) }, } ); default: @@ -73,16 +74,17 @@ export function SingleProductSearchCard( props ) {

+ { __( 'Eligible Tier: ' ) } + { getTierLabel( + get( props.siteProducts, 'jetpack_search.price_tier_slug' ), + recordCount + ) } +
{ __( 'Your current site record size: %s record', 'Your current site record size: %s records', { args: recordCount, count: recordCount } ) } -
- { getTierLabel( - get( props.siteProducts, 'jetpack_search.price_tier_slug' ), - recordCount - ) }

+
diff --git a/_inc/client/plans/single-products.scss b/_inc/client/plans/single-products.scss index 6ea9906666bfc..ed0caefe28acc 100644 --- a/_inc/client/plans/single-products.scss +++ b/_inc/client/plans/single-products.scss @@ -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; From 68465881ac22742074e7cd5fe8d591c5a5f7b321 Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Thu, 26 Mar 2020 15:33:16 -0600 Subject: [PATCH 5/6] Use site products for pricing and currency code --- _inc/client/plans/product-selector.jsx | 1 - _inc/client/plans/single-product-search/index.jsx | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/_inc/client/plans/product-selector.jsx b/_inc/client/plans/product-selector.jsx index 07a1fded0c60c..0319ffdda0432 100644 --- a/_inc/client/plans/product-selector.jsx +++ b/_inc/client/plans/product-selector.jsx @@ -234,7 +234,6 @@ class ProductSelector extends Component { return ( ); diff --git a/_inc/client/plans/single-product-search/index.jsx b/_inc/client/plans/single-product-search/index.jsx index 441d27ac86709..2f2fd906dc19d 100644 --- a/_inc/client/plans/single-product-search/index.jsx +++ b/_inc/client/plans/single-product-search/index.jsx @@ -51,9 +51,9 @@ function handleSelectedTimeframeChangeFactory( setTimeframe ) { export function SingleProductSearchCard( props ) { const [ timeframe, setTimeframe ] = useState( 'yearly' ); const handleSelectedTimeframeChange = handleSelectedTimeframeChangeFactory( setTimeframe ); - const currencyCode = get( props.products, 'jetpack_search.currency_code', '' ); - const monthlyPrice = get( props.products, 'jetpack_search_monthly.cost', '' ); - const yearlyPrice = get( props.products, 'jetpack_search.cost', '' ); + 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 ? ( From c863e16683259f9824f09c6da0804d471f0a31a8 Mon Sep 17 00:00:00 2001 From: Jason Moon Date: Thu, 26 Mar 2020 15:33:57 -0600 Subject: [PATCH 6/6] Use a unified loading state for single feature products --- _inc/client/plans/product-selector.jsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/_inc/client/plans/product-selector.jsx b/_inc/client/plans/product-selector.jsx index 0319ffdda0432..260000f1c5c94 100644 --- a/_inc/client/plans/product-selector.jsx +++ b/_inc/client/plans/product-selector.jsx @@ -233,7 +233,7 @@ class ProductSelector extends Component { renderSearchProduct() { return ( ); @@ -250,8 +250,6 @@ class ProductSelector extends Component { } export default connect( state => { - const isFetchingData = - isFetchingSiteData( state ) || isFetchingProducts( state ) || ! getAvailablePlans( state ); return { activeSitePurchases: getActiveSitePurchases( state ), dailyBackupUpgradeUrl: getUpgradeUrl( state, 'jetpack-backup-daily' ), @@ -261,8 +259,11 @@ export default connect( state => { sitePlan: getSitePlan( state ), siteProducts: getSiteProducts( state ), siteRawlUrl: getSiteRawUrl( state ), - isFetchingData, - isFetchingDataForSearch: isFetchingData || isFetchingSiteProducts( state ), + isFetchingData: + 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 ), };