Skip to content

Commit

Permalink
Plans: Enable term savings price display - as a grid feature (#96357)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskmnds authored Nov 25, 2024
1 parent 07b9ed5 commit a2013c3
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 11 deletions.
8 changes: 7 additions & 1 deletion client/my-sites/plans-features-main/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import config from '@automattic/calypso-config';
import config, { isEnabled } from '@automattic/calypso-config';
import {
chooseDefaultCustomerType,
getPlan,
Expand Down Expand Up @@ -838,6 +838,9 @@ const PlansFeaturesMain = ( {
enableReducedFeatureGroupSpacing={ showSimplifiedFeatures }
enableLogosOnlyForEnterprisePlan={ showSimplifiedFeatures }
hideFeatureGroupTitles={ showSimplifiedFeatures }
enableTermSavingsPriceDisplay={ isEnabled(
'plans/term-savings-price-display'
) }
/>
) }
{ showEscapeHatch && hidePlansFeatureComparison && viewAllPlansButton }
Expand Down Expand Up @@ -897,6 +900,9 @@ const PlansFeaturesMain = ( {
}
enableFeatureTooltips
featureGroupMap={ featureGroupMapForComparisonGrid }
enableTermSavingsPriceDisplay={ isEnabled(
'plans/term-savings-price-display'
) }
/>
) }
<ComparisonGridToggle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,7 @@ const WrappedComparisonGrid = ( {
hideUnsupportedFeatures,
enableFeatureTooltips,
featureGroupMap,
enableTermSavingsPriceDisplay,
...otherProps
}: ComparisonGridExternalProps ) => {
const gridContainerRef = useRef< HTMLDivElement >( null );
Expand Down Expand Up @@ -1186,6 +1187,7 @@ const WrappedComparisonGrid = ( {
enableFeatureTooltips={ enableFeatureTooltips }
featureGroupMap={ featureGroupMap }
hideUnsupportedFeatures={ hideUnsupportedFeatures }
enableTermSavingsPriceDisplay={ enableTermSavingsPriceDisplay }
>
<ComparisonGrid
intervalType={ intervalType }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ const WrappedFeaturesGrid = ( props: FeaturesGridExternalProps ) => {
featureGroupMap = {},
hideFeatureGroupTitles,
enterpriseFeaturesList,
enableTermSavingsPriceDisplay,
} = props;

const gridContainerRef = useRef< HTMLDivElement >( null );
Expand Down Expand Up @@ -430,6 +431,7 @@ const WrappedFeaturesGrid = ( props: FeaturesGridExternalProps ) => {
hideFeatureGroupTitles={ hideFeatureGroupTitles }
featureGroupMap={ featureGroupMap }
enterpriseFeaturesList={ enterpriseFeaturesList }
enableTermSavingsPriceDisplay={ enableTermSavingsPriceDisplay }
>
<FeaturesGrid { ...props } gridSize={ gridSize ?? undefined } />
</PlansGridContextProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { isWpcomEnterpriseGridPlan, type PlanSlug } from '@automattic/calypso-products';
import {
getPlanSlugForTermVariant,
isWpcomEnterpriseGridPlan,
PERIOD_LIST,
TERM_MONTHLY,
type PlanSlug,
} from '@automattic/calypso-products';
import { PlanPrice } from '@automattic/components';
import { AddOns, Plans } from '@automattic/data-stores';
import clsx from 'clsx';
import { useTranslate } from 'i18n-calypso';
import { usePlansGridContext } from '../../../grid-context';
Expand All @@ -14,12 +21,32 @@ interface HeaderPriceProps {
visibleGridPlans: GridPlan[];
}

/**
* Returns the term variant plan slug for savings calculation.
* This currently resolves to the monthly plan slug for annual/biennial/triennial plans.
*/
const useTermVariantPlanSlugForSavings = ( {
planSlug,
billingPeriod,
}: {
planSlug: PlanSlug;
billingPeriod?: -1 | ( typeof PERIOD_LIST )[ number ];
} ) => {
// If the billing period is yearly or above, we return the monthly variant's plan slug
if ( billingPeriod && 365 <= billingPeriod ) {
return getPlanSlugForTermVariant( planSlug, TERM_MONTHLY );
}

return null;
};

const HeaderPrice = ( { planSlug, visibleGridPlans }: HeaderPriceProps ) => {
const translate = useTranslate();
const { gridPlansIndex } = usePlansGridContext();
const { gridPlansIndex, enableTermSavingsPriceDisplay, siteId, coupon, helpers } =
usePlansGridContext();
const {
current,
pricing: { currencyCode, originalPrice, discountedPrice, introOffer },
pricing: { currencyCode, originalPrice, discountedPrice, introOffer, billingPeriod },
} = gridPlansIndex[ planSlug ];
const isPricedPlan = null !== originalPrice.monthly;

Expand All @@ -45,6 +72,16 @@ const HeaderPrice = ( { planSlug, visibleGridPlans }: HeaderPriceProps ) => {
ignoreWhitespace: true,
} );

const storageAddOns = AddOns.useStorageAddOns( { siteId } );
const termVariantPlanSlug = useTermVariantPlanSlugForSavings( { planSlug, billingPeriod } );
const termVariantPricing = Plans.usePricingMetaForGridPlans( {
planSlugs: termVariantPlanSlug ? [ termVariantPlanSlug ] : [],
storageAddOns,
coupon,
siteId,
useCheckPlanAvailabilityForPurchase: helpers?.useCheckPlanAvailabilityForPurchase,
} )?.[ termVariantPlanSlug ?? '' ];

if ( isWpcomEnterpriseGridPlan( planSlug ) || ! isPricedPlan ) {
return null;
}
Expand Down Expand Up @@ -123,7 +160,56 @@ const HeaderPrice = ( { planSlug, visibleGridPlans }: HeaderPriceProps ) => {
);
}

if ( isAnyVisibleGridPlanOneTimeDiscounted || isAnyVisibleGridPlanOnIntroOffer ) {
const termVariantPrice =
termVariantPricing?.discountedPrice.monthly ?? termVariantPricing?.originalPrice.monthly ?? 0;
const planPrice = discountedPrice.monthly ?? originalPrice.monthly ?? 0;
const savings =
termVariantPrice > planPrice
? Math.floor( ( ( termVariantPrice - planPrice ) / termVariantPrice ) * 100 )
: 0;

if ( enableTermSavingsPriceDisplay && termVariantPricing && savings ) {
return (
<div className="plans-grid-next-header-price">
<div className="plans-grid-next-header-price__badge">
{ translate( 'Save %(savings)d%%', {
args: { savings },
comment: 'Example: Save 35%',
} ) }
</div>
<div
className={ clsx( 'plans-grid-next-header-price__pricing-group', {
'is-large-currency': isLargeCurrency,
} ) }
>
<PlanPrice
currencyCode={ currencyCode }
rawPrice={ termVariantPricing.originalPrice.monthly }
displayPerMonthNotation={ false }
isLargeCurrency={ isLargeCurrency }
isSmallestUnit
priceDisplayWrapperClassName="plans-grid-next-header-price__display-wrapper"
original
/>
<PlanPrice
currencyCode={ currencyCode }
rawPrice={ discountedPrice.monthly ?? originalPrice.monthly }
displayPerMonthNotation={ false }
isLargeCurrency={ isLargeCurrency }
isSmallestUnit
priceDisplayWrapperClassName="plans-grid-next-header-price__display-wrapper"
discounted
/>
</div>
</div>
);
}

if (
isAnyVisibleGridPlanOneTimeDiscounted ||
isAnyVisibleGridPlanOnIntroOffer ||
enableTermSavingsPriceDisplay
) {
return (
<div className="plans-grid-next-header-price">
<div className="plans-grid-next-header-price__badge is-hidden">' '</div>
Expand Down
29 changes: 23 additions & 6 deletions packages/plans-grid-next/src/components/test/header-price.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,34 @@ jest.mock( 'react-redux', () => ( {
useSelector: jest.fn(),
} ) );
jest.mock( '../../grid-context', () => ( { usePlansGridContext: jest.fn() } ) );
jest.mock( '@automattic/data-stores', () => ( {
...jest.requireActual( '@automattic/data-stores' ),
AddOns: {
useStorageAddOns: jest.fn(),
},
Plans: {
usePricingMetaForGridPlans: jest.fn(),
},
} ) );

import {
type PlanSlug,
PLAN_ANNUAL_PERIOD,
PLAN_ENTERPRISE_GRID_WPCOM,
PLAN_PERSONAL,
} from '@automattic/calypso-products';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render } from '@testing-library/react';
import React from 'react';
import React, { useMemo } from 'react';
import { usePlansGridContext } from '../../grid-context';
import HeaderPrice from '../shared/header-price';

const Wrapper = ( { children } ) => {
const queryClient = useMemo( () => new QueryClient(), [] );

return <QueryClientProvider client={ queryClient }>{ children }</QueryClientProvider>;
};

describe( 'HeaderPrice', () => {
const defaultProps = {
isLargeCurrency: false,
Expand Down Expand Up @@ -53,7 +69,7 @@ describe( 'HeaderPrice', () => {
},
} ) );

const { container } = render( <HeaderPrice { ...defaultProps } /> );
const { container } = render( <HeaderPrice { ...defaultProps } />, { wrapper: Wrapper } );
const rawPrice = container.querySelector( '.plan-price.is-original' );
const discountedPrice = container.querySelector( '.plan-price.is-discounted' );

Expand All @@ -78,7 +94,7 @@ describe( 'HeaderPrice', () => {
},
} ) );

const { container } = render( <HeaderPrice { ...defaultProps } /> );
const { container } = render( <HeaderPrice { ...defaultProps } />, { wrapper: Wrapper } );
const rawPrice = container.querySelector( '.plan-price' );
const discountedPrice = container.querySelector( '.plan-price.is-discounted' );

Expand All @@ -104,7 +120,8 @@ describe( 'HeaderPrice', () => {
} ) );

const { container } = render(
<HeaderPrice { ...defaultProps } planSlug={ PLAN_ENTERPRISE_GRID_WPCOM } />
<HeaderPrice { ...defaultProps } planSlug={ PLAN_ENTERPRISE_GRID_WPCOM } />,
{ wrapper: Wrapper }
);

expect( container ).toBeEmptyDOMElement();
Expand Down Expand Up @@ -134,7 +151,7 @@ describe( 'HeaderPrice', () => {
},
} ) );

const { container } = render( <HeaderPrice { ...defaultProps } /> );
const { container } = render( <HeaderPrice { ...defaultProps } />, { wrapper: Wrapper } );
const badge = container.querySelector( '.plans-grid-next-header-price__badge' );

expect( badge ).toHaveTextContent( 'Special Offer' );
Expand All @@ -157,7 +174,7 @@ describe( 'HeaderPrice', () => {
},
} ) );

const { container } = render( <HeaderPrice { ...defaultProps } /> );
const { container } = render( <HeaderPrice { ...defaultProps } />, { wrapper: Wrapper } );
const badge = container.querySelector( '.plans-grid-next-header-price__badge' );

expect( badge ).toHaveTextContent( 'One time discount' );
Expand Down
3 changes: 3 additions & 0 deletions packages/plans-grid-next/src/grid-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface PlansGridContext {
enableStorageAsBadge?: boolean;
enableReducedFeatureGroupSpacing?: boolean;
enableLogosOnlyForEnterprisePlan?: boolean;
enableTermSavingsPriceDisplay?: boolean;
featureGroupMap: Partial< FeatureGroupMap >;
hideUnsupportedFeatures?: boolean;
hideFeatureGroupTitles?: boolean;
Expand All @@ -48,6 +49,7 @@ const PlansGridContextProvider = ( {
enableStorageAsBadge,
enableReducedFeatureGroupSpacing,
enableLogosOnlyForEnterprisePlan,
enableTermSavingsPriceDisplay,
featureGroupMap,
hideUnsupportedFeatures,
hideFeatureGroupTitles,
Expand Down Expand Up @@ -80,6 +82,7 @@ const PlansGridContextProvider = ( {
enableStorageAsBadge,
enableReducedFeatureGroupSpacing,
enableLogosOnlyForEnterprisePlan,
enableTermSavingsPriceDisplay,
featureGroupMap,
hideUnsupportedFeatures,
hideFeatureGroupTitles,
Expand Down
7 changes: 7 additions & 0 deletions packages/plans-grid-next/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,13 @@ export type GridContextProps = {
* Hide the titles for feature groups in the features grid
*/
hideFeatureGroupTitles?: boolean;

/**
* Enable the display of the term savings in plan prices.
* Prices will display crossed out with the savings from shorter term accentuated in a label.
* This carries lower precedence than promo/coupon and introductory pricing, irrespective of whether set or not.
*/
enableTermSavingsPriceDisplay?: boolean;
};

export type ComparisonGridExternalProps = Omit<
Expand Down

0 comments on commit a2013c3

Please sign in to comment.