Skip to content

Commit

Permalink
Better handling on My Jetpack product interstitials if site already h…
Browse files Browse the repository at this point in the history
…as prodcut (#36570)

* Update product interstitials to use paid plan check, better handling on table interstitial if site already has a plan

* changelog

* version management

* More version fixing
  • Loading branch information
jboland88 committed Mar 27, 2024
1 parent 4cb56e2 commit 8d7171a
Show file tree
Hide file tree
Showing 27 changed files with 152 additions and 63 deletions.
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Expand Up @@ -102,7 +102,7 @@ const ProductDetailCard = ( {
pricingForUi,
isBundle,
supportedProducts,
hasRequiredPlan,
hasPaidPlanForProduct,
status,
pluginSlug,
postCheckoutUrl,
Expand Down Expand Up @@ -132,7 +132,7 @@ const ProductDetailCard = ( {
* Or when:
* - it's a quantity-based product
*/
const needsPurchase = ( ! isFree && ! hasRequiredPlan ) || quantity != null;
const needsPurchase = ( ! isFree && ! hasPaidPlanForProduct ) || quantity != null;

// Redirect to the referrer URL when the `redirect_to_referrer` query param is present.
const referrerURL = useRedirectToReferrer();
Expand Down Expand Up @@ -261,12 +261,13 @@ const ProductDetailCard = ( {
);
}

const hasTrialButton = ( ! isBundle || ( isBundle && ! hasRequiredPlan ) ) && trialAvailable;
const hasTrialButton =
( ! isBundle || ( isBundle && ! hasPaidPlanForProduct ) ) && trialAvailable;

// If we prefer the product name, use that everywhere instead of the title
const productMoniker = name && preferProductName ? name : title;
const defaultCtaLabel =
! isBundle && hasRequiredPlan
! isBundle && hasPaidPlanForProduct
? sprintf(
/* translators: placeholder is product name. */
__( 'Install %s', 'jetpack-my-jetpack' ),
Expand Down Expand Up @@ -361,7 +362,7 @@ const ProductDetailCard = ( {
</div>
) }

{ ( ! isBundle || ( isBundle && ! hasRequiredPlan ) ) && (
{ ( ! isBundle || ( isBundle && ! hasPaidPlanForProduct ) ) && (
<Text
component={ ProductDetailButton }
onClick={ clickHandler }
Expand All @@ -375,7 +376,7 @@ const ProductDetailCard = ( {
</Text>
) }

{ ! isBundle && trialAvailable && ! hasRequiredPlan && (
{ ! isBundle && trialAvailable && ! hasPaidPlanForProduct && (
<Text
component={ ProductDetailButton }
onClick={ trialClickHandler }
Expand Down Expand Up @@ -415,7 +416,7 @@ const ProductDetailCard = ( {
</div>
) }

{ isBundle && hasRequiredPlan && (
{ isBundle && hasPaidPlanForProduct && (
<div className={ styles[ 'product-has-required-plan' ] }>
<CheckmarkIcon size={ 36 } />
<Text>{ __( 'Active on your site', 'jetpack-my-jetpack' ) }</Text>
Expand Down
Expand Up @@ -28,6 +28,7 @@ import { useRedirectToReferrer } from '../../hooks/use-redirect-to-referrer';
* @param {boolean} props.isFetching - True if there is a pending request to load the product.
* @param {string} props.tier - Product tier slug, i.e. 'free' or 'upgraded'.
* @param {Function} props.trackProductButtonClick - Tracks click event for the product button.
* @param {boolean} props.preferProductName - Whether to show the product name instead of the title.
* @returns {object} - ProductDetailTableColumn component.
*/
const ProductDetailTableColumn = ( {
Expand All @@ -37,15 +38,19 @@ const ProductDetailTableColumn = ( {
isFetching,
tier,
trackProductButtonClick,
preferProductName,
} ) => {
const { siteSuffix = '', myJetpackCheckoutUri = '' } = getMyJetpackWindowInitialState();

// Extract the product details.
const {
name,
featuresByTier = [],
pricingForUi: { tiers: tiersPricingForUi },
title,
postCheckoutUrl,
isBundle,
hasPaidPlanForProduct,
} = detail;

// Extract the pricing details for the provided tier.
Expand Down Expand Up @@ -120,31 +125,39 @@ const ProductDetailTableColumn = ( {
/* dummy arg to avoid bad minification */ 0
);

const callToAction =
customCallToAction ||
( isFree
? __( 'Start for Free', 'jetpack-my-jetpack' )
const productMoniker = name && preferProductName ? name : title;
const defaultCtaLabel =
! isBundle && hasPaidPlanForProduct
? sprintf(
/* translators: placeholder is product name. */
__( 'Install %s', 'jetpack-my-jetpack' ),
productMoniker
)
: sprintf(
/* translators: placeholder is product name. */
__( 'Get %s', 'jetpack-my-jetpack' ),
title,
/* dummy arg to avoid bad minification */ 0
) );
productMoniker
);
const callToAction =
customCallToAction ||
( isFree ? __( 'Start for Free', 'jetpack-my-jetpack' ) : defaultCtaLabel );

return (
<PricingTableColumn primary={ ! isFree }>
<PricingTableHeader>
{ isFree ? (
<ProductPrice price={ 0 } legend={ '' } currency={ 'USD' } hidePriceFraction />
) : (
<ProductPrice
price={ price }
offPrice={ offPrice }
legend={ priceDescription }
currency={ currencyCode }
hideDiscountLabel={ isOneMonthOffer }
hidePriceFraction
/>
! hasPaidPlanForProduct && (
<ProductPrice
price={ price }
offPrice={ offPrice }
legend={ priceDescription }
currency={ currencyCode }
hideDiscountLabel={ isOneMonthOffer }
hidePriceFraction
/>
)
) }
<Button
fullWidth
Expand Down Expand Up @@ -200,6 +213,7 @@ ProductDetailTableColumn.propTypes = {
detail: PropTypes.object.isRequired,
tier: PropTypes.string.isRequired,
trackProductButtonClick: PropTypes.func.isRequired,
preferProductName: PropTypes.bool.isRequired,
};

/**
Expand All @@ -212,18 +226,29 @@ ProductDetailTableColumn.propTypes = {
* @param {Function} props.onProductButtonClick - Click handler for the product button.
* @param {Function} props.trackProductButtonClick - Tracks click event for the product button.
* @param {boolean} props.isFetching - True if there is a pending request to load the product.
* @param {boolean} props.preferProductName - Whether to show the product name instead of the title.
* @returns {object} - ProductDetailTable react component.
*/
const ProductDetailTable = ( {
slug,
onProductButtonClick,
trackProductButtonClick,
isFetching,
preferProductName,
} ) => {
const { fileSystemWriteAccess = 'no' } = getMyJetpackWindowInitialState();

const { detail } = useProduct( slug );
const { description, featuresByTier = [], pluginSlug, status, tiers = [], title } = detail;
const {
description,
featuresByTier = [],
pluginSlug,
status,
tiers = [],
hasPaidPlanForProduct,
title,
pricingForUi: { tiers: tiersPricingForUi },
} = detail;

// If the plugin can not be installed automatically, the user will have to take extra steps.
const cantInstallPlugin = 'plugin_absent' === status && 'no' === fileSystemWriteAccess;
Expand Down Expand Up @@ -269,23 +294,36 @@ const ProductDetailTable = ( {
[ featuresByTier ]
);

const tierIsFree = tier => {
const { isFree } = tiersPricingForUi[ tier ];
return isFree;
};

return (
<>
{ cantInstallPluginNotice }

<PricingTable title={ description } items={ pricingTableItems }>
{ tiers.map( ( tier, index ) => (
<ProductDetailTableColumn
key={ index }
tier={ tier }
detail={ detail }
isFetching={ isFetching }
onProductButtonClick={ onProductButtonClick }
trackProductButtonClick={ trackProductButtonClick }
primary={ index === 0 }
cantInstallPlugin={ cantInstallPlugin }
/>
) ) }
{ tiers.map( ( tier, index ) => {
// Don't show the column if this is a free offering and we already have a plan
if ( hasPaidPlanForProduct && tierIsFree( tier ) ) {
return null;
}

return (
<ProductDetailTableColumn
key={ index }
tier={ tier }
detail={ detail }
isFetching={ isFetching }
onProductButtonClick={ onProductButtonClick }
trackProductButtonClick={ trackProductButtonClick }
primary={ index === 0 }
cantInstallPlugin={ cantInstallPlugin }
preferProductName={ preferProductName }
/>
);
} ) }
</PricingTable>
</>
);
Expand All @@ -296,6 +334,7 @@ ProductDetailTable.propTypes = {
onProductButtonClick: PropTypes.func.isRequired,
trackProductButtonClick: PropTypes.func.isRequired,
isFetching: PropTypes.bool.isRequired,
preferProductName: PropTypes.bool.isRequired,
};

export default ProductDetailTable;
Expand Up @@ -140,18 +140,17 @@ export default function ProductInterstitial( {
postCheckoutUrl = activatedProduct?.post_checkout_url
? activatedProduct.post_checkout_url
: myJetpackCheckoutUri;
const hasRequiredPlan = tier
? product?.hasRequiredTier?.[ tier ]
: product?.hasRequiredPlan;
// there is a separate hasRequiredTier, but it is not implemented
const hasPaidPlanForProduct = product?.hasPaidPlanForProduct;
const isFree = tier
? product?.pricingForUi?.tiers?.[ tier ]?.isFree
: product?.pricingForUi?.isFree;
const needsPurchase = ! isFree && ! hasRequiredPlan;
const needsPurchase = ! isFree && ! hasPaidPlanForProduct;

// If the product is CRM, redirect the user to the Jetpack CRM pricing page.
// This is done because CRM is not part of the WP billing system
// and we can't send them to checkout like we can with the rest of the products
if ( product.pluginSlug === 'zero-bs-crm' && ! hasRequiredPlan ) {
if ( product.pluginSlug === 'zero-bs-crm' && ! hasPaidPlanForProduct ) {
window.location.href = 'https://jetpackcrm.com/pricing/';
return;
}
Expand Down Expand Up @@ -218,6 +217,7 @@ export default function ProductInterstitial( {
clickHandler={ clickHandler }
onProductButtonClick={ clickHandler }
trackProductButtonClick={ trackProductClick }
preferProductName={ preferProductName }
isFetching={ isActivating || siteIsRegistering }
/>
) : (
Expand Down
@@ -0,0 +1,4 @@
Significance: minor
Type: fixed

Better handling on product interstitial pages if the site already has a paid product
2 changes: 1 addition & 1 deletion projects/packages/my-jetpack/composer.json
Expand Up @@ -78,7 +78,7 @@
"link-template": "https://github.com/Automattic/jetpack-my-jetpack/compare/${old}...${new}"
},
"branch-alias": {
"dev-trunk": "4.19.x-dev"
"dev-trunk": "4.20.x-dev"
},
"version-constants": {
"::PACKAGE_VERSION": "src/class-initializer.php"
Expand Down
2 changes: 1 addition & 1 deletion projects/packages/my-jetpack/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@automattic/jetpack-my-jetpack",
"version": "4.19.1-alpha",
"version": "4.20.0-alpha",
"description": "WP Admin page with information and configuration shared among all Jetpack stand-alone plugins",
"homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/packages/my-jetpack/#readme",
"bugs": {
Expand Down
2 changes: 1 addition & 1 deletion projects/packages/my-jetpack/src/class-initializer.php
Expand Up @@ -36,7 +36,7 @@ class Initializer {
*
* @var string
*/
const PACKAGE_VERSION = '4.19.1-alpha';
const PACKAGE_VERSION = '4.20.0-alpha';

/**
* HTML container ID for the IDC screen on My Jetpack page.
Expand Down
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/backup/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/boost/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -0,0 +1,5 @@
Significance: patch
Type: other
Comment: Updated composer.lock.


4 changes: 2 additions & 2 deletions projects/plugins/jetpack/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion projects/plugins/jetpack/package.json
Expand Up @@ -53,7 +53,7 @@
"@automattic/jetpack-components": "workspace:*",
"@automattic/jetpack-connection": "workspace:*",
"@automattic/jetpack-licensing": "workspace:*",
"@automattic/jetpack-my-jetpack": "workspace:4.19.1-alpha",
"@automattic/jetpack-my-jetpack": "workspace:4.20.0-alpha",
"@automattic/jetpack-partner-coupon": "workspace:*",
"@automattic/jetpack-publicize-components": "workspace:*",
"@automattic/jetpack-shared-extension-utils": "workspace:*",
Expand Down
@@ -0,0 +1,5 @@
Significance: patch
Type: changed
Comment: Updated composer.lock.


0 comments on commit 8d7171a

Please sign in to comment.