Skip to content
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
9 changes: 4 additions & 5 deletions client/my-sites/stats/jetpack-upsell-section/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getSiteTitle } from 'calypso/state/sites/selectors';
import { getSelectedSiteId } from 'calypso/state/ui/selectors';
import UpsellCard from './upsell-card';
import {
filterUpsellsBySiteFeatures,
getAvailableUpsells,
getUpsellFeatureSlugs,
Product,
Expand Down Expand Up @@ -43,11 +44,9 @@ function getVisibleUpsells( siteId: number | null, siteFeatures: string[] ): Pro
return [];
}

// Filter available upsells against site features.
// If an upsell has even one feature that is not active on the site, present it to the user.
const filteredUpsells = getAvailableUpsells().filter( ( upsell ) =>
upsell.features.some( ( feature ) => ! siteFeatures.includes( feature ) )
);
// Hide upsells for products the site already owns, either directly or through
// a bundling plan (e.g. don't upsell Backup to a site on Jetpack Security).
const filteredUpsells = filterUpsellsBySiteFeatures( getAvailableUpsells(), siteFeatures );

// Add the checkout URL to the results.
const finalUpsells = filteredUpsells.map( ( upsell ) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { filterUpsellsBySiteFeatures, getAvailableUpsells } from '../upsell-card/available-upsells';

// Feature slugs a site on Jetpack Security (Daily) reports: Backup, Scan and
// Anti-spam are bundled, but not Search, VideoPress, Boost or Social.
const SECURITY_DAILY_FEATURES = [
'antispam',
'backups',
'backups-daily',
'full-activity-log',
'scan',
];

describe( 'filterUpsellsBySiteFeatures', () => {
const upsells = getAvailableUpsells();
const visibleSlugs = ( siteFeatures: string[] ) =>
filterUpsellsBySiteFeatures( upsells, siteFeatures ).map( ( upsell ) => upsell.slug );

it( 'shows all upsells when the site has no features', () => {
expect( visibleSlugs( [] ) ).toEqual( [
'security',
'backup',
'search',
'video',
'boost',
'social',
] );
} );

it( 'hides the Backup and Security upsells when a plan already bundles them', () => {
expect( visibleSlugs( SECURITY_DAILY_FEATURES ) ).toEqual( [
'search',
'video',
'boost',
'social',
] );
} );

it( 'hides the Backup upsell but keeps Security when only Backup is owned', () => {
expect( visibleSlugs( [ 'backups' ] ) ).toEqual( [
'security',
'search',
'video',
'boost',
'social',
] );
} );

it( 'hides every upsell when the site has all owned features', () => {
const allOwnedFeatures = upsells.flatMap( ( upsell ) => upsell.ownedFeatures );
expect( visibleSlugs( allOwnedFeatures ) ).toEqual( [] );
} );
} );
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export type Product = {
isFree: boolean;
slug: string;
title: string;
features: string[];
// Feature slugs that indicate the product (or a plan bundling it) is already
// owned. The upsell is hidden when the site has all of them.
ownedFeatures: string[];
checkoutSlug: string;
checkoutUrl: string | null;
};
Expand All @@ -37,26 +39,7 @@ export function getAvailableUpsells() {
isFree: false,
slug: 'security',
title: 'Security',
features: [
'akismet',
'antispam',
'backups',
'backups-daily',
'core/audio',
'full-activity-log',
'google-analytics',
'google-my-business',
'priority_support',
'real-time-backups',
'scan',
'simple-payments',
'subscriber-unlimited-imports',
'support',
'vaultpress-backups',
'video-hosting',
'wordads',
'wordads-jetpack',
],
ownedFeatures: [ 'backups', 'scan', 'antispam' ],
checkoutSlug: PLAN_JETPACK_SECURITY_T1_YEARLY,
},
{
Expand All @@ -68,7 +51,7 @@ export function getAvailableUpsells() {
isFree: false,
slug: 'backup',
title: 'Backup',
features: [ 'backups', 'full-activity-log', 'real-time-backups', 'priority_support' ],
ownedFeatures: [ 'backups' ],
checkoutSlug: PRODUCT_JETPACK_BACKUP_T1_YEARLY,
},
{
Expand All @@ -80,7 +63,7 @@ export function getAvailableUpsells() {
isFree: false,
slug: 'search',
title: 'Search',
features: [ 'search', 'instant-search' ],
ownedFeatures: [ 'search' ],
checkoutSlug: PRODUCT_JETPACK_SEARCH,
},
{
Expand All @@ -92,7 +75,7 @@ export function getAvailableUpsells() {
isFree: false,
slug: 'video',
title: 'VideoPress',
features: [ 'videopress', 'videopress-1tb-storage' ],
ownedFeatures: [ 'videopress' ],
checkoutSlug: PRODUCT_JETPACK_VIDEOPRESS,
},
{
Expand All @@ -104,14 +87,7 @@ export function getAvailableUpsells() {
isFree: true,
slug: 'boost',
title: 'Boost',
features: [
'cloud-critical-css',
'cornerstone-10-pages',
'image-cdn-liar',
'image-cdn-quality',
'image-size-analysis',
'performance-history',
],
ownedFeatures: [ 'cloud-critical-css' ],
checkoutSlug: PRODUCT_JETPACK_BOOST,
},
{
Expand All @@ -123,11 +99,7 @@ export function getAvailableUpsells() {
isFree: true,
slug: 'social',
title: 'Social',
features: [
'social-enhanced-publishing',
'social-image-generator',
'subscriber-unlimited-imports',
],
ownedFeatures: [ 'social-enhanced-publishing' ],
checkoutSlug: PRODUCT_JETPACK_SOCIAL_BASIC,
},
] as Product[];
Expand All @@ -137,5 +109,14 @@ export function getAvailableUpsells() {
// Currently we end up calling getAvailableUpsells() twice per render.
export function getUpsellFeatureSlugs(): string[] {
const upsells = getAvailableUpsells();
return upsells.flatMap( ( upsell ) => upsell.features );
return upsells.flatMap( ( upsell ) => upsell.ownedFeatures );
}
Comment on lines 110 to +113

export function filterUpsellsBySiteFeatures(
upsells: Product[],
siteFeatures: string[]
): Product[] {
return upsells.filter(
( upsell ) => ! upsell.ownedFeatures.every( ( feature ) => siteFeatures.includes( feature ) )
);
}
Loading