Skip to content

Commit 3d59836

Browse files
Experiment streamlining the plans grid + checkout: Update the plan interval dropdown (#103418)
* Restore wpcom plan variants as radio buttons * Show the radio buttons for the corresponding assignments * Restrict showing all variants to wpcom plans * Use generic params for styling the RadioButton * Show all product term variants * Add absolute discount function * Assign experiment styling to the corresponding variation * Use generic params for styling the Dropdown * Use optimistic UI for dropdown selection * Remove duplicate radio/dropdown treatment variation check * Refactor discount util function to support absolute values * Show the monthly savings for intro offers
1 parent 7ee6b85 commit 3d59836

File tree

10 files changed

+261
-51
lines changed

10 files changed

+261
-51
lines changed

client/my-sites/checkout/src/components/checkout-sidebar-plan-upsell/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { recordTracksEvent } from 'calypso/state/analytics/actions';
1818
import { useGetProductVariants } from '../../hooks/product-variants';
1919
import {
2020
getItemVariantCompareToPrice,
21-
getItemVariantDiscountPercentage,
21+
getItemVariantDiscount,
2222
} from '../item-variation-picker/util';
2323
import type { WPCOMProductVariant } from '../item-variation-picker';
2424
import './style.scss';
@@ -187,7 +187,7 @@ export function CheckoutSidebarPlanUpsell() {
187187
upsellVariant,
188188
currentVariant
189189
);
190-
const percentSavings = getItemVariantDiscountPercentage( upsellVariant, currentVariant );
190+
const percentSavings = getItemVariantDiscount( upsellVariant, currentVariant );
191191
if ( percentSavings === 0 ) {
192192
debug( 'percent savings is too low', percentSavings );
193193
return null;

client/my-sites/checkout/src/components/item-variation-picker/item-variation-dropdown.tsx

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@ import {
22
isJetpackPlan,
33
isJetpackProduct,
44
isMultiYearDomainProduct,
5+
isWpComPlan,
56
} from '@automattic/calypso-products';
67
import { Gridicon } from '@automattic/components';
78
import { useTranslate } from 'i18n-calypso';
8-
import { FunctionComponent, useCallback, useEffect, useState } from 'react';
9+
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react';
910
import isJetpackCheckout from 'calypso/lib/jetpack/is-jetpack-checkout';
11+
import {
12+
useStreamlinedPriceExperiment,
13+
isStreamlinedPriceDropdownTreatment,
14+
} from 'calypso/my-sites/plans-features-main/hooks/use-streamlined-price-experiment';
1015
import { JetpackItemVariantDropDownPrice } from './jetpack-variant-dropdown-price';
11-
import { CurrentOption, Dropdown, OptionList, Option } from './styles';
16+
import { CurrentOption, Dropdown, OptionList, Option, WPCheckoutCheckIcon } from './styles';
1217
import { ItemVariantDropDownPrice } from './variant-dropdown-price';
1318
import type { ItemVariationPickerProps, WPCOMProductVariant } from './types';
1419
import type { ResponseCartProduct } from '@automattic/shopping-cart';
@@ -27,15 +32,32 @@ export const ItemVariationDropDown: FunctionComponent< ItemVariationPickerProps
2732
} ) => {
2833
const translate = useTranslate();
2934
const [ highlightedVariantIndex, setHighlightedVariantIndex ] = useState< number | null >( null );
35+
const [ , streamlinedPriceExperimentAssignment ] = useStreamlinedPriceExperiment();
36+
const isStreamlinedPrice =
37+
isStreamlinedPriceDropdownTreatment( streamlinedPriceExperimentAssignment ) &&
38+
isWpComPlan( selectedItem.product_slug );
39+
40+
const [ optimisticSelectedItem, setOptimisticSelectedItem ] = useState< string | null >( null );
41+
42+
useEffect( () => {
43+
if ( isStreamlinedPrice ) {
44+
setOptimisticSelectedItem( selectedItem.product_slug );
45+
}
46+
}, [ selectedItem.product_slug, isStreamlinedPrice ] );
3047

3148
// Multi-year domain products must be compared by volume because they have the same product id.
32-
const selectedVariantIndexRaw = variants.findIndex( ( variant ) =>
33-
isMultiYearDomainProduct( selectedItem )
34-
? selectedItem.volume === variant.volume
35-
: selectedItem.product_id === variant.productId
36-
);
37-
// findIndex returns -1 if it fails and we want null.
38-
const selectedVariantIndex = selectedVariantIndexRaw > -1 ? selectedVariantIndexRaw : null;
49+
const selectedVariantIndex = useMemo( () => {
50+
const rawIndex = variants.findIndex( ( variant ) => {
51+
if ( isStreamlinedPrice && optimisticSelectedItem ) {
52+
return variant.productSlug === optimisticSelectedItem;
53+
}
54+
// If not optimistic, or optimisticSelectedItem is not set, use the original logic
55+
return isMultiYearDomainProduct( selectedItem )
56+
? selectedItem.volume === variant.volume && variant.productId === selectedItem.product_id
57+
: selectedItem.product_id === variant.productId;
58+
} );
59+
return rawIndex > -1 ? rawIndex : null;
60+
}, [ variants, isStreamlinedPrice, optimisticSelectedItem, selectedItem ] );
3961

4062
// reset the dropdown highlight when the selected product changes
4163
useEffect( () => {
@@ -45,10 +67,13 @@ export const ItemVariationDropDown: FunctionComponent< ItemVariationPickerProps
4567
// wrapper around onChangeItemVariant to close up dropdown on change
4668
const handleChange = useCallback(
4769
( uuid: string, productSlug: string, productId: number, volume?: number ) => {
70+
if ( isStreamlinedPrice ) {
71+
setOptimisticSelectedItem( productSlug );
72+
}
4873
onChangeItemVariant( uuid, productSlug, productId, volume );
4974
toggle( null );
5075
},
51-
[ onChangeItemVariant, toggle ]
76+
[ onChangeItemVariant, toggle, isStreamlinedPrice ]
5277
);
5378

5479
const selectNextVariant = useCallback( () => {
@@ -121,13 +146,22 @@ export const ItemVariationDropDown: FunctionComponent< ItemVariationPickerProps
121146
return null;
122147
}
123148

149+
let compareTo = undefined;
150+
if ( isStreamlinedPrice ) {
151+
compareTo = variants.find( ( variant ) => variant.termIntervalInMonths === 1 );
152+
}
124153
const ItemVariantDropDownPriceWrapper: FunctionComponent< { variant: WPCOMProductVariant } > = (
125154
props
126155
) =>
127156
isJetpack( props.variant ) ? (
128157
<JetpackItemVariantDropDownPrice { ...props } allVariants={ variants } />
129158
) : (
130-
<ItemVariantDropDownPrice { ...props } product={ selectedItem } />
159+
<ItemVariantDropDownPrice
160+
{ ...props }
161+
product={ selectedItem }
162+
isStreamlinedPrice={ isStreamlinedPrice }
163+
compareTo={ compareTo }
164+
/>
131165
);
132166

133167
return (
@@ -143,6 +177,7 @@ export const ItemVariationDropDown: FunctionComponent< ItemVariationPickerProps
143177
onClick={ () => toggle( id ) }
144178
open={ isOpen }
145179
role="button"
180+
detached={ isStreamlinedPrice }
146181
>
147182
{ selectedVariantIndex !== null ? (
148183
<ItemVariantDropDownPriceWrapper variant={ variants[ selectedVariantIndex ] } />
@@ -157,6 +192,7 @@ export const ItemVariationDropDown: FunctionComponent< ItemVariationPickerProps
157192
highlightedVariantIndex={ highlightedVariantIndex }
158193
selectedItem={ selectedItem }
159194
handleChange={ handleChange }
195+
isStreamlinedPrice={ isStreamlinedPrice }
160196
/>
161197
) }
162198
</Dropdown>
@@ -168,15 +204,19 @@ function ItemVariantOptionList( {
168204
highlightedVariantIndex,
169205
selectedItem,
170206
handleChange,
207+
isStreamlinedPrice,
171208
}: {
172209
variants: WPCOMProductVariant[];
173210
highlightedVariantIndex: number | null;
174211
selectedItem: ResponseCartProduct;
175212
handleChange: ( uuid: string, productSlug: string, productId: number, volume?: number ) => void;
213+
isStreamlinedPrice: boolean;
176214
} ) {
177-
const compareTo = variants.find( ( variant ) => variant.productId === selectedItem.product_id );
215+
const compareTo = isStreamlinedPrice
216+
? variants.find( ( variant ) => variant.termIntervalInMonths === 1 )
217+
: variants.find( ( variant ) => variant.productId === selectedItem.product_id );
178218
return (
179-
<OptionList role="listbox" tabIndex={ -1 }>
219+
<OptionList role="listbox" tabIndex={ -1 } detached={ isStreamlinedPrice }>
180220
{ variants.map( ( variant, index ) => (
181221
<ItemVariantOption
182222
key={ variant.productSlug + variant.variantLabel.noun }
@@ -193,6 +233,7 @@ function ItemVariantOptionList( {
193233
variant={ variant }
194234
allVariants={ variants }
195235
selectedItem={ selectedItem }
236+
isStreamlinedPrice={ isStreamlinedPrice }
196237
/>
197238
) ) }
198239
</OptionList>
@@ -206,13 +247,15 @@ function ItemVariantOption( {
206247
variant,
207248
allVariants,
208249
selectedItem,
250+
isStreamlinedPrice,
209251
}: {
210252
isSelected: boolean;
211253
onSelect: () => void;
212254
compareTo?: WPCOMProductVariant;
213255
variant: WPCOMProductVariant;
214256
allVariants: WPCOMProductVariant[];
215257
selectedItem: ResponseCartProduct;
258+
isStreamlinedPrice: boolean;
216259
} ) {
217260
const { variantLabel, productId, productSlug } = variant;
218261
return (
@@ -224,6 +267,7 @@ function ItemVariantOption( {
224267
role="option"
225268
onClick={ onSelect }
226269
selected={ isSelected }
270+
detached={ isStreamlinedPrice }
227271
>
228272
{ isJetpack( variant ) ? (
229273
<JetpackItemVariantDropDownPrice variant={ variant } allVariants={ allVariants } />
@@ -232,8 +276,10 @@ function ItemVariantOption( {
232276
variant={ variant }
233277
compareTo={ compareTo }
234278
product={ selectedItem }
279+
isStreamlinedPrice={ isStreamlinedPrice }
235280
/>
236281
) }
282+
{ isStreamlinedPrice && isSelected && <WPCheckoutCheckIcon /> }
237283
</Option>
238284
);
239285
}

client/my-sites/checkout/src/components/item-variation-picker/styles.tsx

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { css } from '@emotion/react';
22
import styled from '@emotion/styled';
3+
import { CheckIcon } from '../check-icon';
34
import { CurrentOptionProps, OptionProps } from './types';
45

56
export const CurrentOption = styled.button< CurrentOptionProps >`
@@ -21,6 +22,7 @@ export const CurrentOption = styled.button< CurrentOptionProps >`
2122
2223
${ ( props ) =>
2324
props.open &&
25+
! props.detached &&
2426
css`
2527
border-radius: 3px 3px 0 0;
2628
` }
@@ -39,14 +41,46 @@ export const Option = styled.li< OptionProps >`
3941
align-items: center;
4042
/* the calc aligns the price with the price in CurrentOption */
4143
padding: 10px calc( 14px + 24px + 16px ) 10px 16px;
44+
position: relative;
45+
46+
${ ( props ) =>
47+
props.detached &&
48+
css`
49+
padding-top: 14px;
50+
padding-bottom: 14px;
51+
` }
4252
4353
&:hover {
4454
background: var( --studio-wordpress-blue-5 );
4555
}
4656
47-
&.item-variant-option--selected {
48-
background: var( --studio-wordpress-blue-50 );
49-
color: #fff;
57+
${ ( props ) =>
58+
! props.detached
59+
? css`
60+
&.item-variant-option--selected {
61+
background: var( --studio-wordpress-blue-50 );
62+
color: #fff;
63+
}
64+
`
65+
: css`
66+
&.item-variant-option--selected * {
67+
color: var( --studio-black );
68+
}
69+
` }
70+
`;
71+
72+
export const WPCheckoutCheckIcon = styled( CheckIcon )`
73+
fill: ${ ( props ) => props.theme.colors.success };
74+
margin-right: 4px;
75+
position: absolute;
76+
top: 50%;
77+
right: 16px;
78+
transform: translateY( -50% );
79+
.rtl & {
80+
margin-right: 0;
81+
margin-left: 4px;
82+
right: auto;
83+
left: 16px;
5084
}
5185
`;
5286

@@ -60,12 +94,27 @@ export const Dropdown = styled.div`
6094
}
6195
`;
6296

63-
export const OptionList = styled.ul`
97+
export const OptionList = styled.ul< { detached: boolean } >`
6498
position: absolute;
6599
width: 100%;
66100
z-index: 4;
67101
margin: 0;
68102
box-shadow: rgba( 0, 0, 0, 0.16 ) 0px 1px 4px;
103+
${ ( props ) =>
104+
props.detached &&
105+
css`
106+
box-shadow:
107+
0px 50px 43px 0px rgba( 0, 0, 0, 0.02 ),
108+
0px 30px 36px 0px rgba( 0, 0, 0, 0.04 ),
109+
0px 15px 27px 0px rgba( 0, 0, 0, 0.07 ),
110+
0px 5px 15px 0px rgba( 0, 0, 0, 0.08 );
111+
margin-top: 10px;
112+
113+
${ Option }:first-of-type {
114+
border-top-left-radius: 3px;
115+
border-top-right-radius: 3px;
116+
}
117+
` }
69118
70119
${ Option } {
71120
margin-top: -1px;
@@ -100,6 +149,28 @@ export const Discount = styled.span`
100149
}
101150
`;
102151

152+
export const DiscountAbsolute = styled.span< {
153+
color: string;
154+
backgroundColor: string;
155+
} >`
156+
text-align: center;
157+
color: ${ ( props ) => props.color };
158+
display: block;
159+
background-color: ${ ( props ) => props.backgroundColor };
160+
padding: 0 10px;
161+
border-radius: 4px;
162+
font-size: 12px;
163+
line-height: 20px;
164+
.rtl & {
165+
margin-right: 0;
166+
margin-left: 8px;
167+
}
168+
// Keep selected state override for now, adjust if needed
169+
.item-variant-option--selected & {
170+
color: ${ ( props ) => props.color };
171+
}
172+
`;
173+
103174
export const DoNotPayThis = styled.del`
104175
text-decoration: line-through;
105176
margin-right: 8px;
@@ -173,4 +244,7 @@ export const IntroPricingText = styled.span`
173244
export const PriceTextContainer = styled.span`
174245
font-size: 14px;
175246
text-align: right;
247+
display: flex;
248+
flex-direction: column;
249+
align-items: flex-end;
176250
`;

client/my-sites/checkout/src/components/item-variation-picker/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ export type OnChangeItemVariant = (
4242

4343
export type CurrentOptionProps = {
4444
open: boolean;
45+
detached?: boolean;
4546
};
4647

4748
export type OptionProps = {
4849
selected: boolean;
50+
detached?: boolean;
4951
};

client/my-sites/checkout/src/components/item-variation-picker/util.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ export function getItemVariantCompareToPrice(
6161
return ( compareTo.priceInteger / compareTo.termIntervalInMonths ) * variant.termIntervalInMonths;
6262
}
6363

64-
export function getItemVariantDiscountPercentage(
64+
export function getItemVariantDiscount(
6565
variant: WPCOMProductVariant,
66-
compareTo?: WPCOMProductVariant
66+
compareTo?: WPCOMProductVariant,
67+
discountType: 'percentage' | 'absolute' = 'percentage'
6768
): number {
6869
const compareToPriceForVariantTerm = getItemVariantCompareToPrice( variant, compareTo );
6970

@@ -73,11 +74,19 @@ export function getItemVariantDiscountPercentage(
7374
? variant.priceBeforeDiscounts
7475
: variant.priceInteger;
7576

77+
if ( ! compareToPriceForVariantTerm ) {
78+
return 0;
79+
}
80+
81+
if ( discountType === 'absolute' ) {
82+
return compareToPriceForVariantTerm - variantPrice;
83+
}
84+
7685
// Extremely low "discounts" are possible if the price of the longer term has been rounded
7786
// if they cannot be rounded to at least a percentage point we should not show them.
78-
const discountPercentage = compareToPriceForVariantTerm
79-
? Math.round( 100 - ( variantPrice / compareToPriceForVariantTerm ) * 100 )
80-
: 0;
87+
const discountPercentage = Math.round(
88+
100 - ( variantPrice / compareToPriceForVariantTerm ) * 100
89+
);
8190

8291
return discountPercentage;
8392
}

0 commit comments

Comments
 (0)