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

My Jetpack: add feature as possible recommendations #40639

Open
wants to merge 9 commits into
base: trunk
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ const EvaluationRecommendations: FC = () => {
fluid
>
{ recommendedModules.map( module => {
const Card = JetpackModuleToProductCard[ module ];
const moduleName = module.replace( 'feature_', '' );
const Card = JetpackModuleToProductCard[ moduleName ];
return (
Card && (
<Col tagName="li" key={ module } lg={ 4 }>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,23 @@ import styles from './style.module.scss';
import usePricingData from './use-pricing-data';

const PriceComponent = ( { slug }: { slug: string } ) => {
const { discountPrice, fullPrice, currencyCode } = usePricingData( slug );
const { discountPrice, fullPrice, currencyCode, isFeature, hasFreeOffering } =
usePricingData( slug );
const isFreeFeature = isFeature && hasFreeOffering && ! fullPrice;
return (
<div className={ styles.priceContainer }>
{ discountPrice && (
<span className={ styles.price }>{ formatCurrency( discountPrice, currencyCode ) }</span>
) }
<span className={ clsx( styles.price, discountPrice && styles.discounted ) }>
{ formatCurrency( fullPrice, currencyCode ) }
<span className={ clsx( styles.price, { [ styles.discounted ]: discountPrice } ) }>
{ ! isFreeFeature && formatCurrency( fullPrice, currencyCode ) }
{ isFreeFeature && __( 'Free', 'jetpack-my-jetpack' ) }
</span>
<span className={ styles.term }>{ __( '/month, billed yearly', 'jetpack-my-jetpack' ) }</span>
{ ! isFreeFeature && (
<span className={ styles.term }>
{ __( '/month, billed yearly', 'jetpack-my-jetpack' ) }
</span>
) }
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@ import styles from './style.module.scss';
import usePricingData from './use-pricing-data';

const RecommendationActions = ( { slug }: { slug: string } ) => {
const { secondaryAction, purchaseAction, isActivating } = usePricingData( slug );
const { secondaryAction, primaryAction, isFeature, isActivating, isInstalling } =
usePricingData( slug );

return (
<div className={ styles.actions }>
<div className={ clsx( styles.buttons, styles.upsell ) }>
{ purchaseAction && (
<Button size="small" { ...purchaseAction }>
{ purchaseAction.label }
{ primaryAction && (
<Button
size="small"
disabled={ isFeature && ( isActivating || isInstalling ) }
{ ...primaryAction }
>
{ primaryAction.label }
</Button>
) }
{ secondaryAction && (
<Button size="small" variant="secondary" disabled={ isActivating } { ...secondaryAction }>
{ secondaryAction.label }
</Button>
) }
<Button size="small" variant="secondary" disabled={ isActivating } { ...secondaryAction }>
{ secondaryAction.label }
</Button>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { __ } from '@wordpress/i18n';
import { useCallback } from 'react';
import { PRODUCT_STATUSES } from '../../constants';
import useActivate from '../../data/products/use-activate';
import useInstallStandalonePlugin from '../../data/products/use-install-standalone-plugin';
import useProduct from '../../data/products/use-product';
import { ProductCamelCase } from '../../data/types';
import { getMyJetpackWindowInitialState } from '../../data/utils/get-my-jetpack-window-state';
Expand Down Expand Up @@ -52,7 +53,18 @@ const parsePricingData = ( pricingForUi: ProductCamelCase[ 'pricingForUi' ] ) =>
};
};

const getPurchaseAction = ( detail: ProductCamelCase, onCheckout: () => void ) => {
// type for onCheckout and onActivate
type Actions = {
onCheckout: () => void;
onActivate: () => void;
onInstall: () => void;
onManage: () => void;
};

const getPrimaryAction = (
detail: ProductCamelCase,
{ onCheckout, onActivate, onInstall, onManage }: Actions
) => {
const isUpgradable =
detail.status === PRODUCT_STATUSES.ACTIVE &&
( detail.isUpgradableByBundle.length || detail.isUpgradable );
Expand All @@ -66,10 +78,32 @@ const getPurchaseAction = ( detail: ProductCamelCase, onCheckout: () => void ) =
return null;
}

if ( detail.isFeature ) {
if ( detail.status === PRODUCT_STATUSES.MODULE_DISABLED ) {
return { label: __( 'Activate', 'jetpack-my-jetpack' ), onClick: onActivate };
}
if ( detail.status === PRODUCT_STATUSES.ABSENT ) {
return { label: __( 'Install', 'jetpack-my-jetpack' ), onClick: onInstall };
}
if ( detail.status === PRODUCT_STATUSES.USER_CONNECTION_ERROR ) {
return { label: __( 'Connect', 'jetpack-my-jetpack' ), href: '#/connection' };
}

return {
label: __( 'Manage', 'jetpack-my-jetpack' ),
href: detail.manageUrl,
onClick: onManage,
};
}

return { label: __( 'Purchase', 'jetpack-my-jetpack' ), onClick: onCheckout };
};

const getSecondaryAction = ( detail: ProductCamelCase, onActivate: () => void ) => {
if ( detail.isFeature ) {
return null;
}

const START_FOR_FREE_FEATURE_FLAG = false;
const isNotActiveOrNeedsExplicitFreePlan =
! detail.isPluginActive ||
Expand Down Expand Up @@ -98,6 +132,7 @@ const usePricingData = ( slug: string ) => {
const { wpcomProductSlug, wpcomFreeProductSlug, ...data } = parsePricingData(
detail.pricingForUi
);
const { install: installPlugin, isPending: isInstalling } = useInstallStandalonePlugin( slug );

const { isUserConnected } = useMyJetpackConnection();
const { myJetpackUrl, siteSuffix } = getMyJetpackWindowInitialState();
Expand Down Expand Up @@ -135,10 +170,31 @@ const usePricingData = ( slug: string ) => {
runCheckout();
}, [ activate, recordEvent, runCheckout, slug ] );

const handleInstall = useCallback( () => {
recordEvent( 'jetpack_myjetpack_evaluation_recommendations_install_plugin_click', {
product: slug,
} );
installPlugin();
}, [ slug, installPlugin, recordEvent ] );

const handleManage = useCallback( () => {
recordEvent( 'jetpack_myjetpack_evaluation_recommendations_manage_click', {
product: slug,
} );
}, [ slug, recordEvent ] );

return {
secondaryAction: getSecondaryAction( detail, handleActivate ),
purchaseAction: getPurchaseAction( detail, handleCheckout ),
primaryAction: getPrimaryAction( detail, {
onCheckout: handleCheckout,
onActivate: handleActivate,
onInstall: handleInstall,
onManage: handleManage,
} ),
isFeature: detail.isFeature,
hasFreeOffering: detail.hasFreeOffering,
isActivating,
isInstalling,
...data,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import BoostCard from './boost-card';
import CompleteCard from './complete-card';
import CrmCard from './crm-card';
import GrowthCard from './growth-card';
import NewsletterCard from './newsletter-card';
import ProtectCard from './protect-card';
import RelatedPostsCard from './related-posts-card';
import SearchCard from './search-card';
import SecurityCard from './security-card';
import SiteAcceleratorCard from './site-accelerator-card';
import SocialCard from './social-card';
import StatsCard from './stats-card';
import VideopressCard from './videopress-card';
Expand All @@ -33,4 +36,8 @@ export const JetpackModuleToProductCard: {
extras: null,
scan: null,
creator: null,
// Features:
newsletter: NewsletterCard,
'related-posts': RelatedPostsCard,
'site-accelerator': SiteAcceleratorCard,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PRODUCT_SLUGS } from '../../data/constants';
import ProductCard from '../connected-product-card';
import type { FC } from 'react';

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

const NewsletterCard: FC< NewsletterCardProps > = ( { admin, recommendation } ) => {
return (
<ProductCard
slug={ PRODUCT_SLUGS.NEWSLETTER }
admin={ admin }
recommendation={ recommendation }
/>
);
};

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

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

const RelatedPostsCard: FC< RelatedPostsCardProps > = ( { admin, recommendation } ) => {
return (
<ProductCard
slug={ PRODUCT_SLUGS.RELATED_POSTS }
admin={ admin }
recommendation={ recommendation }
/>
);
};

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

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

const SiteAcceleratorCard: FC< SiteAcceleratorCardProps > = ( { admin, recommendation } ) => {
return (
<ProductCard
slug={ PRODUCT_SLUGS.SITE_ACCELERATOR }
admin={ admin }
recommendation={ recommendation }
/>
);
};

export default SiteAcceleratorCard;
10 changes: 7 additions & 3 deletions projects/packages/my-jetpack/_inc/data/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,21 @@ export const PRODUCT_SLUGS = {
ANTI_SPAM: 'anti-spam',
BACKUP: 'backup',
BOOST: 'boost',
BRUTE_FORCE: 'brute-force',
CRM: 'crm',
CREATOR: 'creator',
EXTRAS: 'extras',
JETPACK_AI: 'jetpack-ai',
NEWSLETTER: 'newsletter',
PROTECT: 'protect',
RELATED_POSTS: 'related-posts',
SCAN: 'scan',
SEARCH: 'search',
SITE_ACCELERATOR: 'site-accelerator',
SOCIAL: 'social',
SECURITY: 'security',
PROTECT: 'protect',
VIDEOPRESS: 'videopress',
STATS: 'stats',
VIDEOPRESS: 'videopress',
SECURITY: 'security',
GROWTH: 'growth',
COMPLETE: 'complete',
} satisfies Record< string, JetpackModule >;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

My Jetpack: introduce feature cards for recommendations in My Jetpack.
12 changes: 8 additions & 4 deletions projects/packages/my-jetpack/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ type JetpackModule =
| 'extras'
| 'ai'
| 'jetpack-ai'
| 'protect'
| 'scan'
| 'search'
| 'social'
| 'security'
| 'protect'
| 'videopress'
| 'stats'
| 'videopress'
| 'security'
| 'growth'
| 'complete';
| 'complete'
| 'site-accelerator'
| 'newsletter'
| 'related-posts';

type ThreatItem = {
// Protect API properties (free plan)
Expand Down Expand Up @@ -174,6 +177,7 @@ interface Window {
has_paid_plan_for_product: boolean;
features_by_tier: Array< string >;
is_bundle: boolean;
is_feature: boolean;
is_plugin_active: boolean;
is_upgradable: boolean;
is_upgradable_by_bundle: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,6 @@ abstract class Module_Product extends Product {
*/
public static $module_name = null;

/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = false;

/**
* Get the plugin slug - ovewrite it ans return Jetpack's
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public static function get_manage_url() {
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );

if ( $plugin_filename ) {
Expand Down
8 changes: 8 additions & 0 deletions projects/packages/my-jetpack/src/products/class-product.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ abstract class Product {
*/
const EXPIRATION_CUTOFF_TIME = '+2 months';

/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = false;

/**
* Whether this product requires a site connection
*
Expand Down Expand Up @@ -182,6 +189,7 @@ public static function get_info() {
'is_plugin_active' => static::is_plugin_active(),
'is_upgradable' => static::is_upgradable(),
'is_upgradable_by_bundle' => static::is_upgradable_by_bundle(),
'is_feature' => static::$is_feature,
'supported_products' => static::get_supported_products(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'requires_user_connection' => static::$requires_user_connection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public static function get_manage_url() {
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );

if ( $plugin_filename ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public static function get_manage_url() {
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );

if ( $plugin_filename ) {
Expand Down
Loading