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

Storage Add-ons: Expose add-on upsells to plans page #83005

Merged
merged 14 commits into from
Oct 27, 2023

Conversation

jeyip
Copy link
Contributor

@jeyip jeyip commented Oct 13, 2023

Related to https://github.com/Automattic/martech/issues/2023

Proposed Changes

  • Add storage add-on upsells to business and WooCommerce plans in the /plans page
  • Add storage add-on upsell to spotlight card if business or WooCommerce plan
  • Don't show add-on price if the storage add-on has already been purchased
  • Don't show add-on prices if the user can no longer purchase storage add-ons given the 150GB max site storage limits

Important:

  • Currently, the user can purchase a 50GB storage upgrade add-on for a total of 100GB site storage. They can, alternatively, purchase a 100GB storage upgrade for 150GB total site storage. Users cannot, however, purchase both, for a total of 200GB site storage. p1694555758071819/1694555365.713609-slack-CFFF01Q4V
  • Storage add-on upsell UI is stale if revisiting the /plans page after recently purchasing a storage add-on. Normally the storage add-on dropdown will default to the most recently purchased plan. For a few minutes after a recent purchase though, it will not. This behavior is what we see in the /add-ons page as well.

GIFs

2023-10-24 10 29 58

2023-10-26 01 08 30

Testing Instructions

  • Navigate to /plans/{SITE_SLUG}
  • Verify that storage add-on upsell dropdowns are displayed for business and WooCommerce plans
  • Verify that, when a storage add-on upsell is selected, plan header prices and billing timeframes update accordingly
  • Verify that storage add-ons upgrades are added to the checkout cart as expected
  • Repeat the same verifications when the business or WooCommerce plan are in the spotlight card
  • Verify that, when a storage add-on is purchased, it is rendered in the dropdown as the default on initial page load ( for recently purchased add-ons, stale data may be presented for a minute or two until updated. This same behavior is seen in the /add-ons page )
  • Verify that, after a storage add-on has been purchased, selecting a lower tier storage add-on option disables the plan's CTA and asks the user to contact support

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md 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 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-ajp-p2)?

@jeyip jeyip self-assigned this Oct 13, 2023
@github-actions
Copy link

github-actions bot commented Oct 13, 2023

@matticbot
Copy link
Contributor

matticbot commented Oct 13, 2023

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

Sections (~498 bytes added 📈 [gzipped])

name                  parsed_size           gzip_size
update-design-flow        +1747 B  (+0.1%)     +412 B  (+0.1%)
plugins                   +1747 B  (+0.1%)     +412 B  (+0.1%)
plans                     +1747 B  (+0.1%)     +412 B  (+0.1%)
link-in-bio-tld-flow      +1747 B  (+0.1%)     +412 B  (+0.1%)
jetpack-app               +1747 B  (+0.4%)     +412 B  (+0.3%)
add-ons                    +479 B  (+0.1%)      +86 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 (~555 bytes added 📈 [gzipped])

name                                             parsed_size           gzip_size
async-load-signup-steps-plans-theme-preselected      +1747 B  (+0.4%)     +412 B  (+0.3%)
async-load-signup-steps-plans                        +1747 B  (+0.4%)     +412 B  (+0.3%)
async-load-signup-steps-add-ons                       +562 B  (+0.4%)     +143 B  (+0.3%)

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 jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from 22ceae6 to 9bf387f Compare October 22, 2023 20:40
@jeyip
Copy link
Contributor Author

jeyip commented Oct 23, 2023

This PR is almost ready for review -- I'm still wrangling with some odd caching logic 🤔

@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch 5 times, most recently from c93a5a5 to f405ccf Compare October 24, 2023 17:32
@@ -10,20 +11,23 @@ import { getSelectedSite } from 'calypso/state/ui/selectors';

const useAddOnCheckoutLink = (): ( ( addOnSlug: string, quantity?: number ) => string ) => {
const selectedSite = useSelector( getSelectedSite );
const checkoutLinkCallback = useCallback(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only change made in this file is the usage of useCallback to align with the 2023 plans-grid optimization efforts p1697705360647249/1697700875.046909-slack-CV2TX2PAN

Copy link
Contributor

@chriskmnds chriskmnds Oct 24, 2023

Choose a reason for hiding this comment

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

Hmm I don't think this needs to return a callback at all. We should probably instead be passing in selectedSiteSlug and return the checkout link directly from the hook. #83005 (comment)

buttonText = translate( 'Get Essential', { textOnly: true } );
}
return renderedGridPlans.map(
( { planSlug, availableForPurchase, features: { storageOptions } } ) => {
Copy link
Contributor Author

@jeyip jeyip Oct 24, 2023

Choose a reason for hiding this comment

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

Note:

Changes for L368-420 are caused by auto-formatting. The only meaningful update here is that we destructure the storageOptions parameter and pass it to PlanFeatures2023GridActions

@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from a5e2c92 to 8a70647 Compare October 24, 2023 18:27
@jeyip jeyip marked this pull request as ready for review October 24, 2023 19:32
@jeyip jeyip requested a review from a team as a code owner October 24, 2023 19:32
@jeyip jeyip requested a review from aneeshd16 October 24, 2023 19:32
@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 Oct 24, 2023
@@ -25,6 +25,7 @@ import {
isEcommercePlan,
TYPE_P2_PLUS,
} from '@automattic/calypso-products';
import useAddOnCheckoutLink from 'calypso/my-sites/add-ons/hooks/use-add-on-checkout-link';
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a local dependency we are introducing here. Let's either move it to a respective package and import from there or surface it differently into this hook :-)

Copy link
Contributor

Choose a reason for hiding this comment

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

p.s. looking at the code for useAddOnCheckoutLink, it should be easy to just copy over into a respective package by passing siteId in the props.

If that's the direction, would that be packages/data-stores/add-ons, or is it packages/add-ons?

Copy link
Contributor Author

@jeyip jeyip Oct 24, 2023

Choose a reason for hiding this comment

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

Ah thanks for catching this @chriskmnds! My mistake 🙏 I've been digging into the weeds with this PR and was planning to pass it into use-grid-plans as a callback, but it totally slipped my mind

Copy link
Contributor

@chriskmnds chriskmnds Oct 24, 2023

Choose a reason for hiding this comment

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

OK. Please read my other comments too. I don't think we should be passing this through as a callback. It complicates things further, and there seems to be a cleaner approach to putting these together.

From the looks of it, it sounds like we can instead compile the storage add-ons with a checkout URL from the existing useStorageAddOns hook. In which case, it becomes less of an immediate concern to migrate or refactor the useAddOnCheckoutLink hook.

IF it makes sense. Otherwise, I think moving useAddOnCheckoutLink to the respective add-ons package might be better than passing in a callback here 🤔

Copy link
Contributor Author

@jeyip jeyip Oct 24, 2023

Choose a reason for hiding this comment

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

OK. Please read my other comments too.

Yeah I'm seeing the other comments now, and they make sense. 🙂

I'll hold off on responding to individual comments for the time being until I've read everything as a whole

Copy link
Contributor Author

@jeyip jeyip Oct 25, 2023

Choose a reason for hiding this comment

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

From the looks of it, it sounds like we can instead compile the storage add-ons with a checkout URL from the existing useStorageAddOns hook. In which case, it becomes less of an immediate concern to migrate or refactor the useAddOnCheckoutLink hook.

IF it makes sense. Otherwise, I think moving useAddOnCheckoutLink to the respective add-ons package might be better than passing in a callback here 🤔

I'll move forward with both of these

I like the idea of generating storage add-ons with their respective checkout links for developer convenience, especially because the requisite change is simple and it works well for both existing UI consumers in /add-ons and /plans. It also seems like it will maintain a cleaner separation between the plans-grid and storage add-ons

Beyond that though, moving useAddOnCheckoutLink also looks like an easy task and gets us one step closer to having add-ons in a self-contained package. It's as good a reason as any to kick off the our migration, so I'll opt to handle that in an immediate follow-up

Edit:

I add checkout links to each storage add-on in this commit 5ca53a6

Edit 2:
Created a PR to migrate the checkout link hook to data-stores/add-ons #83489

@@ -651,6 +657,7 @@ const PlansFeaturesMain = ( {
<QuerySites siteId={ siteId } />
<QuerySitePlans siteId={ siteId } />
<QueryActivePromotions />
<QueryProductsList />
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note:

<QueryProductList /> is necessary to generate add-on product data on the /plans page

Copy link
Contributor

@chriskmnds chriskmnds left a comment

Choose a reason for hiding this comment

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

Thanks @jeyip for the work. I left some comments that I believe need addressing. Let me know if I can provide more direction.

Especially on introducing new dependencies into the folder (more so into "npm-ready" parts).

@@ -10,20 +11,23 @@ import { getSelectedSite } from 'calypso/state/ui/selectors';

const useAddOnCheckoutLink = (): ( ( addOnSlug: string, quantity?: number ) => string ) => {
const selectedSite = useSelector( getSelectedSite );
const checkoutLinkCallback = useCallback(
Copy link
Contributor

@chriskmnds chriskmnds Oct 24, 2023

Choose a reason for hiding this comment

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

Hmm I don't think this needs to return a callback at all. We should probably instead be passing in selectedSiteSlug and return the checkout link directly from the hook. #83005 (comment)

storageOptions?: StorageOption[];
};

export default function useDefaultStorageOption( { storageOptions, storageAddOnsForPlan }: Props ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add a description here.

I haven't grasped entirely the context here, but I presume this is necessary to live under plans-grid, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a short description to the hook ac5daa4

@@ -17,5 +17,4 @@ export const isStorageUpgradeableForPlan = ( {
storageOptions.length > 1 &&
intervalType === 'yearly' &&
showUpgradeableStorage &&
isInSignup &&
flowName === 'onboarding';
( flowName === 'onboarding' || ! isInSignup );
Copy link
Contributor

Choose a reason for hiding this comment

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

I wish we could get rid of flowName from here, along with isInSignup (as a general approach to start removing context from plans-grid). Can we maybe compile these into a property and pass into the grid? 🤔

Something for a follow-up perhaps.

Copy link
Contributor Author

@jeyip jeyip Oct 24, 2023

Choose a reason for hiding this comment

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

as a general approach to start removing context from plans-grid

Gotcha 👍

Yeah I'm happy to consolidate these properties. For a bit more context, isInSignup and flowName are temporary and will disappear entirely from this condition once we release add-on upsells to the rest of the onboarding flows.

Edit:

Will handle this in a follow-up :D

Copy link
Contributor

@chriskmnds chriskmnds left a comment

Choose a reason for hiding this comment

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

Thanks @jeyip for the work. I left some comments that I believe need addressing. Let me know if I can provide more direction.

Especially on introducing new dependencies into the folder (more so into "npm-ready" parts).

(oops, looks like I clicked on "approve changes" previously 😬 )

@jeyip
Copy link
Contributor Author

jeyip commented Oct 24, 2023

Thanks @jeyip for the work. I left some comments that I believe need addressing. Let me know if I can provide more direction.

Thanks for the speedy review @chriskmnds!

Reading and responding to your comments now 😎 I'm also going to take a beat, step away for a bit, and re-review my code in an hour or two to make sure I haven't missed anything else. I think I've tunnel visioned a bit while working on this PR 😵‍💫

@chriskmnds
Copy link
Contributor

chriskmnds commented Oct 24, 2023

Thanks @jeyip for the work. I left some comments that I believe need addressing. Let me know if I can provide more direction.

Thanks for the speedy review @chriskmnds!

Reading and responding to your comments now 😎 I'm also going to take a beat, step away for a bit, and re-review my code in an hour or two to make sure I haven't missed anything else. I think I've tunnel visioned a bit while working on this PR 😵‍💫

Thanks. My comments may feel a bit "all over the place". I kinda formed my thinking through commenting :D
Hopefully, this last comment puts most things together though: #83005 (comment)

Happy to take a look when back on Thursday/Friday or bounce more ideas.

@jeyip
Copy link
Contributor Author

jeyip commented Oct 24, 2023

I kinda formed my thinking through commenting :D
Hopefully, this last comment puts most things together though: #83005 (comment)

No worries about forming thoughts through comments -- It's all very helpful! And again, thanks for the speedy review. Yeah I'm noticing context from some later comments informing the discussion in earlier comments, so I'll take a look at all of your feedback as a whole first instead of responding to each idea individually.

Happy to take a look when back on Thursday/Friday or bounce more ideas.

Sounds great! Enjoy your AFK :D

Edit:

Moved this PR into "Needs Author Reply" and working on the suggested changes shortly

@chriskmnds chriskmnds dismissed their stale review October 24, 2023 22:02

nothing critical. we've discussed the important bit about the dependency. the rest I think are down to taste/cleanup (I got off the wrong assumption at one point at least :)

@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from 5ca53a6 to 3e5c14e Compare October 25, 2023 20:13
@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from d692b23 to 96b3ae1 Compare October 26, 2023 00:57
Copy link
Contributor

@chriskmnds chriskmnds left a comment

Choose a reason for hiding this comment

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

Tests out as outlined. I think quite impressive how nicely aligned everything is with the NPM migration work. also admire your approach and attitude toward comments and follow-ups! :D

A couple of things I notice, probably unrelated to this PR:

  • When I change between monthly/yearly plans when visiting with a free site, it clears the previous selections: so going into "yearly" tab, selecting a storage add-on, then clicking "monthly" in the toggle, then back to "yearly", the selections get cleared (and prices might flicker on a slower browser between previously updated/higher price and the default).

  • Verify that, when a storage add-on is purchased, it is rendered in the dropdown as the default on initial page load ( for recently purchased add-ons, stale data may be presented for a minute or two until updated. This same behavior is seen in the /add-ons page )

    So I confirm this, but also notice this feels a bit unclear to me. I upgraded a site from Personal to Business and purchased an additional +50GB space (so made the 100GB purchase). Now the dropdown always shows 100 as the default selected value in the Spotlight plan, which feels a little weird. I would expect the two (current plan's value and upgrades) to be separated a little better in the Spotlight plan i.e. it is not clear what that 100 refers to (especially since now my "plan" has a 100GB storage). Probably just a consequence to the two concepts being separated. 🤷‍♂️

    Screenshot 2023-10-26 at 1 32 42 PM
  • Verify that, after a storage add-on has been purchased, selecting a lower tier storage add-on option disables the plan's CTA and asks the user to contact support

    Confirmed, but similar impressions to the above. It feels a little cryptic/hidden maybe.

  • on checkout, if I change my selection to 2 years for the plan, the storage add-on stays for a single year. Not sure if that's confusing. It might make sense is the user could change the storage add-on's periodicity at the same time too.

    Screenshot 2023-10-26 at 1 28 28 PM

client/my-sites/plans-grid/components/actions.tsx Outdated Show resolved Hide resolved
if ( freePlan ) {
if (
freePlan ||
( storageAddOnsForPlan && ! canPurchaseStorageAddOns && nonDefaultStorageOptionSelected )
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a thought. Since we use this condition (canPurchaseStorageAddOns && nonDefaultStorageOptionSelected) at multiple places, would it make sense to combine into one? Not sure how we'd name that though :D

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey apologies if I'm not seeing it, but one of the conditions is canPurchaseStorageAddOns && nonDefaultStorageOptionSelected and the other is ! canPurchaseStorageAddOns && nonDefaultStorageOptionSelected.

I'm not seeing how they can be combined 😅

@jeyip
Copy link
Contributor Author

jeyip commented Oct 26, 2023

I think quite impressive how nicely aligned everything is with the NPM migration work. also admire your approach and attitude toward comments and follow-ups! :D

Recent work on storage add-ons has definitely felt more and more aligned with your 2023 plans pricing page work thanks to your help 😎 I really appreciate the feedback you've been providing

When I change between monthly/yearly plans when visiting with a free site, it clears the previous selections: so going into "yearly" tab, selecting a storage add-on, then clicking "monthly" in the toggle, then back to "yearly", the selections get cleared (and prices might flicker on a slower browser between previously updated/higher price and the default).

Good catch -- this is definitely worth a look 🤔

Edit: Fixed this in f467bd9

I would expect the two (current plan's value and upgrades) to be separated a little better in the Spotlight plan i.e. it is not clear what that 100 refers to (especially since now my "plan" has a 100GB storage).

Confirmed, but similar impressions to the above. It feels a little cryptic/hidden maybe.

This is a really interesting observation. I wonder if there's a way we can make it clearer to users that a given storage add-on option is what's currently applied to their site as opposed to an upgrade that can be purchased. Let me touch base with @vinimotaa and it's something that we can focus on in a follow-up

on checkout, if I change my selection to 2 years for the plan, the storage add-on stays for a single year. Not sure if that's confusing. It might make sense is the user could change the storage add-on's periodicity at the same time too.

Unfortunately only yearly terms were implemented for storage add-on upgrades https://github.com/Automattic/martech/issues/1907#issuecomment-1662901136, and if I recall correctly, it was for the sake of development velocity. The analogy given by Even was that of purchased domains alongside a 3-year plan, and the idea that said domain isn't registered for 3 years, but for 1 year. With that being said, I do agree that once we see how popular these add-ons might or might not be ( and how much further effort we'd like to invest), it would make a lot of sense to provide longer terms.

@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from 97f35d9 to 7a25662 Compare October 27, 2023 04:50
jeyip added 2 commits October 26, 2023 21:52
Add checkout functionality to spotlight plan

Fix plans features main tests

Fix type errors

Refactor grid plans and default storage options

Prevent purchased add-ons from being added to cart

Remove unnecessary usePastBillingTransactions hook

Add checkout link to storage add-ons data structure

Add description to use default storage option hook

Display prices for all add-on options
@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from 7a25662 to f467bd9 Compare October 27, 2023 04:52
@jeyip jeyip force-pushed the add/storage-add-on-upsells-to-plans-page branch from 4773d1a to 8ff5ed5 Compare October 27, 2023 23:22
@jeyip jeyip merged commit f307925 into trunk Oct 27, 2023
3 checks passed
@jeyip jeyip deleted the add/storage-add-on-upsells-to-plans-page branch October 27, 2023 23:45
@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 Oct 27, 2023
paulopmt1 pushed a commit that referenced this pull request Oct 30, 2023
* Expose add-on upsells to plans page

* Add selected storage add-on to checkout cart

* Add storage add-on upsells to spotlight plans

* Query product list on load

The product list is needed to derive information about storage add-ons on the /plans page

* Display purchased or default storage option on load
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