Skip to content

Commit

Permalink
Add/bundles to recommendations (#40281)
Browse files Browse the repository at this point in the history
* Fix console errors

* Add cards for Complete, Security, and Growth

* Add Security, Complete, and Growth interstitials

* Fix bundles

* Fix intro eligibility on recommendation cards

* changelog

* Remove a few unnecessary changes

* Translate feature string
  • Loading branch information
CodeyGuyDylan authored Nov 21, 2024
1 parent 1352759 commit 24a8e6b
Show file tree
Hide file tree
Showing 35 changed files with 485 additions and 41 deletions.
10 changes: 8 additions & 2 deletions projects/packages/my-jetpack/_inc/admin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
SearchInterstitial,
VideoPressInterstitial,
StatsInterstitial,
SecurityInterstitial,
GrowthInterstitial,
CompleteInterstitial,
} from './components/product-interstitial';
import JetpackAiProductPage from './components/product-interstitial/jetpack-ai/product-page';
import RedeemTokenScreen from './components/redeem-token-screen';
Expand Down Expand Up @@ -87,8 +90,11 @@ const MyJetpack = () => {
<Route path={ MyJetpackRoutes.AddLicense } element={ <AddLicenseScreen /> } />
) }
<Route path={ MyJetpackRoutes.RedeemToken } element={ <RedeemTokenScreen /> } />
<Route path="/redeem-token" element={ <RedeemTokenScreen /> } />
<Route path="/jetpack-ai" element={ <JetpackAiProductPage /> } />
<Route path={ MyJetpackRoutes.RedeemToken } element={ <RedeemTokenScreen /> } />
<Route path={ MyJetpackRoutes.JetpackAi } element={ <JetpackAiProductPage /> } />
<Route path={ MyJetpackRoutes.AddSecurity } element={ <SecurityInterstitial /> } />
<Route path={ MyJetpackRoutes.AddGrowth } element={ <GrowthInterstitial /> } />
<Route path={ MyJetpackRoutes.AddComplete } element={ <CompleteInterstitial /> } />
</Routes>
</HashRouter>
</QueryClientProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ import useAnalytics from '../../hooks/use-analytics';
import useMyJetpackConnection from '../../hooks/use-my-jetpack-connection';

const parsePricingData = ( pricingForUi: ProductCamelCase[ 'pricingForUi' ] ) => {
const { tiers, wpcomFreeProductSlug } = pricingForUi;
const { tiers, wpcomFreeProductSlug, introductoryOffer } = pricingForUi;

if ( pricingForUi.tiers ) {
const { discountPrice, fullPrice, currencyCode, wpcomProductSlug, quantity } = tiers.upgraded;
const {
discountPrice,
fullPrice,
currencyCode,
wpcomProductSlug,
quantity,
introductoryOffer: tierIntroOffer,
} = tiers.upgraded;
const hasDiscount = discountPrice && discountPrice !== fullPrice;
const eligibleForIntroDiscount = ! tierIntroOffer?.reason;
return {
wpcomFreeProductSlug,
wpcomProductSlug: ! quantity ? wpcomProductSlug : `${ wpcomProductSlug }:-q-${ quantity }`,
discountPrice: hasDiscount ? discountPrice / 12 : null,
discountPrice: hasDiscount && eligibleForIntroDiscount ? discountPrice / 12 : null,
fullPrice: fullPrice / 12,
currencyCode,
};
Expand All @@ -34,7 +42,9 @@ const parsePricingData = ( pricingForUi: ProductCamelCase[ 'pricingForUi' ] ) =>
return {
wpcomFreeProductSlug,
wpcomProductSlug,
discountPrice: isIntroductoryOffer ? discountPricePerMonth : null,
discountPrice:
// Only display discount if site is elgible
isIntroductoryOffer && ! introductoryOffer?.reason ? discountPricePerMonth : null,
fullPrice: fullPricePerMonth,
currencyCode,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import AiCard from './ai-card';
import AntiSpamCard from './anti-spam-card';
import BackupCard from './backup-card';
import BoostCard from './boost-card';
import CompleteCard from './complete-card';
import CrmCard from './crm-card';
import GrowthCard from './growth-card';
import ProtectCard from './protect-card';
import SearchCard from './search-card';
import SecurityCard from './security-card';
import SocialCard from './social-card';
import StatsCard from './stats-card';
import VideopressCard from './videopress-card';
Expand All @@ -23,10 +26,11 @@ export const JetpackModuleToProductCard: {
social: SocialCard,
ai: AiCard,
'jetpack-ai': AiCard,
security: SecurityCard,
growth: GrowthCard,
complete: CompleteCard,
// Not existing:
extras: null,
scan: null,
security: null,
creator: null,
growth: null,
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const AntiSpamCard = props => {
};

AntiSpamCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

export default AntiSpamCard;
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ const NoBackupsValueSection = props => {
};

BackupCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

NoBackupsValueSection.propTypes = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PRODUCT_SLUGS } from '../../data/constants';
import ProductCard from '../connected-product-card';
import type { FC } from 'react';

interface CompleteCardProps {
admin?: boolean;
recommendation?: boolean;
}

const CompleteCard: FC< CompleteCardProps > = ( { admin, recommendation } ) => {
return (
<ProductCard
slug={ PRODUCT_SLUGS.COMPLETE }
showMenu
admin={ admin }
recommendation={ recommendation }
/>
);
};

export default CompleteCard;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const CrmCard = props => {
};

CrmCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

export default CrmCard;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const ExtrasCard = props => {
};

ExtrasCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

export default ExtrasCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PRODUCT_SLUGS } from '../../data/constants';
import ProductCard from '../connected-product-card';
import type { FC } from 'react';

interface GrowthCardProps {
admin?: boolean;
recommendation?: boolean;
}

const GrowthCard: FC< GrowthCardProps > = ( { admin, recommendation } ) => {
return (
<ProductCard
slug={ PRODUCT_SLUGS.GROWTH }
showMenu
admin={ admin }
recommendation={ recommendation }
/>
);
};

export default GrowthCard;
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ type DisplayItemsProps = {
};

type DisplayItemType = Record<
// We don't have a card for Security or Extras, and scan is displayed as protect.
// We don't have a card for these products/bundles, and scan is displayed as protect.
// 'jetpack-ai' is the official slug for the AI module, so we also exclude 'ai'.
// The backend still supports the 'ai' slug, so it is part of the JetpackModule type.
Exclude< JetpackModule, 'extras' | 'scan' | 'security' | 'ai' | 'creator' | 'growth' >,
Exclude<
JetpackModule,
'extras' | 'scan' | 'security' | 'ai' | 'creator' | 'growth' | 'complete'
>,
FC< { admin: boolean } >
>;

Expand Down Expand Up @@ -103,13 +106,9 @@ const ProductCardsSection: FC< ProductCardsSectionProps > = ( { noticeMessage }
}, [ ownedProducts.length ] );

const filterProducts = ( products: JetpackModule[] ) => {
const productsWithNoCard = [ 'scan', 'security', 'growth', 'extras', 'complete' ];
return products.filter( product => {
if (
product === 'scan' ||
product === 'security' ||
product === 'growth' ||
product === 'extras'
) {
if ( productsWithNoCard.includes( product ) ) {
return false;
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const SearchCard = props => {
};

SearchCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

export default SearchCard;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { PRODUCT_SLUGS } from '../../data/constants';
import ProductCard from '../connected-product-card';
import type { FC } from 'react';

interface SecurityCardProps {
admin?: boolean;
recommendation?: boolean;
}

const SecurityCard: FC< SecurityCardProps > = ( { admin, recommendation } ) => {
return (
<ProductCard
slug={ PRODUCT_SLUGS.SECURITY }
showMenu
admin={ admin }
recommendation={ recommendation }
/>
);
};

export default SecurityCard;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const SocialCard = props => {
};

SocialCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

export default SocialCard;
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const StatsCard = props => {
};

StatsCard.propTypes = {
admin: PropTypes.bool.isRequired,
admin: PropTypes.bool,
};

export default StatsCard;
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function Price( { value, currency, isOld } ) {
* @param {boolean} [props.highlightLastFeature] - Whether to highlight the last feature of the list of features
* @param {boolean} [props.isFetching] - Whether the product is being activated
* @param {boolean} [props.isFetchingSuccess] - Whether the product was activated successfully
* @param {boolean} [props.isUpsell] - Whether the product is an upsell
* @return {object} ProductDetailCard react component.
*/
const ProductDetailCard = ( {
Expand All @@ -85,6 +86,7 @@ const ProductDetailCard = ( {
highlightLastFeature = false,
isFetching = false,
isFetchingSuccess = false,
isUpsell = false,
} ) => {
const {
fileSystemWriteAccess = 'no',
Expand All @@ -110,6 +112,7 @@ const ProductDetailCard = ( {
postCheckoutUrl,
} = detail;

const isBundleUpsell = isBundle && isUpsell;
const cantInstallPlugin = status === 'plugin_absent' && 'no' === fileSystemWriteAccess;

const {
Expand Down Expand Up @@ -184,7 +187,7 @@ const ProductDetailCard = ( {
} );

// Suppported products icons.
const icons = isBundle
const icons = isBundleUpsell
? supportedProducts
.join( '_plus_' )
.split( '_' )
Expand Down Expand Up @@ -255,12 +258,12 @@ const ProductDetailCard = ( {
}

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

// If we prefer the product name, use that everywhere instead of the title
const productMoniker = name && preferProductName ? name : title;
const defaultCtaLabel =
! isBundle && hasPaidPlanForProduct
! isBundleUpsell && hasPaidPlanForProduct
? sprintf(
/* translators: placeholder is product name. */
__( 'Install %s', 'jetpack-my-jetpack' ),
Expand Down Expand Up @@ -288,18 +291,18 @@ const ProductDetailCard = ( {
return (
<div
className={ clsx( styles.card, className, {
[ styles[ 'is-bundle-card' ] ]: isBundle,
[ styles[ 'is-bundle-card' ] ]: isBundleUpsell,
} ) }
>
{ isBundle && (
{ isBundleUpsell && (
<div className={ styles[ 'card-header' ] }>
<StarIcon className={ styles[ 'product-bundle-icon' ] } size={ 16 } />
<Text variant="label">{ __( 'Popular upgrade', 'jetpack-my-jetpack' ) }</Text>
</div>
) }

<div className={ styles.container }>
{ isBundle && <div className={ styles[ 'product-bundle-icons' ] }>{ icons }</div> }
{ isBundleUpsell && <div className={ styles[ 'product-bundle-icons' ] }>{ icons }</div> }
<ProductIcon slug={ slug } />

<H3>{ productMoniker }</H3>
Expand Down Expand Up @@ -367,21 +370,21 @@ const ProductDetailCard = ( {
</div>
) }

{ ( ! isBundle || ( isBundle && ! hasPaidPlanForProduct ) ) && (
{ ( ! isBundleUpsell || ( isBundleUpsell && ! hasPaidPlanForProduct ) ) && (
<ProductDetailCardButton
component={ ProductDetailButton }
onClick={ clickHandler }
hasMainCheckoutStarted={ hasMainCheckoutStarted }
isFetching={ isFetching }
isFetchingSuccess={ isFetchingSuccess }
cantInstallPlugin={ cantInstallPlugin }
isPrimary={ ! isBundle }
isPrimary={ ! isBundleUpsell }
className={ styles[ 'checkout-button' ] }
label={ ctaLabel }
/>
) }

{ ! isBundle && trialAvailable && ! hasPaidPlanForProduct && (
{ ! isBundleUpsell && trialAvailable && ! hasPaidPlanForProduct && (
<ProductDetailCardButton
component={ ProductDetailButton }
onClick={ trialClickHandler }
Expand Down Expand Up @@ -421,7 +424,7 @@ const ProductDetailCard = ( {
</div>
) }

{ isBundle && hasPaidPlanForProduct && (
{ isBundleUpsell && hasPaidPlanForProduct && (
<div className={ styles[ 'product-has-required-plan' ] }>
<CheckmarkIcon size={ 36 } />
<Text>{ __( 'Active on your site', 'jetpack-my-jetpack' ) }</Text>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 24a8e6b

Please sign in to comment.