Skip to content

Commit 7800534

Browse files
authored
Plans: display selected plan description (#101433)
* Enhance Plans Features: Integrate selected feature data into subheader and update styles * Add useSelectedFeature hook to retrieve selected feature data. * Pass selected feature data to PlansPageSubheader for dynamic header updates. * Update SecondaryFormattedHeader to display feature description when available. * Add CSS rule for button line-height in formatted header styles. * Add unit tests for useSelectedFeature hook in plans features * Create a new test file for the useSelectedFeature hook. * Implement tests to verify behavior when no feature or plan is selected, when plans or features are not found, and when handling null gridPlans. * Ensure correct feature retrieval from both array and object types in grid plans.
1 parent ac36a51 commit 7800534

File tree

5 files changed

+218
-4
lines changed

5 files changed

+218
-4
lines changed

client/components/formatted-header/style.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,8 @@
142142
cursor: pointer;
143143
color: var(--studio-gray-80);
144144
}
145+
146+
.button.is-link {
147+
line-height: inherit;
148+
}
145149
}

client/my-sites/plans-features-main/components/plans-page-subheader.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import { OnboardSelect } from '@automattic/data-stores';
33
import styled from '@emotion/styled';
44
import { useSelect } from '@wordpress/data';
55
import { useTranslate } from 'i18n-calypso';
6+
import { ReactNode } from 'react';
67
import FormattedHeader from 'calypso/components/formatted-header';
78
import { ONBOARD_STORE } from 'calypso/landing/stepper/stores';
9+
import { SelectedFeatureData } from '../hooks/use-selected-feature';
810

911
const Subheader = styled.p`
1012
margin: -32px 0 40px 0;
@@ -23,14 +25,36 @@ const Subheader = styled.p`
2325
}
2426
`;
2527

26-
const SecondaryFormattedHeader = ( { siteSlug }: { siteSlug?: string | null } ) => {
28+
const SecondaryFormattedHeader = ( {
29+
siteSlug,
30+
selectedFeature,
31+
}: {
32+
siteSlug?: string | null;
33+
selectedFeature: SelectedFeatureData | null;
34+
} ) => {
2735
const translate = useTranslate();
28-
const headerText = translate( 'Upgrade your plan to access this feature and more' );
29-
const subHeaderText = (
36+
let headerText: ReactNode = translate( 'Upgrade your plan to access this feature and more' );
37+
let subHeaderText: ReactNode = (
3038
<Button className="plans-features-main__view-all-plans is-link" href={ `/plans/${ siteSlug }` }>
3139
{ translate( 'View all plans' ) }
3240
</Button>
3341
);
42+
if ( selectedFeature?.description ) {
43+
headerText = selectedFeature.description;
44+
subHeaderText = translate(
45+
'Upgrade your plan to access this feature and more. Or {{button}}view all plans{{/button}}.',
46+
{
47+
components: {
48+
button: (
49+
<Button
50+
className="plans-features-main__view-all-plans is-link"
51+
href={ `/plans/${ siteSlug }` }
52+
/>
53+
),
54+
},
55+
}
56+
);
57+
}
3458

3559
return (
3660
<FormattedHeader
@@ -106,13 +130,15 @@ const PlansPageSubheader = ( {
106130
showPlanBenefits,
107131
offeringFreePlan,
108132
onFreePlanCTAClick,
133+
selectedFeature,
109134
}: {
110135
siteSlug?: string | null;
111136
isDisplayingPlansNeededForFeature: boolean;
112137
deemphasizeFreePlan?: boolean;
113138
offeringFreePlan?: boolean;
114139
showPlanBenefits?: boolean;
115140
onFreePlanCTAClick: () => void;
141+
selectedFeature: SelectedFeatureData | null;
116142
} ) => {
117143
const translate = useTranslate();
118144

@@ -149,7 +175,9 @@ const PlansPageSubheader = ( {
149175
) : (
150176
showPlanBenefits && <PlanBenefitHeader />
151177
) }
152-
{ isDisplayingPlansNeededForFeature && <SecondaryFormattedHeader siteSlug={ siteSlug } /> }
178+
{ isDisplayingPlansNeededForFeature && (
179+
<SecondaryFormattedHeader siteSlug={ siteSlug } selectedFeature={ selectedFeature } />
180+
) }
153181
</>
154182
);
155183
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
import { GridPlan } from '@automattic/plans-grid-next';
5+
import { renderHook } from '@testing-library/react';
6+
import useSelectedFeature from '../use-selected-feature';
7+
8+
const mockFeature = {
9+
getSlug: () => 'feature-1',
10+
getTitle: () => 'Feature 1',
11+
getDescription: () => 'Description 1',
12+
availableForCurrentPlan: true,
13+
availableOnlyForAnnualPlans: false,
14+
};
15+
16+
const mockGridPlans = [
17+
{
18+
planSlug: 'plan-1' as string,
19+
features: {
20+
wpcomFeatures: [ mockFeature ],
21+
storageFeature: {
22+
getSlug: () => 'feature-2',
23+
getTitle: () => 'Feature 2',
24+
getDescription: () => 'Description 2',
25+
availableForCurrentPlan: true,
26+
availableOnlyForAnnualPlans: false,
27+
},
28+
jetpackFeatures: [],
29+
},
30+
isVisible: true,
31+
tagline: 'Test Plan',
32+
planTitle: 'Test Plan',
33+
availableForPurchase: true,
34+
pricing: { price: 0 },
35+
},
36+
] as unknown as GridPlan[];
37+
38+
describe( 'useSelectedFeature', () => {
39+
it( 'returns null when no feature or plan is selected', () => {
40+
const { result } = renderHook( () =>
41+
useSelectedFeature( {
42+
gridPlans: mockGridPlans,
43+
selectedFeature: undefined,
44+
selectedPlan: undefined,
45+
} )
46+
);
47+
expect( result.current ).toBeNull();
48+
} );
49+
50+
it( 'returns null when plan is not found', () => {
51+
const { result } = renderHook( () =>
52+
useSelectedFeature( {
53+
gridPlans: mockGridPlans,
54+
selectedFeature: 'feature-1',
55+
selectedPlan: 'non-existent-plan',
56+
} )
57+
);
58+
expect( result.current ).toBeNull();
59+
} );
60+
61+
it( 'finds feature in array type features', () => {
62+
const { result } = renderHook( () =>
63+
useSelectedFeature( {
64+
gridPlans: mockGridPlans,
65+
selectedFeature: 'feature-1',
66+
selectedPlan: 'plan-1',
67+
} )
68+
);
69+
expect( result.current ).toEqual( {
70+
slug: 'feature-1',
71+
title: 'Feature 1',
72+
description: 'Description 1',
73+
} );
74+
} );
75+
76+
it( 'finds feature in object type features', () => {
77+
const { result } = renderHook( () =>
78+
useSelectedFeature( {
79+
gridPlans: mockGridPlans,
80+
selectedFeature: 'feature-2',
81+
selectedPlan: 'plan-1',
82+
} )
83+
);
84+
expect( result.current ).toEqual( {
85+
slug: 'feature-2',
86+
title: 'Feature 2',
87+
description: 'Description 2',
88+
} );
89+
} );
90+
91+
it( 'returns null when feature is not found in plan', () => {
92+
const { result } = renderHook( () =>
93+
useSelectedFeature( {
94+
gridPlans: mockGridPlans,
95+
selectedFeature: 'non-existent-feature',
96+
selectedPlan: 'plan-1',
97+
} )
98+
);
99+
expect( result.current ).toBeNull();
100+
} );
101+
102+
it( 'handles null gridPlans', () => {
103+
const { result } = renderHook( () =>
104+
useSelectedFeature( {
105+
gridPlans: null,
106+
selectedFeature: 'feature-1',
107+
selectedPlan: 'plan-1',
108+
} )
109+
);
110+
expect( result.current ).toBeNull();
111+
} );
112+
} );
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { GridPlan } from '@automattic/plans-grid-next';
2+
import { useMemo } from '@wordpress/element';
3+
4+
interface Params {
5+
gridPlans: GridPlan[] | null;
6+
selectedFeature?: string;
7+
selectedPlan?: string;
8+
}
9+
10+
export type SelectedFeatureData = {
11+
slug: string;
12+
title: string;
13+
description: string;
14+
};
15+
16+
const useSelectedFeature = ( {
17+
selectedFeature,
18+
selectedPlan,
19+
gridPlans,
20+
}: Params ): SelectedFeatureData | null => {
21+
const selectedFeatureData = useMemo( () => {
22+
if ( ! selectedPlan || ! selectedFeature ) {
23+
return null;
24+
}
25+
26+
const selectedPlanGroup = gridPlans?.find( ( { planSlug } ) => planSlug === selectedPlan );
27+
28+
if ( ! selectedPlanGroup?.features ) {
29+
return null;
30+
}
31+
32+
for ( const featureType of Object.values( selectedPlanGroup.features ) ) {
33+
if ( Array.isArray( featureType ) ) {
34+
const foundFeature = featureType?.find(
35+
( feature ) => feature?.getSlug() === selectedFeature
36+
);
37+
if ( foundFeature ) {
38+
return foundFeature;
39+
}
40+
} else if (
41+
featureType &&
42+
typeof featureType === 'object' &&
43+
featureType?.getSlug() === selectedFeature
44+
) {
45+
return featureType;
46+
}
47+
}
48+
return null;
49+
}, [ gridPlans, selectedFeature, selectedPlan ] );
50+
51+
if ( selectedFeatureData ) {
52+
return {
53+
slug: selectedFeature as string,
54+
title: selectedFeatureData.getTitle(),
55+
description: selectedFeatureData.getDescription(),
56+
};
57+
}
58+
59+
return null;
60+
};
61+
62+
export default useSelectedFeature;

client/my-sites/plans-features-main/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import useGenerateActionHook from './hooks/use-generate-action-hook';
7979
import usePlanBillingPeriod from './hooks/use-plan-billing-period';
8080
import usePlanFromUpsells from './hooks/use-plan-from-upsells';
8181
import usePlanIntentFromSiteMeta from './hooks/use-plan-intent-from-site-meta';
82+
import useSelectedFeature from './hooks/use-selected-feature';
8283
import useGetFreeSubdomainSuggestion from './hooks/use-suggested-free-domain-from-paid-domain';
8384
import type {
8485
PlansIntent,
@@ -755,6 +756,12 @@ const PlansFeaturesMain = ( {
755756
</div>
756757
);
757758

759+
const selectedFeatureData = useSelectedFeature( {
760+
gridPlans: gridPlansForFeaturesGrid,
761+
selectedPlan,
762+
selectedFeature,
763+
} );
764+
758765
return (
759766
<>
760767
<div className={ clsx( 'plans-features-main', 'is-pricing-grid-2023-plans-features-main' ) }>
@@ -808,6 +815,7 @@ const PlansFeaturesMain = ( {
808815
<PlansPageSubheader
809816
siteSlug={ siteSlug }
810817
isDisplayingPlansNeededForFeature={ isDisplayingPlansNeededForFeature }
818+
selectedFeature={ selectedFeatureData }
811819
offeringFreePlan={ offeringFreePlan }
812820
deemphasizeFreePlan={ deemphasizeFreePlan }
813821
onFreePlanCTAClick={ onFreePlanCTAClick }

0 commit comments

Comments
 (0)