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 (grid): Disable term-savings price display if another discount is active (intro/coupon/etc.) #96631

Merged

Conversation

chriskmnds
Copy link
Contributor

Related to https://github.com/Automattic/martech/issues/3403
Related to #96357
Branched from #96492

Proposed Changes

This is a follow-up to the term-savings price display (see core implementation: #96357) to disable the term-savings display if any plan, across any available/effective term for the rendered grid appears as discounted already.

See related discussion: pdvytD-Se-p2#comment-645. Implementation directly addresses comment pdvytD-Se-p2#comment-650:

  • a plan on a "one time discount" coupon will disable the term-savings
  • a plan on a "special offer" introductory offer will disable the term-savings
  • term-savings are disabled for all the plans, so switching between monthly/yearly/2yearly/3yearly will not resurface the term-savings, as long as any visible plan is discounted

Why are these changes being made?

Avoid confusion when plans are discounted while the term-savings display is active. Keep the focus on the effective/promoted discount across the user's interaction with the grid.

See related discussion: pdvytD-Se-p2#comment-645. Implementation directly addresses comment pdvytD-Se-p2#comment-650

Testing Instructions

TBD - WIP

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@chriskmnds chriskmnds self-assigned this Nov 21, 2024
@chriskmnds chriskmnds requested a review from jeyip November 21, 2024 16:06
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Nov 21, 2024
@chriskmnds chriskmnds force-pushed the upate/plans-grid-skip-term-savings-if-other-discount branch from 35896ac to f53e700 Compare November 22, 2024 09:43
@chriskmnds chriskmnds force-pushed the update/plans-grid-header-price-cleanup branch from f73ba06 to c8e0442 Compare November 22, 2024 10:40
@chriskmnds chriskmnds marked this pull request as ready for review November 22, 2024 16:07
@chriskmnds chriskmnds requested a review from a team as a code owner November 22, 2024 16:07
@chriskmnds
Copy link
Contributor Author

@jeyip This is a pretty insane change. If this is where we want things to head, then I'll continue pushing through more surrounding refactors. I think this is where we want things to head, conceptually.

cc @southp as something to keep an eye on. The primary consideration going forward should on maintaining the memoization of the affected hooks. Otherwise, it literally can be 4x the processing on any interaction (if not already - needs to be tested). 🤷

@chriskmnds
Copy link
Contributor Author

@jeyip This is a pretty insane change. If this is where we want things to head, then I'll continue pushing through more surrounding refactors. I think this is where we want things to head, conceptually.

cc @southp as something to keep an eye on. The primary consideration going forward should on maintaining the memoization of the affected hooks. Otherwise, it literally can be 4x the processing on any interaction (if not already - needs to be tested). 🤷

Well, I'm going to experiment with another approach. This feels a little over the line.

@jeyip
Copy link
Contributor

jeyip commented Nov 22, 2024

@jeyip This is a pretty insane change. If this is where we want things to head, then I'll continue pushing through more surrounding refactors. I think this is where we want things to head, conceptually.

Well, I'm going to experiment with another approach. This feels a little over the line.

Thanks for exploring this change. Pretty unfortunate that it gets crazy. I haven't reviewed the code yet, but I'll take a closer look before I sign off

@chriskmnds chriskmnds force-pushed the update/plans-grid-header-price-cleanup branch from c8e0442 to d196100 Compare November 25, 2024 09:55
Base automatically changed from update/plans-grid-header-price-cleanup to trunk November 25, 2024 10:13
@jeyip
Copy link
Contributor

jeyip commented Dec 2, 2024

Just wrapped up work on storage add-ons related to this experiment all ready for review ( Plans: Make storage selection reflection in header price optional, Plans: Move useStorageAddOns logic selection in pricing meta hook ). Starting a deep dive into this PR today, and alternative approaches we might consider, especially given your concerns voiced here

@jeyip jeyip force-pushed the upate/plans-grid-skip-term-savings-if-other-discount branch from b67e8f2 to 1fcb677 Compare December 2, 2024 21:28
@jeyip
Copy link
Contributor

jeyip commented Dec 2, 2024

While I recognize that we might abandon this PR, I rebased this branch with trunk and resolved merge conflicts to better understand the breadth of what we're updating. There were about 15 extra files conflated with our code changes, which have since been resolved and removed.

Things should be easier to wrap my head around now ( and also for anyone else that's following along ). I see that the builds are failing. Not sure why that is because I smoke tested behavior before pushing the rebase and things looked correct. Probably some .stories that haven't been updated, unit tests or types that need to be fixed, etc. I can take a closer look tomorrow 🙂

With Coupon ( No term savings shown )

Screenshot 2024-12-02 at 13 47 34

Without Coupon ( Term savings shown )

Screenshot 2024-12-02 at 13 47 14

Copy link
Contributor

@jeyip jeyip left a comment

Choose a reason for hiding this comment

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

Ah. After digging into the code a bit more, I understand the concern. Summarizing to make sure I understand correctly, but feel free to chime in with any important info I'm missing.

Whereas previously we were retrieving and joining plans data only for the term being rendered ( monthly, or annual, or 2year, etc. ), we are now doing so for all billing terms in the useGridPlans hooks. Besides the added code complexity, we're also seeing O(n) functions become O(n^2) here and here

We're doing so because we'd like to calculate whether or not special offers, coupons, or discounts are visible to any plan on any term, at which point we can decide whether or not we want to render term savings.

I'll start thinking about alternate approaches as well.

@chriskmnds
Copy link
Contributor Author

@jeyip Thanks for rebasing. I'll dive back into this and see what else I can come up.

Whereas previously we were retrieving and joining plans data only for the term being rendered ( monthly, or annual, or 2year, etc. ), we are now doing so for all billing terms in the useGridPlans hooks. Besides the added code complexity, we're also seeing O(n) functions become O(n^2) here and here

Your assessment is generally correct, although complexity is still linear, just O(n*4) (or however many terms we have). No quadratic or exponential stuff happening differently from before.

Having to loop through all the plans that we may "potentially" render is inevitable for the logic we want to introduce. That's essentially 1:1 to what we intend to achieve. The alternatives I'm thinking of are more about reducing how much we touch when doing so, creating additional structure from indexing on term, etc:

  1. Just use usePricingMetaForGridPlans for all the plans, skipping the remaining logic in useGridPlans, as this is strictly a pricing concern. Or look directly into the usePlans/useSitePlans hooks, which should already have data for all the plans.
  2. Update usePricingMetaForGridPlans to run over all the plans.

@chriskmnds chriskmnds force-pushed the upate/plans-grid-skip-term-savings-if-other-discount branch from 1fcb677 to 9512296 Compare December 3, 2024 19:02
Comment on lines 760 to 766
const usePlanSlugsForAllDisplayedIntervals = () => {
const currentPlanSlugs = gridPlansForFeaturesGrid?.map( ( { planSlug } ) => planSlug );

return currentPlanSlugs?.flatMap( ( planSlug ) =>
filteredDisplayedIntervals
.map( ( term ) =>
getPlanSlugForTermVariant( planSlug, URL_FRIENDLY_TERMS_MAPPING[ term ] )
)
.filter( ( planSlug ) => planSlug !== undefined )
);
};

const pricingForAllDisplayedIntervals = Plans.usePricingMetaForGridPlans( {
planSlugs: usePlanSlugsForAllDisplayedIntervals() ?? [],
storageAddOns,
coupon,
siteId,
useCheckPlanAvailabilityForPurchase,
} );

const enableTermSavingsPriceDisplay = useMemo( () => {
const isAnyGridPlanDiscounted = Object.values( pricingForAllDisplayedIntervals ?? {} ).reduce(
( isDiscounted, { discountedPrice, introOffer } ) => {
const hasDiscount =
'number' === typeof discountedPrice.monthly ||
( introOffer && ! introOffer.isOfferComplete );
return isDiscounted || !! hasDiscount;
},
false
);

if ( isAnyGridPlanDiscounted ) {
return false;
}

return (
( isEnabled( 'plans/term-savings-price-display' ) ||
longerPlanTermDefaultExperiment.isEligibleForTermSavings ) &&
isInSignup
);
}, [
pricingForAllDisplayedIntervals,
longerPlanTermDefaultExperiment.isEligibleForTermSavings,
isInSignup,
] );

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jeyip this is one of the alternatives I had in mind. Does it look more sane from previously?

Complexity is still of the same order (processing all plans), but a smaller surface area of the changes (not touching hooks, etc.). Next step would be to clean this up into a separate hook so it doesn't live like this in the component.

Copy link
Contributor

@jeyip jeyip Dec 4, 2024

Choose a reason for hiding this comment

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

this is one of the alternatives I had in mind. Does it look more sane from previously?

From where it was before, the smaller surface area is absolutely better 👍

Currently reading the code to gauge my understanding.

Copy link
Contributor

@jeyip jeyip Dec 4, 2024

Choose a reason for hiding this comment

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

Currently reading the code to gauge my understanding.

Yes this is significantly more easy to follow 🎉

Next step would be to clean this up into a separate hook so it doesn't live like this in the component.

Totally agree on encapsulating this complexity.

I suspect you might've already thought about this, but it also seems like an eventual refactor ( doesn't have to be done immediately imo ) would be for the new hook to handle isAnyPlanPriceDiscounted logic in the Header-Price component.

const termVariantPricing = Plans.usePricingMetaForGridPlans( {
planSlugs: termVariantPlanSlug ? [ termVariantPlanSlug ] : [],
storageAddOns,
coupon,
siteId,
useCheckPlanAvailabilityForPurchase: helpers?.useCheckPlanAvailabilityForPurchase,
} )?.[ termVariantPlanSlug ?? '' ];
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;
useEffect( () => {
if (
isGridPlanOneTimeDiscounted ||
isGridPlanOnIntroOffer ||
( enableTermSavingsPriceDisplay && savings )
) {
setIsAnyPlanPriceDiscounted( true );
}
}, [

I understand that they are distinct concepts, but it seems like we could generate info for isAnyPlanPriceDiscounted while we calculate isAnyGridPlanDiscounted. Curious about your thoughts though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok - so I moved it now to a separate hook.

I suspect you might've already thought about this, but it also seems like an eventual refactor ( doesn't have to be done immediately imo ) would be for the new hook to handle isAnyPlanPriceDiscounted logic in the Header-Price component.

Indeed, I had the same thinking. However, we can maybe excuse the two co-existing in that they serve different purposes - one being a header UI concern (that the header price has indeed rendered a discounted price), the other being a general flag into the system/app to enable/disable other things. I think a good refactor might be to turn it into a hook in the plans data-store. I will explore that in a follow-up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok - so I moved it now to a separate hook.

Wonderful! Now we can also explore any necessary performance optimizations in one place ( if necessary ).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Awesome. I'll create an issue for the above in a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

^ #97132

@matticbot
Copy link
Contributor

This PR modifies the release build for the following Calypso Apps:

For info about this notification, see here: PCYsg-OT6-p2

  • notifications
  • wpcom-block-editor

To test WordPress.com changes, run install-plugin.sh $pluginSlug upate/plans-grid-skip-term-savings-if-other-discount on your sandbox.

@matticbot
Copy link
Contributor

matticbot commented Dec 3, 2024

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

Sections (~155 bytes added 📈 [gzipped])

name                  parsed_size           gzip_size
update-design-flow         +474 B  (+0.0%)     +155 B  (+0.0%)
plugins                    +474 B  (+0.0%)     +155 B  (+0.0%)
plans                      +474 B  (+0.0%)     +155 B  (+0.0%)
link-in-bio-tld-flow       +474 B  (+0.0%)     +155 B  (+0.0%)
jetpack-app                +474 B  (+0.1%)     +155 B  (+0.1%)

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.

Async-loaded Components (~155 bytes added 📈 [gzipped])

name                                             parsed_size           gzip_size
async-load-signup-steps-plans-theme-preselected       +474 B  (+0.1%)     +155 B  (+0.1%)
async-load-signup-steps-plans                         +474 B  (+0.1%)     +155 B  (+0.1%)

React components that are loaded lazily, when a certain part of UI is displayed for the first time.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@jeyip
Copy link
Contributor

jeyip commented Dec 3, 2024

Hey @chriskmnds thanks for pushing changes. I'm being pulled away to take a look at #96936 (comment), but I'll take a look at this PR shortly

Edit: Okay taking a look now

@@ -754,6 +757,52 @@ const PlansFeaturesMain = ( {
</div>
);

const usePlanSlugsForAllDisplayedIntervals = () => {
Copy link
Contributor

@jeyip jeyip Dec 4, 2024

Choose a reason for hiding this comment

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

Non-blocking / Nitpick:

I'm seeing that we have two conventional ways of describing the length of a plan. Just to confirm my understanding, is the difference between "Displayed Interval" and "term" that the former describes a UI concern while the other describes a business concern?

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 think one refers to the intervals in the toggle and the URL parameters, the other to the plan term/period.

@chriskmnds chriskmnds force-pushed the upate/plans-grid-skip-term-savings-if-other-discount branch from 64d369f to 7de9c28 Compare December 4, 2024 14:01
@chriskmnds chriskmnds force-pushed the upate/plans-grid-skip-term-savings-if-other-discount branch from 770320d to 8010090 Compare December 4, 2024 14:19
@chriskmnds
Copy link
Contributor Author

@jeyip I think we are ready here to test/deploy. I just can't figure out the type error in the unit tests, where it comes from 🤔

@jeyip
Copy link
Contributor

jeyip commented Dec 4, 2024

It looks like f38a86a fixed it unless you're concerned about the as PlanSlug[] statement 👍

@jeyip
Copy link
Contributor

jeyip commented Dec 4, 2024

Testing this PR shortly

@jeyip
Copy link
Contributor

jeyip commented Dec 4, 2024

Testing

Requirements

One key concern for us was performance. I ran non-scientific performance evaluations with google chrome dev tools. Note that these metrics will be slower due to the nature of executing code simultaneously with chrome's inspector. Real world interaction times will generally be far below 2.5 seconds.

  • No CPU or network throttling
  • I used the mouse to switch from 1 year annual plans to 2 year plans in the term dropdown selector

Example chart from Trunk

Screenshot 2024-12-04 at 15 03 32

After three trials, scripting times were 2683 ms, 2623 ms, and 2553 ms, with an average of 2619ms.

Example Chart with this PR

Screenshot 2024-12-04 at 15 02 14

After three trials, scripting times were 2631 ms, 2808 ms, and 2616 ms, with an average of 2685ms.

What we're focused on is the scripting time in yellow, and there was a modest 2.5% increase on the average time to completion.

For each of the flows listed below:

  • Visit onboarding flow with query params ?flags=plans/term-savings-price-display. Ensure that term savings are displayed for 1y, 2y, and 3y plans.
  • Switch to a currency with special offers ( MXN, INR, PHP ). Return to flow with ?flags=plans/term-savings-price-display. Ensure that no term savings are displayed at all. Only currency specific special offers.
  • Visit onboarding flow with query params ?flags=plans/term-savings-price-display&coupon=federate25. Ensure that no term savings are displayed at all. Only the coupon savings. See here for visuals.

Flows:

  • /start
  • /setup/link-in-bio-tld

Browsers

  • Chrome

Notes

  • I'm purposefully not testing logged in plans page. We will no longer run this experiment in that view ( more details here )
  • I spotted a visual bug unrelated to this PR ( it's present in production ). For the currency based discount ( MXN, INR, PHP ), there's an excessive amount of space between the plan title and header pricing when switching between 1 year and multi-year term views. This does not happen if the page is initially rendered on a multi-year view.

2024-12-04 15 37 31

Copy link
Contributor

@jeyip jeyip left a comment

Choose a reason for hiding this comment

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

The term dropdown interactions still seem reasonably responsive, and the changes look good to me 👍 I found some strange behavior with currency based special offers mentioned in my testing notes, but that's also present in production.

@chriskmnds
Copy link
Contributor Author

Thanks @jeyip . I will do deploy now.

I found some strange behavior with currency-based special offers mentioned in my testing notes, but that's also present in production.

Can you port that comment into an issue so we can address it? Feel free to assign me to it. :-)

@chriskmnds chriskmnds merged commit 90dd8f2 into trunk Dec 5, 2024
11 checks passed
@chriskmnds chriskmnds deleted the upate/plans-grid-skip-term-savings-if-other-discount branch December 5, 2024 17:53
@github-actions github-actions bot removed the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Dec 5, 2024
@jeyip
Copy link
Contributor

jeyip commented Dec 5, 2024

Can you port that comment into an issue so we can address it? Feel free to assign me to it. :-)

Of course -- I'm on it 🙂

Edit: #97145

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants