Skip to content

Commit

Permalink
Expose add-on upsells to /plans pages
Browse files Browse the repository at this point in the history
Add checkout functionality to spotlight plan

Fix plans features main tests

Fix type errors

Refactor grid plans and default storage options

Prevent purchased add-ons from being added to cart

Remove unnecessary usePastBillingTransactions hook

Add checkout link to storage add-ons data structure

Add description to use default storage option hook

Display prices for all add-on options
  • Loading branch information
jeyip committed Oct 27, 2023
1 parent 5ecc05f commit 0bfe4b6
Show file tree
Hide file tree
Showing 12 changed files with 93 additions and 68 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;
26 changes: 14 additions & 12 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 @@ -315,7 +313,12 @@ const getAddOnsTransformed = createSelector(

// if the current storage add on option is greater than the available upgrade, remove it
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
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ const usePricingMetaForGridPlans: UsePricingMetaForGridPlans = ( {
const selectedStorageAddOn = storageAddOns?.find( ( addOn ) => {
return selectedStorageOption && addOn?.featureSlugs?.includes( selectedStorageOption );
} );
const storageAddOnPrices = selectedStorageAddOn?.purchased
? null
: selectedStorageAddOn?.prices;
const storageAddOnPrices =
selectedStorageAddOn?.purchased || selectedStorageAddOn?.exceedsSiteStorageLimits
? null
: selectedStorageAddOn?.prices;
const storageAddOnPriceMonthly = storageAddOnPrices?.monthlyPrice || 0;
const storageAddOnPriceYearly = storageAddOnPrices?.yearlyPrice || 0;

Expand Down
1 change: 0 additions & 1 deletion client/my-sites/plans-features-main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,6 @@ const PlansFeaturesMain = ( {
const cartItemForStorageAddOn = cartItems?.find(
( items ) => items.product_slug === PRODUCT_1GB_SPACE
);

if ( cartItemForStorageAddOn?.extra ) {
recordTracksEvent( 'calypso_signup_storage_add_on_upgrade_click', {
add_on_slug: cartItemForStorageAddOn.extra.feature_slug,
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
20 changes: 13 additions & 7 deletions client/my-sites/plans-grid/components/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ type PlanFeaturesActionsButtonProps = {
siteId?: number | null;
isStuck: boolean;
isLargeCurrency?: boolean;
storageOptions: StorageOption[];
storageOptions?: StorageOption[];
};

const DummyDisabledButton = styled.div`
Expand Down Expand Up @@ -231,13 +231,13 @@ const LoggedInPlansFeatureActionButton = ( {
isLargeCurrency: boolean;
planTitle: TranslateResult;
handleUpgradeButtonClick: () => void;
planSlug: string;
planSlug: PlanSlug;
currentPlanManageHref?: string;
canUserManageCurrentPlan?: boolean | null;
currentSitePlanSlug?: string | null;
buttonText?: string;
planActionOverrides?: PlanActionOverrides;
storageOptions: StorageOption[];
storageOptions?: StorageOption[];
} ) => {
const [ activeTooltipId, setActiveTooltipId ] = useManageTooltipToggle();
const translate = useTranslate();
Expand All @@ -247,10 +247,17 @@ const LoggedInPlansFeatureActionButton = ( {
[ planSlug ]
);
const { current, storageAddOnsForPlan } = gridPlansIndex[ planSlug ];
const defaultStorageOption = useDefaultStorageOption( { storageOptions, storageAddOnsForPlan } );
const defaultStorageOption = useDefaultStorageOption( {
storageOptions,
storageAddOnsForPlan,
} );
const canPurchaseStorageAddOns = storageAddOnsForPlan?.some(
( storageAddOn ) => ! storageAddOn?.purchased
( 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;
Expand Down Expand Up @@ -287,8 +294,7 @@ const LoggedInPlansFeatureActionButton = ( {
return (
<Button
className={ classNames( classes, 'is-storage-upgradeable' ) }
href={ currentPlanManageHref }
disabled={ ! currentPlanManageHref }
href={ storageAddOnCheckoutHref }
>
{ translate( 'Upgrade' ) }
</Button>
Expand Down
8 changes: 4 additions & 4 deletions client/my-sites/plans-grid/components/features-grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -520,10 +520,10 @@ class FeaturesGrid extends Component< FeaturesGridType > {

const shouldRenderStorageTitle =
storageOptions.length > 0 &&
( storageOptions.length === 1 || intervalType !== 'yearly' || ! showUpgradeableStorage );
// TODO: Revisit what conditions are necessary to continue hiding in stepper flows
// ! isInSignup ||
// ! ( flowName === 'onboarding' ) );
( storageOptions.length === 1 ||
intervalType !== 'yearly' ||
! showUpgradeableStorage ||
( isInSignup && ! ( flowName === 'onboarding' ) ) );

const canUpgradeStorageForPlan = isStorageUpgradeableForPlan( {
flowName: flowName ?? '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,9 @@ const getStorageOptionPrice = (
storageAddOnsForPlan: ( AddOnMeta | null )[] | null,
storageOptionSlug: string
) => {
const matchedStorageAddOn = storageAddOnsForPlan?.find(
return storageAddOnsForPlan?.find(
( addOn ) => addOn?.featureSlugs?.includes( storageOptionSlug )
);

return matchedStorageAddOn?.purchased
? undefined
: matchedStorageAddOn?.prices?.formattedMonthlyPrice;
)?.prices?.formattedMonthlyPrice;
};

const StorageAddOnOption = ( {
Expand Down Expand Up @@ -96,10 +92,14 @@ export const StorageAddOnDropdown = ( {
( select ) => select( WpcomPlansUI.store ).getSelectedStorageOptionForPlan( planSlug ),
[ planSlug ]
);
const defaultStorageOption = useDefaultStorageOption( { storageOptions, storageAddOnsForPlan } );
const defaultStorageOption = useDefaultStorageOption( {
storageOptions,
storageAddOnsForPlan,
} );

useEffect( () => {
setSelectedStorageOptionForPlan( { addOnSlug: defaultStorageOption, planSlug } );
defaultStorageOption &&
setSelectedStorageOptionForPlan( { addOnSlug: defaultStorageOption, planSlug } );
}, [] );

const selectControlOptions = storageOptions.map( ( storageOption ) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import type { StorageOption } from '@automattic/calypso-products';
import type { StorageOption, WPComStorageAddOnSlug } from '@automattic/calypso-products';
import type { AddOnMeta } from '@automattic/data-stores';

type Props = {
storageAddOnsForPlan: ( AddOnMeta | null )[] | null;
storageOptions: StorageOption[];
storageOptions?: StorageOption[];
};

/**
* Returns the storage add-on upsell option to display to
* the user on initial load. If the user has purchased a
* storage add-on, that will be the default. Otherwise,
* the storage included with any given plan will be used.
*
*/
export default function useDefaultStorageOption( { storageOptions, storageAddOnsForPlan }: Props ) {
const [ purchasedStorageAddOn ] =
storageAddOnsForPlan?.find( ( storageAddOn ) => storageAddOn?.purchased )?.featureSlugs ?? [];
const [ purchasedStorageAddOnSlug ]: [ WPComStorageAddOnSlug ] =
( storageAddOnsForPlan?.find( ( storageAddOn ) => storageAddOn?.purchased )?.featureSlugs as [
WPComStorageAddOnSlug,
] ) ?? [];

const defaultStorageOption =
purchasedStorageAddOn ||
storageOptions?.find( ( storageOption ) => ! storageOption.isAddOn )?.slug;

return defaultStorageOption;
return (
purchasedStorageAddOnSlug ||
storageOptions?.find( ( storageOption ) => ! storageOption.isAddOn )?.slug
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ const useUpgradeClickHandler = ( { gridPlans, onUpgradeClick }: Props ) => {
? addOn.featureSlugs?.includes( selectedStorageOption )
: false;
} );
const storageAddOnCartItem = storageAddOn && {
product_slug: storageAddOn.productSlug,
quantity: storageAddOn.quantity,
volume: 1,
extra: { feature_slug: selectedStorageOption },
};
const storageAddOnCartItem = storageAddOn &&
! storageAddOn.purchased && {
product_slug: storageAddOn.productSlug,
quantity: storageAddOn.quantity,
volume: 1,
extra: { feature_slug: selectedStorageOption },
};

if ( cartItemForPlan ) {
onUpgradeClick?.( [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { StorageOption } from '@automattic/calypso-products';

export const isStorageUpgradeableForPlan = ( {
// flowName,
flowName,
intervalType,
// isInSignup,
isInSignup,
showUpgradeableStorage,
storageOptions,
}: {
Expand All @@ -14,7 +14,7 @@ export const isStorageUpgradeableForPlan = ( {
storageOptions: StorageOption[];
} ) =>
// Don't show for the enterprise plan which has no storage options
storageOptions.length > 1 && intervalType === 'yearly' && showUpgradeableStorage;
// TODO: Revisit what conditions are necessary to continue hiding in stepper flows
// isInSignup &&
// flowName === 'onboarding';
storageOptions.length > 1 &&
intervalType === 'yearly' &&
showUpgradeableStorage &&
( flowName === 'onboarding' || ! isInSignup );
2 changes: 2 additions & 0 deletions packages/data-stores/src/add-ons/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ export interface AddOnMeta {
formattedMonthlyPrice: string;
formattedYearlyPrice: string;
} | null;
checkoutLink?: string;
exceedsSiteStorageLimits?: boolean;
}

0 comments on commit 0bfe4b6

Please sign in to comment.