Skip to content

Commit

Permalink
Storage Add-ons: Expose add-on upsells to plans page (#83005)
Browse files Browse the repository at this point in the history
* Expose add-on upsells to plans page

* Add selected storage add-on to checkout cart

* Add storage add-on upsells to spotlight plans

* Query product list on load

The product list is needed to derive information about storage add-ons on the /plans page

* Display purchased or default storage option on load
  • Loading branch information
jeyip authored Oct 27, 2023
1 parent ff6537e commit f307925
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 103 deletions.
28 changes: 16 additions & 12 deletions client/my-sites/add-ons/hooks/use-add-on-checkout-link.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useCallback } from '@wordpress/element';
import { useSelector } from 'react-redux';
import { getSelectedSite } from 'calypso/state/ui/selectors';

Expand All @@ -10,20 +11,23 @@ import { getSelectedSite } from 'calypso/state/ui/selectors';

const useAddOnCheckoutLink = (): ( ( addOnSlug: string, quantity?: number ) => string ) => {
const selectedSite = useSelector( getSelectedSite );
const checkoutLinkCallback = useCallback(
( addOnSlug: string, quantity?: number ): string => {
// If no site is provided, return the checkout link with the add-on (will render site-selector).
if ( ! selectedSite ) {
return `/checkout/${ addOnSlug }`;
}

return ( addOnSlug: string, quantity?: number ): string => {
// If no site is provided, return the checkout link with the add-on (will render site-selector).
if ( ! selectedSite ) {
return `/checkout/${ addOnSlug }`;
}
const checkoutLinkFormat = `/checkout/${ selectedSite?.slug }/${ addOnSlug }`;

const checkoutLinkFormat = `/checkout/${ selectedSite?.slug }/${ addOnSlug }`;

if ( quantity ) {
return checkoutLinkFormat + `:-q-${ quantity }`;
}
return checkoutLinkFormat;
};
if ( quantity ) {
return checkoutLinkFormat + `:-q-${ quantity }`;
}
return checkoutLinkFormat;
},
[ selectedSite ]
);
return checkoutLinkCallback;
};

export default useAddOnCheckoutLink;
28 changes: 15 additions & 13 deletions client/my-sites/add-ons/hooks/use-add-ons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import jetpackStatsIcon from '../icons/jetpack-stats';
import spaceUpgradeIcon from '../icons/space-upgrade';
import unlimitedThemesIcon from '../icons/unlimited-themes';
import isStorageAddonEnabled from '../is-storage-addon-enabled';
import useAddOnCheckoutLink from './use-add-on-checkout-link';
import useAddOnDisplayCost from './use-add-on-display-cost';
import useAddOnFeatureSlugs from './use-add-on-feature-slugs';
import useAddOnPrices from './use-add-on-prices';
Expand All @@ -44,7 +45,7 @@ const useSpaceUpgradesPurchased = ( {
isInSignup: boolean;
siteId?: number;
} ) => {
const { billingTransactions } = usePastBillingTransactions( isInSignup );
const { billingTransactions, isLoading } = usePastBillingTransactions( isInSignup );
const filter = useSelector( ( state ) => getBillingTransactionFilters( state, 'past' ) );

return useMemo( () => {
Expand All @@ -64,12 +65,13 @@ const useSpaceUpgradesPurchased = ( {
}
}

return spaceUpgradesPurchased;
}, [ billingTransactions, filter, isInSignup, siteId ] );
return { isLoading, spaceUpgradesPurchased };
}, [ billingTransactions, filter, isInSignup, siteId, isLoading ] );
};

const useActiveAddOnsDefs = () => {
const translate = useTranslate();
const checkoutLink = useAddOnCheckoutLink();

/*
* TODO: `useAddOnFeatureSlugs` be refactored instead to return an index of `{ [ slug ]: featureSlug[] }`
Expand Down Expand Up @@ -142,6 +144,7 @@ const useActiveAddOnsDefs = () => {
),
featured: false,
purchased: false,
checkoutLink: checkoutLink( PRODUCT_1GB_SPACE, 50 ),
},
{
productSlug: PRODUCT_1GB_SPACE,
Expand All @@ -156,6 +159,7 @@ const useActiveAddOnsDefs = () => {
),
featured: false,
purchased: false,
checkoutLink: checkoutLink( PRODUCT_1GB_SPACE, 100 ),
},
{
productSlug: PRODUCT_JETPACK_STATS_PWYW_YEARLY,
Expand Down Expand Up @@ -218,12 +222,6 @@ const getAddOnsTransformed = createSelector(

return activeAddOns
.filter( ( addOn: any ) => {
// if a user already has purchased a storage upgrade
// remove all upgrades smaller than the smallest purchased upgrade (we only allow purchasing upgrades in ascending order)
if ( spaceUpgradesPurchased.length && addOn.productSlug === PRODUCT_1GB_SPACE ) {
return ( addOn.quantity ?? 0 ) >= Math.min( ...spaceUpgradesPurchased );
}

// remove the Jetpack AI add-on if the site already supports the feature
if (
addOn.productSlug === PRODUCT_JETPACK_AI_MONTHLY &&
Expand Down Expand Up @@ -313,9 +311,14 @@ const getAddOnsTransformed = createSelector(
const currentMaxStorage = mediaStorage?.max_storage_bytes / Math.pow( 1024, 3 );
const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage;

// if the current storage add on option is greater than the available upgrade, remove it
// if the current storage add on option is greater than the available upgrade
if ( ( addOn.quantity ?? 0 ) > availableStorageUpgrade ) {
return null;
return {
...addOn,
name,
description,
exceedsSiteStorageLimits: true,
};
}
}

Expand Down Expand Up @@ -355,8 +358,7 @@ const useAddOns = ( siteId?: number, isInSignup = false ): ( AddOnMeta | null )[
// if upgrade is bought - show as manage
// if upgrade is not bought - only show it if available storage and if it's larger than previously bought upgrade
const { data: mediaStorage } = useMediaStorageQuery( siteId );
const { isLoading } = usePastBillingTransactions( isInSignup );
const spaceUpgradesPurchased = useSpaceUpgradesPurchased( { isInSignup, siteId } );
const { isLoading, spaceUpgradesPurchased } = useSpaceUpgradesPurchased( { isInSignup, siteId } );
const activeAddOns = useActiveAddOnsDefs();

return useSelector( ( state ): ( AddOnMeta | null )[] => {
Expand Down
4 changes: 3 additions & 1 deletion client/my-sites/add-ons/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ const AddOnsMain: React.FunctionComponent< Props > = () => {
const translate = useTranslate();
const selectedSite = useSelector( getSelectedSite );
const addOns = useAddOns( selectedSite?.ID );
const filteredAddOns = addOns.filter( ( addOn ) => ! addOn?.exceedsSiteStorageLimits );

const checkoutLink = useAddOnCheckoutLink();

const canManageSite = useSelector( ( state ) => {
Expand Down Expand Up @@ -131,7 +133,7 @@ const AddOnsMain: React.FunctionComponent< Props > = () => {
actionPrimary={ { text: translate( 'Buy add-on' ), handler: handleActionPrimary } }
actionSecondary={ { text: translate( 'Manage add-on' ), handler: handleActionSelected } }
useAddOnAvailabilityStatus={ useAddOnPurchaseStatus }
addOns={ addOns }
addOns={ filteredAddOns }
highlightFeatured={ true }
/>
</ContentWithHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,13 @@ const usePricingMetaForGridPlans: UsePricingMetaForGridPlans = ( {
? isPlanAvailableForPurchase( state, selectedSiteId, planSlug )
: false );
const selectedStorageOption = selectedStorageOptions?.[ planSlug ];
const storageAddOnPrices = storageAddOns?.find( ( addOn ) => {
return addOn?.featureSlugs?.includes( selectedStorageOption || '' );
} )?.prices;
const selectedStorageAddOn = storageAddOns?.find( ( addOn ) => {
return selectedStorageOption && addOn?.featureSlugs?.includes( selectedStorageOption );
} );
const storageAddOnPrices =
selectedStorageAddOn?.purchased || selectedStorageAddOn?.exceedsSiteStorageLimits
? null
: selectedStorageAddOn?.prices;
const storageAddOnPriceMonthly = storageAddOnPrices?.monthlyPrice || 0;
const storageAddOnPriceYearly = storageAddOnPrices?.yearlyPrice || 0;

Expand Down
9 changes: 8 additions & 1 deletion client/my-sites/plans-features-main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import page from 'page';
import { useSelector } from 'react-redux';
import QueryActivePromotions from 'calypso/components/data/query-active-promotions';
import QueryPlans from 'calypso/components/data/query-plans';
import QueryProductsList from 'calypso/components/data/query-products-list';
import QuerySitePlans from 'calypso/components/data/query-site-plans';
import QuerySites from 'calypso/components/data/query-sites';
import FormattedHeader from 'calypso/components/formatted-header';
Expand Down Expand Up @@ -352,9 +353,14 @@ const PlansFeaturesMain = ( {
const planPath = cartItemForPlan?.product_slug
? getPlanPath( cartItemForPlan.product_slug )
: '';

const checkoutUrl = cartItemForStorageAddOn
? `/checkout/${ siteSlug }/${ planPath },${ cartItemForStorageAddOn.product_slug }:-q-${ cartItemForStorageAddOn.quantity }`
: `/checkout/${ siteSlug }/${ planPath }`;

const checkoutUrlWithArgs = addQueryArgs(
{ ...( withDiscount && { coupon: withDiscount } ) },
`/checkout/${ siteSlug }/${ planPath }`
checkoutUrl
);

page( checkoutUrlWithArgs );
Expand Down Expand Up @@ -651,6 +657,7 @@ const PlansFeaturesMain = ( {
<QuerySites siteId={ siteId } />
<QuerySitePlans siteId={ siteId } />
<QueryActivePromotions />
<QueryProductsList />
<PlanUpsellModal
isModalOpen={ isModalOpen }
paidDomainName={ paidDomainName }
Expand Down
2 changes: 2 additions & 0 deletions client/my-sites/plans-features-main/test/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jest.mock( 'calypso/state/selectors/is-eligible-for-wpcom-monthly-plan', () => j
jest.mock( 'calypso/state/selectors/can-upgrade-to-plan', () => jest.fn() );
jest.mock( 'calypso/state/ui/selectors', () => ( {
getSelectedSiteId: jest.fn(),
getSelectedSite: jest.fn(),
} ) );
jest.mock(
'calypso/my-sites/plans-grid/hooks/npm-ready/data-store/use-plan-features-for-grid-plans',
Expand All @@ -44,6 +45,7 @@ jest.mock( 'calypso/my-sites/plans-features-main/hooks/data-store/use-priced-api
jest.fn()
);
jest.mock( 'calypso/components/data/query-active-promotions', () => jest.fn() );
jest.mock( 'calypso/components/data/query-products-list', () => jest.fn() );

import {
PLAN_FREE,
Expand Down
64 changes: 57 additions & 7 deletions client/my-sites/plans-grid/components/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import {
TERM_ANNUALLY,
type PlanSlug,
PLAN_HOSTING_TRIAL_MONTHLY,
type StorageOption,
} from '@automattic/calypso-products';
import { Button, Gridicon } from '@automattic/components';
import { WpcomPlansUI } from '@automattic/data-stores';
import { formatCurrency } from '@automattic/format-currency';
import { isMobile } from '@automattic/viewport';
import styled from '@emotion/styled';
import { useSelect } from '@wordpress/data';
import { useCallback } from '@wordpress/element';
import classNames from 'classnames';
import { localize, TranslateResult, useTranslate } from 'i18n-calypso';
Expand All @@ -23,6 +26,7 @@ import { useManageTooltipToggle } from 'calypso/my-sites/plans-grid/hooks/use-ma
import { useSelector } from 'calypso/state';
import { getPlanBillPeriod } from 'calypso/state/plans/selectors';
import { usePlansGridContext } from '../grid-context';
import useDefaultStorageOption from '../hooks/npm-ready/data-store/use-default-storage-option';
import { Plans2023Tooltip } from './plans-2023-tooltip';
import type { PlanActionOverrides } from '../types';

Expand All @@ -36,6 +40,7 @@ type PlanFeaturesActionsButtonProps = {
isPopular?: boolean;
isInSignup?: boolean;
isLaunchPage?: boolean | null;
isMonthlyPlan?: boolean;
onUpgradeClick: ( overridePlanSlug?: PlanSlug ) => void;
planSlug: PlanSlug;
flowName?: string | null;
Expand All @@ -47,6 +52,7 @@ type PlanFeaturesActionsButtonProps = {
siteId?: number | null;
isStuck: boolean;
isLargeCurrency?: boolean;
storageOptions?: StorageOption[];
};

const DummyDisabledButton = styled.div`
Expand Down Expand Up @@ -208,6 +214,7 @@ const LoggedInPlansFeatureActionButton = ( {
priceString,
isStuck,
isLargeCurrency,
isMonthlyPlan,
planTitle,
handleUpgradeButtonClick,
planSlug,
Expand All @@ -216,34 +223,56 @@ const LoggedInPlansFeatureActionButton = ( {
currentSitePlanSlug,
buttonText,
planActionOverrides,
storageOptions,
}: {
freePlan: boolean;
availableForPurchase?: boolean;
classes: string;
priceString: string | null;
isStuck: boolean;
isLargeCurrency: boolean;
isMonthlyPlan?: boolean;
planTitle: TranslateResult;
handleUpgradeButtonClick: () => void;
planSlug: string;
planSlug: PlanSlug;
currentPlanManageHref?: string;
canUserManageCurrentPlan?: boolean | null;
currentSitePlanSlug?: string | null;
buttonText?: string;
planActionOverrides?: PlanActionOverrides;
storageOptions?: StorageOption[];
} ) => {
const [ activeTooltipId, setActiveTooltipId ] = useManageTooltipToggle();
const translate = useTranslate();
const { gridPlansIndex } = usePlansGridContext();
const { current } = gridPlansIndex[ planSlug ];
const selectedStorageOptionForPlan = useSelect(
( select ) => select( WpcomPlansUI.store ).getSelectedStorageOptionForPlan( planSlug ),
[ planSlug ]
);
const { current, storageAddOnsForPlan } = gridPlansIndex[ planSlug ];
const defaultStorageOption = useDefaultStorageOption( {
storageOptions,
storageAddOnsForPlan,
} );
const canPurchaseStorageAddOns = storageAddOnsForPlan?.some(
( storageAddOn ) => ! storageAddOn?.purchased && ! storageAddOn?.exceedsSiteStorageLimits
);
const storageAddOnCheckoutHref = storageAddOnsForPlan?.find(
( addOn ) =>
selectedStorageOptionForPlan && addOn?.featureSlugs?.includes( selectedStorageOptionForPlan )
)?.checkoutLink;
const nonDefaultStorageOptionSelected = defaultStorageOption !== selectedStorageOptionForPlan;
const currentPlanBillPeriod = useSelector( ( state ) => {
return currentSitePlanSlug ? getPlanBillPeriod( state, currentSitePlanSlug ) : null;
} );
const gridPlanBillPeriod = useSelector( ( state ) => {
return planSlug ? getPlanBillPeriod( state, planSlug ) : null;
} );

if ( freePlan ) {
if (
freePlan ||
( storageAddOnsForPlan && ! canPurchaseStorageAddOns && nonDefaultStorageOptionSelected )
) {
if ( planActionOverrides?.loggedInFreePlan ) {
return (
<Button
Expand All @@ -264,6 +293,17 @@ const LoggedInPlansFeatureActionButton = ( {
}

if ( current && planSlug !== PLAN_P2_FREE ) {
if ( canPurchaseStorageAddOns && nonDefaultStorageOptionSelected && ! isMonthlyPlan ) {
return (
<Button
className={ classNames( classes, 'is-storage-upgradeable' ) }
href={ storageAddOnCheckoutHref }
>
{ translate( 'Upgrade' ) }
</Button>
);
}

return (
<Button
className={ classes }
Expand Down Expand Up @@ -341,10 +381,16 @@ const LoggedInPlansFeatureActionButton = ( {
comment: '%(priceString)s is the full price including the currency. Eg: Get Upgrade - $10',
} );
} else if ( isStuck && isLargeCurrency ) {
buttonTextFallback = translate( 'Upgrade – %(plan)s', {
context: 'verb',
args: { plan: planTitle ?? '' },
comment: '%(plan)s is the name of the plan ',
buttonTextFallback = translate( 'Get %(plan)s {{span}}%(priceString)s{{/span}}', {
args: {
plan: planTitle,
priceString: priceString ?? '',
},
comment:
'%(plan)s is the name of the plan and %(priceString)s is the full price including the currency. Eg: Get Premium - $10',
components: {
span: <span className="plan-features-2023-grid__actions-signup-plan-text" />,
},
} );
} else {
buttonTextFallback = translate( 'Upgrade', { context: 'verb' } );
Expand Down Expand Up @@ -398,6 +444,8 @@ const PlanFeaturesActionsButton: React.FC< PlanFeaturesActionsButtonProps > = (
planActionOverrides,
isStuck,
isLargeCurrency,
isMonthlyPlan,
storageOptions,
} ) => {
const translate = useTranslate();
const { gridPlansIndex } = usePlansGridContext();
Expand Down Expand Up @@ -513,7 +561,9 @@ const PlanFeaturesActionsButton: React.FC< PlanFeaturesActionsButtonProps > = (
priceString={ priceString }
isStuck={ isStuck }
isLargeCurrency={ !! isLargeCurrency }
isMonthlyPlan={ isMonthlyPlan }
planTitle={ planTitle }
storageOptions={ storageOptions }
/>
);
};
Expand Down
Loading

0 comments on commit f307925

Please sign in to comment.