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

Plans: Use 2year and 3year default interval type #96174

Merged
merged 8 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { useExperiment } from 'calypso/lib/explat';

const useTermFromExperimentVariant = (
chriskmnds marked this conversation as resolved.
Show resolved Hide resolved
experimentVariant: string | null | undefined
): string | null => {
switch ( experimentVariant ) {
case 'default_to_three_year_plans':
return '3yearly';
case 'default_to_two_year_plans':
return '2yearly';
default:
return null;
}
};

/**
* Returns:
* - the term as yearly, 2yearly, 3yearly (etc.) to be used as default in the plans
* page based on the experiment variant
* - OR null/undefined if control variant or if the experiment is still loading (Subject to change)
*
* This hook although used for the experiment, it can be refactored in the end to
* define the default term in the grid/plans page.
*
*/
const useLongerPlanTermDefaultExperiment = (): {
isLoadingExperiment: boolean;
term?: string | null;
// TODO: Consider removing this and always return concrete term values (where undefined/null would mean no term savings)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll study this a bit more. I want to make the "clean up" phase after the experiment as easy as possible, e.g., this hook remaining to define the default term/interval type.

isEligibleForTermSavings: boolean;
} => {
// TODO: Figure out how to define explicit types for the experiment assignment
// variation names 'default_to_three_year_plans', 'default_to_two_year_plans'
// and 'emphasize_savings_only'.
const [ isLoadingExperimentAssignment, experimentAssignment ] = useExperiment(
'calypso_plans_page_emphasize_longer_plan_savings'
);
const term = useTermFromExperimentVariant( experimentAssignment?.variationName );

return {
isLoadingExperiment: isLoadingExperimentAssignment,
term: isLoadingExperimentAssignment ? undefined : term,
isEligibleForTermSavings: !! (
experimentAssignment?.variationName && experimentAssignment.variationName !== 'control'
Copy link
Contributor

@jeyip jeyip Nov 25, 2024

Choose a reason for hiding this comment

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

Wondering if I'm missing something, but if variationName returns null for the control, do we still check if experimentAssignment.variationName !== 'control' here?

Wouldn't just the experimentAssignment?.variationName check be enough?

Copy link
Contributor Author

@chriskmnds chriskmnds Nov 25, 2024

Choose a reason for hiding this comment

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

do we still check if experimentAssignment.variationName !== 'control' here?

We don't (probably?). I kept it out of reaction for the most part. was a bit surprised how control is treated as a non-entity/null variation. Almost like a type-safe system is missing internally. I guess no harm in keeping the extra check?

Copy link
Contributor

Choose a reason for hiding this comment

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

I kept it out of reaction for the most part. was a bit surprised how control is treated as a non-entity/null variation.

Sounds good 🙂

),
};
};

export default useLongerPlanTermDefaultExperiment;
34 changes: 20 additions & 14 deletions client/my-sites/plans-features-main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import ComparisonGridToggle from './components/comparison-grid-toggle';
import PlanUpsellModal from './components/plan-upsell-modal';
import { useModalResolutionCallback } from './components/plan-upsell-modal/hooks/use-modal-resolution-callback';
import PlansPageSubheader from './components/plans-page-subheader';
import useLongerPlanTermDefaultExperiment from './hooks/experiments/use-longer-plan-term-default-experiment';
import useCheckPlanAvailabilityForPurchase from './hooks/use-check-plan-availability-for-purchase';
import useDefaultWpcomPlansIntent from './hooks/use-default-wpcom-plans-intent';
import useFilteredDisplayedIntervals from './hooks/use-filtered-displayed-intervals';
Expand Down Expand Up @@ -240,6 +241,8 @@ const PlansFeaturesMain = ( {
const showUpgradeableStorage = config.isEnabled( 'plans/upgradeable-storage' );
const getPlanTypeDestination = usePlanTypeDestinationCallback();

const longerPlanTermDefaultExperiment = useLongerPlanTermDefaultExperiment();

const resolveModal = useModalResolutionCallback( {
isCustomDomainAllowedOnFreePlan,
flowName,
Expand Down Expand Up @@ -282,6 +285,13 @@ const PlansFeaturesMain = ( {
! isPersonalPlan( selectedPlan ) &&
( 'interval' === planTypeSelector || ! previousRoute.startsWith( '/plans/' ) );

const filteredDisplayedIntervals = useFilteredDisplayedIntervals( {
productSlug: currentPlan?.productSlug,
displayedIntervals,
flowName,
paidDomainName,
} );

const term = usePlanBillingPeriod( {
intervalType,
...( selectedPlan ? { defaultValue: getPlan( selectedPlan )?.term } : {} ),
Expand Down Expand Up @@ -457,13 +467,6 @@ const PlansFeaturesMain = ( {
_customerType = 'business';
}

const filteredDisplayedIntervals = useFilteredDisplayedIntervals( {
productSlug: currentPlan?.productSlug,
displayedIntervals,
flowName,
paidDomainName,
} );

const planTypeSelectorProps = useMemo( () => {
const props = {
basePlansPath,
Expand Down Expand Up @@ -632,7 +635,8 @@ const PlansFeaturesMain = ( {
! intent ||
! defaultWpcomPlansIntent || // this may be unnecessary, but just in case
! gridPlansForFeaturesGrid ||
! gridPlansForComparisonGrid
! gridPlansForComparisonGrid ||
longerPlanTermDefaultExperiment.isLoadingExperiment
);

const isPlansGridReady = ! isLoadingGridPlans && ! resolvedSubdomainName.isLoading;
Expand Down Expand Up @@ -838,9 +842,10 @@ const PlansFeaturesMain = ( {
enableReducedFeatureGroupSpacing={ showSimplifiedFeatures }
enableLogosOnlyForEnterprisePlan={ showSimplifiedFeatures }
hideFeatureGroupTitles={ showSimplifiedFeatures }
enableTermSavingsPriceDisplay={ isEnabled(
'plans/term-savings-price-display'
) }
enableTermSavingsPriceDisplay={
isEnabled( 'plans/term-savings-price-display' ) ||
longerPlanTermDefaultExperiment.isEligibleForTermSavings
}
/>
) }
{ showEscapeHatch && hidePlansFeatureComparison && viewAllPlansButton }
Expand Down Expand Up @@ -900,9 +905,10 @@ const PlansFeaturesMain = ( {
}
enableFeatureTooltips
featureGroupMap={ featureGroupMapForComparisonGrid }
enableTermSavingsPriceDisplay={ isEnabled(
'plans/term-savings-price-display'
) }
enableTermSavingsPriceDisplay={
isEnabled( 'plans/term-savings-price-display' ) ||
longerPlanTermDefaultExperiment.isEligibleForTermSavings
}
/>
) }
<ComparisonGridToggle
Expand Down
4 changes: 4 additions & 0 deletions client/my-sites/plans-features-main/test/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ jest.mock( '@automattic/data-stores', () => ( {
jest.mock( 'calypso/components/data/query-active-promotions', () => jest.fn() );
jest.mock( 'calypso/components/data/query-products-list', () => jest.fn() );

jest.mock( '../hooks/experiments/use-longer-plan-term-default-experiment', () => () => ( {
isLoadingExperiment: false,
} ) );

import {
PLAN_FREE,
PLAN_BUSINESS_MONTHLY,
Expand Down
2 changes: 1 addition & 1 deletion client/my-sites/plans/controller.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { productSelect } from 'calypso/my-sites/plans/jetpack-plans/controller';
import setJetpackPlansHeader from 'calypso/my-sites/plans/jetpack-plans/plans-header';
import isSiteWpcom from 'calypso/state/selectors/is-site-wpcom';
import { getSelectedSite, getSelectedSiteId } from 'calypso/state/ui/selectors';
import Plans from './plans';
import Plans from './main';

function showJetpackPlans( context ) {
const state = context.store.getState();
Expand Down
28 changes: 27 additions & 1 deletion client/my-sites/plans/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
PLAN_WOOEXPRESS_MEDIUM_MONTHLY,
PLAN_WOOEXPRESS_SMALL,
PLAN_WOOEXPRESS_SMALL_MONTHLY,
getBillingMonthsForTerm,
URL_FRIENDLY_TERMS_MAPPING,
} from '@automattic/calypso-products';
import page from '@automattic/calypso-router';
import { WpcomPlansUI, Plans } from '@automattic/data-stores';
Expand Down Expand Up @@ -39,10 +41,12 @@ import { PerformanceTrackerStop } from 'calypso/lib/performance-tracking';
import PlansNavigation from 'calypso/my-sites/plans/navigation';
import P2PlansMain from 'calypso/my-sites/plans/p2-plans-main';
import PlansFeaturesMain from 'calypso/my-sites/plans-features-main';
import useLongerPlanTermDefaultExperiment from 'calypso/my-sites/plans-features-main/hooks/experiments/use-longer-plan-term-default-experiment';
import { useSelector } from 'calypso/state';
import { getByPurchaseId } from 'calypso/state/purchases/selectors';
import { canCurrentUser } from 'calypso/state/selectors/can-current-user';
import getCurrentLocaleSlug from 'calypso/state/selectors/get-current-locale-slug';
import getCurrentPlanTerm from 'calypso/state/selectors/get-current-plan-term';
import getCurrentQueryArguments from 'calypso/state/selectors/get-current-query-arguments';
import getDomainFromHomeUpsellInQuery from 'calypso/state/selectors/get-domain-from-home-upsell-in-query';
import isEligibleForWpComMonthlyPlan from 'calypso/state/selectors/is-eligible-for-wpcom-monthly-plan';
Expand Down Expand Up @@ -548,12 +552,34 @@ const ConnectedPlans = connect(
)( withCartKey( withShoppingCart( localize( PlansComponent ) ) ) );

export default function PlansWrapper( props ) {
const { intervalType: intervalTypeFromProps } = props;
const selectedSiteId = useSelector( getSelectedSiteId );
const currentPlan = Plans.useCurrentPlan( { siteId: selectedSiteId } );
const longerPlanTermDefaultExperiment = useLongerPlanTermDefaultExperiment();
/**
* For WP.com plans page, if intervalType is not explicitly specified in the URL,
* we want to show plans of the same term as plan that is currently active
* We want to show the highest term between the current plan and the longer plan term default experiment
*/
const currentPlanTerm = useSelector( ( state ) =>
getIntervalTypeForTerm( getCurrentPlanTerm( state, selectedSiteId ) )
);
const intervalType =
longerPlanTermDefaultExperiment.term &&
currentPlanTerm &&
getBillingMonthsForTerm( URL_FRIENDLY_TERMS_MAPPING[ currentPlanTerm ] ) >
getBillingMonthsForTerm( URL_FRIENDLY_TERMS_MAPPING[ longerPlanTermDefaultExperiment.term ] )
? currentPlanTerm
: longerPlanTermDefaultExperiment.term;

return (
<CalypsoShoppingCartProvider>
<ConnectedPlans { ...props } currentPlan={ currentPlan } selectedSiteId={ selectedSiteId } />
<ConnectedPlans
{ ...props }
currentPlan={ currentPlan }
selectedSiteId={ selectedSiteId }
intervalType={ intervalTypeFromProps ?? intervalType ?? currentPlanTerm }
/>
</CalypsoShoppingCartProvider>
);
}
18 changes: 0 additions & 18 deletions client/my-sites/plans/plans.js

This file was deleted.

21 changes: 19 additions & 2 deletions client/signup/steps/plans/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { triggerGuidesForStep } from 'calypso/lib/guides/trigger-guides-for-step
import { buildUpgradeFunction } from 'calypso/lib/signup/step-actions';
import { getSegmentedIntent } from 'calypso/my-sites/plans/utils/get-segmented-intent';
import PlansFeaturesMain from 'calypso/my-sites/plans-features-main';
import useLongerPlanTermDefaultExperiment from 'calypso/my-sites/plans-features-main/hooks/experiments/use-longer-plan-term-default-experiment';
import { getStepUrl } from 'calypso/signup/utils';
import { getDomainFromUrl } from 'calypso/site-profiler/utils/get-valid-url';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
Expand Down Expand Up @@ -114,9 +115,17 @@ export class PlansStep extends Component {
initialContext,
intervalType,
isDomainOnlySite,
longerPlanTermDefaultExperiment,
} = this.props;

const intervalTypeValue = intervalType || getIntervalType( this.props.path );
const intervalTypeValue =
intervalType ||
getIntervalType(
this.props.path,
flowName === 'onboarding' && longerPlanTermDefaultExperiment.term
? longerPlanTermDefaultExperiment.term
: undefined
);

let errorDisplay;

Expand Down Expand Up @@ -431,6 +440,14 @@ export const isDotBlogDomainRegistration = ( domainItem ) => {
return is_domain_registration && getTld( meta ) === 'blog';
};

const WrappedPlansStep = ( props ) => {
const longerPlanTermDefaultExperiment = useLongerPlanTermDefaultExperiment();

return (
<PlansStep { ...props } longerPlanTermDefaultExperiment={ longerPlanTermDefaultExperiment } />
);
};

export default connect(
( state, { path, signupDependencies: { siteSlug, siteId, domainItem } } ) => ( {
// Blogger plan is only available if user chose either a free domain or a .blog domain registration
Expand All @@ -446,4 +463,4 @@ export default connect(
hasInitializedSitesBackUrl: getCurrentUserSiteCount( state ) ? '/sites/' : false,
} ),
{ recordTracksEvent, saveSignupStep, submitSignupStep, errorNotice }
)( localize( PlansStep ) );
)( localize( WrappedPlansStep ) );
9 changes: 6 additions & 3 deletions client/signup/steps/plans/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ const supportedIntervalTypes: SupportedIntervalTypes[] = [
'3yearly',
];

export const getIntervalType = ( path?: string ): SupportedIntervalTypes => {
export const getIntervalType = (
path?: string,
defaultType = 'yearly'
): SupportedIntervalTypes => {
const url = path ?? window?.location?.href ?? '';
const intervalType = getUrlParts( url ).searchParams.get( 'intervalType' ) || 'yearly';
const intervalType = getUrlParts( url ).searchParams.get( 'intervalType' ) || defaultType;

return (
supportedIntervalTypes.includes( intervalType as SupportedIntervalTypes )
? intervalType
: 'yearly'
: defaultType
) as SupportedIntervalTypes;
};

Expand Down
Loading