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

Storage Add-ons: Expose add-on upsells to plans page #83005

Merged
merged 14 commits into from
Oct 27, 2023
Merged
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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only change made in this file is the usage of useCallback to align with the 2023 plans-grid optimization efforts p1697705360647249/1697700875.046909-slack-CV2TX2PAN

Copy link
Contributor

@chriskmnds chriskmnds Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I don't think this needs to return a callback at all. We should probably instead be passing in selectedSiteSlug and return the checkout link directly from the hook. #83005 (comment)

( 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 />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note:

<QueryProductList /> is necessary to generate add-on product data on the /plans page

<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 )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought. Since we use this condition (canPurchaseStorageAddOns && nonDefaultStorageOptionSelected) at multiple places, would it make sense to combine into one? Not sure how we'd name that though :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey apologies if I'm not seeing it, but one of the conditions is canPurchaseStorageAddOns && nonDefaultStorageOptionSelected and the other is ! canPurchaseStorageAddOns && nonDefaultStorageOptionSelected.

I'm not seeing how they can be combined 😅

) {
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
Loading