From ae165bee68b0f62c7455d8fe0e43848c15d08cd1 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 13 Dec 2024 12:30:28 +0100 Subject: [PATCH 01/12] WIP - Refactor availability logic for storage add-ons --- .../add-ons/components/add-ons-card.tsx | 37 +++-------- .../add-ons/components/add-ons-grid.tsx | 9 +-- client/my-sites/add-ons/main.tsx | 4 +- .../sites/overview/components/plan-card.tsx | 64 ++++++++++-------- config/development.json | 1 - config/horizon.json | 1 - config/production.json | 1 - config/stage.json | 1 - config/test.json | 1 - config/wpcalypso.json | 1 - .../hooks/use-add-on-purchase-status.ts | 41 ++++++++++-- .../src/add-ons/hooks/use-add-ons.ts | 66 +------------------ .../add-ons/lib/is-storage-addon-enabled.ts | 9 --- 13 files changed, 82 insertions(+), 154 deletions(-) delete mode 100644 packages/data-stores/src/add-ons/lib/is-storage-addon-enabled.ts diff --git a/client/my-sites/add-ons/components/add-ons-card.tsx b/client/my-sites/add-ons/components/add-ons-card.tsx index 121454aaea0f7..93be0657f171e 100644 --- a/client/my-sites/add-ons/components/add-ons-card.tsx +++ b/client/my-sites/add-ons/components/add-ons-card.tsx @@ -1,5 +1,5 @@ -import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; import { Badge, Gridicon, Spinner } from '@automattic/components'; +import { useAddOnPurchaseStatus } from '@automattic/data-stores/src/add-ons'; import styled from '@emotion/styled'; import { Card, CardBody, CardFooter, CardHeader, Button } from '@wordpress/components'; import { Icon } from '@wordpress/icons'; @@ -17,16 +17,6 @@ export interface Props { text: string; handler: ( productSlug: string ) => void; }; - useAddOnAvailabilityStatus?: ( { - selectedSiteId, - addOnMeta, - }: { - selectedSiteId?: number | null | undefined; - addOnMeta: AddOnMeta; - } ) => { - available: boolean; - text?: string; - }; highlightFeatured: boolean; addOnMeta: AddOnMeta; } @@ -110,16 +100,10 @@ const Container = styled.div` } `; -const AddOnCard = ( { - addOnMeta, - actionPrimary, - actionSecondary, - useAddOnAvailabilityStatus, - highlightFeatured, -}: Props ) => { +const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatured }: Props ) => { const translate = useTranslate(); const selectedSiteId = useSelector( getSelectedSiteId ); - const availabilityStatus = useAddOnAvailabilityStatus?.( { selectedSiteId, addOnMeta } ); + const availabilityStatus = useAddOnPurchaseStatus( { selectedSiteId, addOnMeta } ); const onActionPrimary = () => { actionPrimary?.handler( addOnMeta.productSlug, addOnMeta.quantity ); @@ -129,17 +113,12 @@ const AddOnCard = ( { }; const shouldRenderLoadingState = addOnMeta.isLoading; + const shouldRenderPrimaryAction = availabilityStatus?.available && ! shouldRenderLoadingState; + const shouldRenderSecondaryAction = ! availabilityStatus?.available && ! shouldRenderLoadingState; - // if product is space upgrade choose the action based on the purchased status - const shouldRenderPrimaryAction = - addOnMeta.productSlug === PRODUCT_1GB_SPACE - ? ! addOnMeta.purchased && ! shouldRenderLoadingState - : availabilityStatus?.available && ! shouldRenderLoadingState; - - const shouldRenderSecondaryAction = - addOnMeta.productSlug === PRODUCT_1GB_SPACE - ? addOnMeta.purchased && ! shouldRenderLoadingState - : ! availabilityStatus?.available && ! shouldRenderLoadingState; + if ( availabilityStatus?.hidden ) { + return null; + } return ( diff --git a/client/my-sites/add-ons/components/add-ons-grid.tsx b/client/my-sites/add-ons/components/add-ons-grid.tsx index 6d2c6faf8b44f..294beb5da47fa 100644 --- a/client/my-sites/add-ons/components/add-ons-grid.tsx +++ b/client/my-sites/add-ons/components/add-ons-grid.tsx @@ -18,13 +18,7 @@ const Container = styled.div` } `; -const AddOnsGrid = ( { - addOns, - actionPrimary, - actionSecondary, - useAddOnAvailabilityStatus, - highlightFeatured, -}: Props ) => { +const AddOnsGrid = ( { addOns, actionPrimary, actionSecondary, highlightFeatured }: Props ) => { return ( { addOns.map( ( addOn ) => @@ -35,7 +29,6 @@ const AddOnsGrid = ( { } actionPrimary={ actionPrimary } actionSecondary={ actionSecondary } - useAddOnAvailabilityStatus={ useAddOnAvailabilityStatus } addOnMeta={ addOn } highlightFeatured={ highlightFeatured } /> diff --git a/client/my-sites/add-ons/main.tsx b/client/my-sites/add-ons/main.tsx index 3182a902c6564..75ca753a49259 100644 --- a/client/my-sites/add-ons/main.tsx +++ b/client/my-sites/add-ons/main.tsx @@ -92,7 +92,6 @@ const AddOnsMain = () => { const translate = useTranslate(); const selectedSite = useSelector( getSelectedSite ) ?? null; const addOns = AddOns.useAddOns( { selectedSiteId: selectedSite?.ID } ); - const filteredAddOns = addOns.filter( ( addOn ) => ! addOn?.exceedsSiteStorageLimits ); const checkoutLink = AddOns.useAddOnCheckoutLink(); @@ -131,8 +130,7 @@ const AddOnsMain = () => { diff --git a/client/sites/overview/components/plan-card.tsx b/client/sites/overview/components/plan-card.tsx index bd8218897b124..f0423de9c3647 100644 --- a/client/sites/overview/components/plan-card.tsx +++ b/client/sites/overview/components/plan-card.tsx @@ -1,16 +1,12 @@ -import { - getPlan, - PlanSlug, - PRODUCT_1GB_SPACE, - PLAN_MONTHLY_PERIOD, -} from '@automattic/calypso-products'; +import { getPlan, PlanSlug, PLAN_MONTHLY_PERIOD } from '@automattic/calypso-products'; import { Button, PlanPrice, LoadingPlaceholder, Badge } from '@automattic/components'; import { AddOns } from '@automattic/data-stores'; +import { ADD_ON_100GB_STORAGE, useAddOnPurchaseStatus } from '@automattic/data-stores/src/add-ons'; import { usePricingMetaForGridPlans } from '@automattic/data-stores/src/plans'; import { usePlanBillingDescription } from '@automattic/plans-grid-next'; import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; -import { PropsWithChildren, useState } from 'react'; +import { useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import PlanStorage, { useDisplayUpgradeLink } from 'calypso/blocks/plan-storage'; import QuerySitePlans from 'calypso/components/data/query-site-plans'; @@ -195,27 +191,36 @@ const PricingSection = () => { ); }; -function PlanStorageFooter( { children }: PropsWithChildren ) { +type NeedMoreStorageProps = { + addOn: AddOns.AddOnMeta; + noLink?: boolean; +}; + +function NeedMoreStorage( { addOn, noLink = false }: NeedMoreStorageProps ) { + const translate = useTranslate(); const site = useSelector( getSelectedSite ); const dispatch = useDispatch(); - const wrapperIsLink = useDisplayUpgradeLink( site?.ID ?? null ); + const availability = useAddOnPurchaseStatus( { addOnMeta: addOn, selectedSiteId: site?.ID } ); + const text = translate( 'Need more storage?' ); - if ( wrapperIsLink ) { - return
{ children }
; + if ( availability.hidden || ! availability.available ) { + return null; + } + + if ( noLink ) { + return text; } return ( -
- -
+ ); } @@ -244,11 +249,12 @@ const PlanCard = () => { const planPurchaseLoading = ! isFreePlan && planPurchase === null; const isLoading = ! planDetails || planPurchaseLoading; - // Check for storage addons available for purchase. + const footerWrapperIsLink = useDisplayUpgradeLink( site?.ID ?? null ); const addOns = AddOns.useAddOns( { selectedSiteId: site?.ID } ); - const storageAddons = addOns.filter( - ( addOn ) => addOn?.productSlug === PRODUCT_1GB_SPACE && ! addOn?.exceedsSiteStorageLimits - ); + // Storage add-ons can be upgraded (i.e., if you already have the 50GB add-on, you can upgrade + // to 100GB) but not downgraded. That's why we only check the availability of the largest + // storage add-on. + const bigStorageAddon = addOns.find( ( addOn ) => addOn?.addOnSlug === ADD_ON_100GB_STORAGE ); const renderManageButton = () => { if ( isJetpack || ! site || isStaging || isAgencyPurchase || isDevelopmentSite ) { @@ -333,8 +339,10 @@ const PlanCard = () => { siteId={ site?.ID } storageBarComponent={ PlanStorageBar } > - { storageAddons.length > 0 && ! isAgencyPurchase && ( - { translate( 'Need more storage?' ) } + { bigStorageAddon && ( +
+ +
) } diff --git a/config/development.json b/config/development.json index b8bb9f7f10c6a..7c5e81c82e32f 100644 --- a/config/development.json +++ b/config/development.json @@ -225,7 +225,6 @@ "stats/paid-wpcom-v2": true, "stats/paid-wpcom-v3": true, "stepper-woocommerce-poc": true, - "storage-addon": true, "subscriber-csv-upload": true, "subscriber-importer": true, "subscription-gifting": true, diff --git a/config/horizon.json b/config/horizon.json index 79c6fcc38d976..b67630ef09d98 100644 --- a/config/horizon.json +++ b/config/horizon.json @@ -147,7 +147,6 @@ "stats/new-date-filtering": true, "stats/paid-wpcom-v2": true, "stepper-woocommerce-poc": true, - "storage-addon": true, "subscriber-csv-upload": true, "subscriber-importer": true, "subscription-gifting": true, diff --git a/config/production.json b/config/production.json index 120bccb4c4698..c55f1e8ea5bbd 100644 --- a/config/production.json +++ b/config/production.json @@ -196,7 +196,6 @@ "stats/paid-wpcom-v2": true, "stats/paid-wpcom-v3": true, "stepper-woocommerce-poc": true, - "storage-addon": true, "subscriber-csv-upload": true, "subscriber-importer": true, "subscription-gifting": true, diff --git a/config/stage.json b/config/stage.json index 5a625607cc26a..bf008b5d037cb 100644 --- a/config/stage.json +++ b/config/stage.json @@ -192,7 +192,6 @@ "stats/paid-wpcom-v2": true, "stats/paid-wpcom-v3": true, "stepper-woocommerce-poc": true, - "storage-addon": true, "subscriber-csv-upload": true, "subscriber-importer": true, "subscription-gifting": true, diff --git a/config/test.json b/config/test.json index 2fd698bbe7d99..4c6ad9716364a 100644 --- a/config/test.json +++ b/config/test.json @@ -139,7 +139,6 @@ "stats/empty-module-v2": true, "stats/new-date-filtering": true, "stepper-woocommerce-poc": true, - "storage-addon": true, "themes/premium": true, "upgrades/redirect-payments": true, "upgrades/upcoming-renewals-notices": true, diff --git a/config/wpcalypso.json b/config/wpcalypso.json index 59a2c4064096e..31c9ae952e09d 100644 --- a/config/wpcalypso.json +++ b/config/wpcalypso.json @@ -188,7 +188,6 @@ "stats/paid-wpcom-v2": true, "stats/paid-wpcom-v3": true, "stepper-woocommerce-poc": true, - "storage-addon": true, "subscriber-csv-upload": true, "subscriber-importer": true, "subscription-gifting": true, diff --git a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts index 6e3f56990051e..3a366b39f67a7 100644 --- a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts +++ b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts @@ -1,6 +1,8 @@ -import { useTranslate } from 'i18n-calypso'; +import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; +import i18n, { useTranslate } from 'i18n-calypso'; import * as Purchases from '../../purchases'; import * as Site from '../../site'; +import { STORAGE_LIMIT } from '../constants'; import type { AddOnMeta } from '../types'; interface Props { @@ -8,11 +10,18 @@ interface Props { selectedSiteId?: number | null | undefined; } +type AddOnPurchaseStatus = { + available: boolean; + hidden?: boolean; + text?: ReturnType< typeof i18n.translate >; +}; + /** * Returns whether add-on product has been purchased or included in site plan. */ -const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ) => { +const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ): AddOnPurchaseStatus => { const translate = useTranslate(); + const mediaStorage = Site.useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); const matchingPurchases = Purchases.useSitePurchasesByProductSlug( { siteId: selectedSiteId, productSlug: addOnMeta.productSlug, @@ -24,13 +33,33 @@ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ) => { /* * Order matters below: - * 1. Check if purchased first. - * 2. Check if site feature next. - * Reason: `siteFeatures.active` involves both purchases and plan features. + * 1. Check if purchased. If storage add-on, check matching quantity. + * 2. If storage add-on, check if quantity is greater than the available upgrade. + * 3. Check if site already has this feature. Check this last since `siteFeatures.active` + * involves both purchases and plan features. */ if ( matchingPurchases ) { - return { available: false, text: translate( 'Purchased' ) }; + if ( addOnMeta.productSlug === PRODUCT_1GB_SPACE ) { + const purchase: Purchases.Purchase = Object.values( matchingPurchases )[ 0 ]; + if ( purchase.purchaseRenewalQuantity === addOnMeta.quantity ) { + return { available: false, text: translate( 'Purchased' ) }; + } + } else { + return { available: false, text: translate( 'Purchased' ) }; + } + } + + if ( addOnMeta.productSlug === PRODUCT_1GB_SPACE ) { + const currentMaxStorage = mediaStorage.data?.maxStorageBytes + ? mediaStorage.data.maxStorageBytes / Math.pow( 1024, 3 ) + : 0; + const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; + const quantity = addOnMeta.quantity ?? 0; + + if ( quantity > availableStorageUpgrade ) { + return { available: false, hidden: true }; + } } if ( isSiteFeature ) { diff --git a/packages/data-stores/src/add-ons/hooks/use-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-add-ons.ts index ed1824145fbfc..0122e5cf86f40 100644 --- a/packages/data-stores/src/add-ons/hooks/use-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-add-ons.ts @@ -15,12 +15,10 @@ import { ADD_ON_50GB_STORAGE, ADD_ON_CUSTOM_DESIGN, ADD_ON_UNLIMITED_THEMES, - STORAGE_LIMIT, } from '../constants'; import customDesignIcon from '../icons/custom-design'; import spaceUpgradeIcon from '../icons/space-upgrade'; import unlimitedThemesIcon from '../icons/unlimited-themes'; -import isStorageAddonEnabled from '../lib/is-storage-addon-enabled'; import useAddOnCheckoutLink from './use-add-on-checkout-link'; import useAddOnDisplayCost from './use-add-on-display-cost'; import useAddOnPrices from './use-add-on-prices'; @@ -82,7 +80,6 @@ const useActiveAddOnsDefs = ( selectedSiteId: Props[ 'selectedSiteId' ] ) => { 'Make more space for high-quality photos, videos, and other media. ' ), featured: false, - purchased: false, checkoutLink: checkoutLink( selectedSiteId ?? null, PRODUCT_1GB_SPACE, 50 ), }, { @@ -98,7 +95,6 @@ const useActiveAddOnsDefs = ( selectedSiteId: Props[ 'selectedSiteId' ] ) => { 'Take your site to the next level. Store all your media in one place without worrying about running out of space.' ), featured: false, - purchased: false, checkoutLink: checkoutLink( selectedSiteId ?? null, PRODUCT_1GB_SPACE, 100 ), }, ] as const, @@ -118,23 +114,14 @@ const useActiveAddOnsDefs = ( selectedSiteId: Props[ 'selectedSiteId' ] ) => { interface Props { selectedSiteId?: number | null | undefined; - enableStorageAddOns?: boolean; } -const useAddOns = ( { - selectedSiteId, - enableStorageAddOns, -}: Props = {} ): ( AddOnMeta | null )[] => { +const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] => { const activeAddOns = useActiveAddOnsDefs( selectedSiteId ); const productSlugs = activeAddOns.map( ( item ) => item.productSlug ); const productsList = ProductsList.useProducts( productSlugs ); - const mediaStorage = Site.useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); const siteFeatures = Site.useSiteFeatures( { siteIdOrSlug: selectedSiteId } ); const sitePurchases = Purchases.useSitePurchases( { siteId: selectedSiteId } ); - const spaceUpgradesPurchased = Purchases.useSitePurchasesByProductSlug( { - siteId: selectedSiteId, - productSlug: PRODUCT_1GB_SPACE, - } ); return useMemo( () => @@ -166,53 +153,6 @@ const useAddOns = ( { return null; } - /** - * If it's a storage add-on. - */ - if ( addOn.productSlug === PRODUCT_1GB_SPACE ) { - // if storage add-ons are not enabled in the config or disabled via hook prop, remove them - if ( - ( 'boolean' === typeof enableStorageAddOns && ! enableStorageAddOns ) || - ( ! isStorageAddonEnabled() && 'boolean' !== typeof enableStorageAddOns ) - ) { - return null; - } - - /** - * If storage add-on is already purchased. - * TODO: Consider migrating this part to `use-add-on-purchase-status` and attach - * that to the add-on's meta if need to. The intention is to have a single source of truth. - */ - const isStorageAddOnPurchased = Object.values( spaceUpgradesPurchased ?? [] ).some( - ( purchase ) => purchase.purchaseRenewalQuantity === addOn.quantity - ); - if ( isStorageAddOnPurchased ) { - return { - ...addOn, - name, - description, - purchased: true, - }; - } - - /** - * If the current storage add-on option is greater than the available upgrade. - * TODO: This is also potentially a candidate for `use-add-on-purchase-status`. - */ - const currentMaxStorage = mediaStorage.data?.maxStorageBytes - ? mediaStorage.data.maxStorageBytes / Math.pow( 1024, 3 ) - : 0; - const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; - if ( ( addOn.quantity ?? 0 ) > availableStorageUpgrade ) { - return { - ...addOn, - name, - description, - exceedsSiteStorageLimits: true, - }; - } - } - /** * Regular product add-ons. */ @@ -224,14 +164,10 @@ const useAddOns = ( { } ), [ activeAddOns, - enableStorageAddOns, - mediaStorage.data?.maxStorageBytes, productsList.data, productsList.isLoading, - siteFeatures.data?.active, siteFeatures.isLoading, sitePurchases.isLoading, - spaceUpgradesPurchased, ] ); }; diff --git a/packages/data-stores/src/add-ons/lib/is-storage-addon-enabled.ts b/packages/data-stores/src/add-ons/lib/is-storage-addon-enabled.ts deleted file mode 100644 index f48978304f485..0000000000000 --- a/packages/data-stores/src/add-ons/lib/is-storage-addon-enabled.ts +++ /dev/null @@ -1,9 +0,0 @@ -import config from '@automattic/calypso-config'; - -/** - * Is the storage addon (space upgrade) available, based on config flag vs. environment. - * @returns {boolean} - Whether or not the storage addon is available - */ -const isStorageAddonEnabled = (): boolean => !! config.isEnabled( 'storage-addon' ); - -export default isStorageAddonEnabled; From 361d982487c51f13f3bca1d814f7327ac104047f Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 13 Dec 2024 12:58:12 +0100 Subject: [PATCH 02/12] Simplify --- .../sites/overview/components/plan-card.tsx | 21 +++---------- .../hooks/use-add-on-purchase-status.ts | 15 +++++---- .../hooks/use-available-storage-add-ons.ts | 31 ++++++++++--------- .../storage/components/plan-storage.tsx | 2 +- .../use-available-storage-dropdown-options.ts | 4 +-- 5 files changed, 31 insertions(+), 42 deletions(-) diff --git a/client/sites/overview/components/plan-card.tsx b/client/sites/overview/components/plan-card.tsx index f0423de9c3647..4cda07a26ed0d 100644 --- a/client/sites/overview/components/plan-card.tsx +++ b/client/sites/overview/components/plan-card.tsx @@ -1,7 +1,6 @@ import { getPlan, PlanSlug, PLAN_MONTHLY_PERIOD } from '@automattic/calypso-products'; import { Button, PlanPrice, LoadingPlaceholder, Badge } from '@automattic/components'; import { AddOns } from '@automattic/data-stores'; -import { ADD_ON_100GB_STORAGE, useAddOnPurchaseStatus } from '@automattic/data-stores/src/add-ons'; import { usePricingMetaForGridPlans } from '@automattic/data-stores/src/plans'; import { usePlanBillingDescription } from '@automattic/plans-grid-next'; import clsx from 'clsx'; @@ -192,21 +191,15 @@ const PricingSection = () => { }; type NeedMoreStorageProps = { - addOn: AddOns.AddOnMeta; noLink?: boolean; }; -function NeedMoreStorage( { addOn, noLink = false }: NeedMoreStorageProps ) { +function NeedMoreStorage( { noLink = false }: NeedMoreStorageProps ) { const translate = useTranslate(); const site = useSelector( getSelectedSite ); const dispatch = useDispatch(); - const availability = useAddOnPurchaseStatus( { addOnMeta: addOn, selectedSiteId: site?.ID } ); const text = translate( 'Need more storage?' ); - if ( availability.hidden || ! availability.available ) { - return null; - } - if ( noLink ) { return text; } @@ -250,11 +243,7 @@ const PlanCard = () => { const isLoading = ! planDetails || planPurchaseLoading; const footerWrapperIsLink = useDisplayUpgradeLink( site?.ID ?? null ); - const addOns = AddOns.useAddOns( { selectedSiteId: site?.ID } ); - // Storage add-ons can be upgraded (i.e., if you already have the 50GB add-on, you can upgrade - // to 100GB) but not downgraded. That's why we only check the availability of the largest - // storage add-on. - const bigStorageAddon = addOns.find( ( addOn ) => addOn?.addOnSlug === ADD_ON_100GB_STORAGE ); + const availableStorageAddOns = AddOns.useAvailableStorageAddOns( { siteId: site?.ID } ); const renderManageButton = () => { if ( isJetpack || ! site || isStaging || isAgencyPurchase || isDevelopmentSite ) { @@ -339,11 +328,11 @@ const PlanCard = () => { siteId={ site?.ID } storageBarComponent={ PlanStorageBar } > - { bigStorageAddon && ( + { availableStorageAddOns.length ? (
- +
- ) } + ) : null } { site && ( diff --git a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts index 3a366b39f67a7..1287c3900d59d 100644 --- a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts +++ b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts @@ -2,7 +2,7 @@ import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; import i18n, { useTranslate } from 'i18n-calypso'; import * as Purchases from '../../purchases'; import * as Site from '../../site'; -import { STORAGE_LIMIT } from '../constants'; +import { isStorageQuantityAvailable } from './use-available-storage-add-ons'; import type { AddOnMeta } from '../types'; interface Props { @@ -40,7 +40,7 @@ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ): AddOnPu */ if ( matchingPurchases ) { - if ( addOnMeta.productSlug === PRODUCT_1GB_SPACE ) { + if ( addOnMeta.quantity ) { const purchase: Purchases.Purchase = Object.values( matchingPurchases )[ 0 ]; if ( purchase.purchaseRenewalQuantity === addOnMeta.quantity ) { return { available: false, text: translate( 'Purchased' ) }; @@ -51,13 +51,12 @@ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ): AddOnPu } if ( addOnMeta.productSlug === PRODUCT_1GB_SPACE ) { - const currentMaxStorage = mediaStorage.data?.maxStorageBytes - ? mediaStorage.data.maxStorageBytes / Math.pow( 1024, 3 ) - : 0; - const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; - const quantity = addOnMeta.quantity ?? 0; + const available = isStorageQuantityAvailable( + addOnMeta.quantity ?? 0, + mediaStorage.data?.maxStorageBytes + ); - if ( quantity > availableStorageUpgrade ) { + if ( ! available ) { return { available: false, hidden: true }; } } diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index 40857fc74791e..4672e6ed0bcd2 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -1,12 +1,21 @@ import { useMemo } from '@wordpress/element'; import { useSiteMediaStorage } from '../../site'; import { STORAGE_LIMIT } from '../constants'; +import { AddOnMeta } from '../types'; import useStorageAddOns from './use-storage-add-ons'; interface Props { siteId?: number | null; } +export function isStorageQuantityAvailable( quantity: number, maxStorageBytes?: number ) { + const currentMaxStorage = + maxStorageBytes !== undefined ? maxStorageBytes / Math.pow( 1024, 3 ) : 0; + const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; + + return quantity <= availableStorageUpgrade; +} + /** * Returns the storage add-ons that are available for purchase considering the current site when present. * Conditions: @@ -14,25 +23,17 @@ interface Props { * - If the storage add-on does not exceed the site storage limits. * - If the quantity of the storage add-on is less than or equal to the available storage upgrade. */ -const useAvailableStorageAddOns = ( { siteId }: Props ) => { +const useAvailableStorageAddOns = ( { siteId }: Props ): AddOnMeta[] => { const storageAddOns = useStorageAddOns( { siteId } ); const siteMediaStorage = useSiteMediaStorage( { siteIdOrSlug: siteId } ); - const currentMaxStorage = siteMediaStorage.data?.maxStorageBytes - ? siteMediaStorage.data.maxStorageBytes / Math.pow( 1024, 3 ) - : 0; - const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; return useMemo( () => { - const availableStorageAddOns = storageAddOns.filter( ( addOn ) => - addOn - ? ! addOn.purchased && - ! addOn.exceedsSiteStorageLimits && - ( addOn.quantity ?? 0 ) <= availableStorageUpgrade - : false - ); - - return availableStorageAddOns?.length ? availableStorageAddOns : null; - }, [ availableStorageUpgrade, storageAddOns ] ); + return storageAddOns + .filter( ( addOn ) => addOn !== null ) + .filter( ( addOn ) => + isStorageQuantityAvailable( addOn.quantity ?? 0, siteMediaStorage.data?.maxStorageBytes ) + ); + }, [ siteMediaStorage, storageAddOns ] ); }; export default useAvailableStorageAddOns; diff --git a/packages/plans-grid-next/src/components/shared/storage/components/plan-storage.tsx b/packages/plans-grid-next/src/components/shared/storage/components/plan-storage.tsx index 079ddd364a258..8c0982629b3c1 100644 --- a/packages/plans-grid-next/src/components/shared/storage/components/plan-storage.tsx +++ b/packages/plans-grid-next/src/components/shared/storage/components/plan-storage.tsx @@ -36,7 +36,7 @@ const PlanStorage = ( { const canUpgradeStorageForPlan = ( current || availableForPurchase ) && showUpgradeableStorage && - availableStorageAddOns && + availableStorageAddOns.length && ELIGIBLE_PLANS_FOR_STORAGE_UPGRADE.includes( planSlug ); return ( diff --git a/packages/plans-grid-next/src/components/shared/storage/hooks/use-available-storage-dropdown-options.ts b/packages/plans-grid-next/src/components/shared/storage/hooks/use-available-storage-dropdown-options.ts index 0b8ba5d1fb044..d2be3f5ba916f 100644 --- a/packages/plans-grid-next/src/components/shared/storage/hooks/use-available-storage-dropdown-options.ts +++ b/packages/plans-grid-next/src/components/shared/storage/hooks/use-available-storage-dropdown-options.ts @@ -25,7 +25,7 @@ const useAvailableStorageDropdownOptions = ( { } = gridPlansIndex[ planSlug ]; return useMemo( () => { - return storageFeature || availableStorageAddOns + return storageFeature || availableStorageAddOns.length ? [ ...( storageFeature ? [ storageFeature?.getSlug() as WPComPlanStorageFeatureSlug ] : [] ), /** @@ -33,7 +33,7 @@ const useAvailableStorageDropdownOptions = ( { * But being extra cautious here (in case the call is made elsewhere in the future, where it might not be redundant) * TODO: Also planning to refactor this closer to the data layer e.g. plans having a "storage-upgradeable" flag. */ - ...( ELIGIBLE_PLANS_FOR_STORAGE_UPGRADE.includes( planSlug ) && availableStorageAddOns + ...( ELIGIBLE_PLANS_FOR_STORAGE_UPGRADE.includes( planSlug ) ? availableStorageAddOns.map( ( addOn ) => addOn?.addOnSlug as AddOns.StorageAddOnSlug ) : [] ), ] From cda2d1b24a57c99058007eb6dc09ca38d3f4278e Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 13 Dec 2024 13:14:18 +0100 Subject: [PATCH 03/12] Simplify --- .../add-ons/components/add-ons-card.tsx | 18 +++++++----- .../hooks/use-add-on-purchase-status.ts | 28 ++++--------------- .../hooks/use-storage-add-on-availability.ts | 25 +++++++++++++++++ packages/data-stores/src/add-ons/index.ts | 1 + 4 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts diff --git a/client/my-sites/add-ons/components/add-ons-card.tsx b/client/my-sites/add-ons/components/add-ons-card.tsx index 93be0657f171e..b29b891d6b357 100644 --- a/client/my-sites/add-ons/components/add-ons-card.tsx +++ b/client/my-sites/add-ons/components/add-ons-card.tsx @@ -1,5 +1,8 @@ import { Badge, Gridicon, Spinner } from '@automattic/components'; -import { useAddOnPurchaseStatus } from '@automattic/data-stores/src/add-ons'; +import { + useAddOnPurchaseStatus, + useStorageAddOnAvailability, +} from '@automattic/data-stores/src/add-ons'; import styled from '@emotion/styled'; import { Card, CardBody, CardFooter, CardHeader, Button } from '@wordpress/components'; import { Icon } from '@wordpress/icons'; @@ -103,7 +106,8 @@ const Container = styled.div` const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatured }: Props ) => { const translate = useTranslate(); const selectedSiteId = useSelector( getSelectedSiteId ); - const availabilityStatus = useAddOnPurchaseStatus( { selectedSiteId, addOnMeta } ); + const purchaseStatus = useAddOnPurchaseStatus( { selectedSiteId, addOnMeta } ); + const available = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } ); const onActionPrimary = () => { actionPrimary?.handler( addOnMeta.productSlug, addOnMeta.quantity ); @@ -113,10 +117,10 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur }; const shouldRenderLoadingState = addOnMeta.isLoading; - const shouldRenderPrimaryAction = availabilityStatus?.available && ! shouldRenderLoadingState; - const shouldRenderSecondaryAction = ! availabilityStatus?.available && ! shouldRenderLoadingState; + const shouldRenderPrimaryAction = purchaseStatus?.available && ! shouldRenderLoadingState; + const shouldRenderSecondaryAction = ! purchaseStatus?.available && ! shouldRenderLoadingState; - if ( availabilityStatus?.hidden ) { + if ( ! available && purchaseStatus.available ) { return null; } @@ -151,10 +155,10 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur { actionSecondary.text } ) } - { availabilityStatus?.text && ( + { purchaseStatus?.text && (
- { availabilityStatus.text } + { purchaseStatus.text }
) } diff --git a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts index 1287c3900d59d..b919909eabd66 100644 --- a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts +++ b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts @@ -1,13 +1,11 @@ -import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; import i18n, { useTranslate } from 'i18n-calypso'; import * as Purchases from '../../purchases'; import * as Site from '../../site'; -import { isStorageQuantityAvailable } from './use-available-storage-add-ons'; import type { AddOnMeta } from '../types'; interface Props { addOnMeta: AddOnMeta; - selectedSiteId?: number | null | undefined; + selectedSiteId?: number | null; } type AddOnPurchaseStatus = { @@ -21,7 +19,6 @@ type AddOnPurchaseStatus = { */ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ): AddOnPurchaseStatus => { const translate = useTranslate(); - const mediaStorage = Site.useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); const matchingPurchases = Purchases.useSitePurchasesByProductSlug( { siteId: selectedSiteId, productSlug: addOnMeta.productSlug, @@ -31,14 +28,12 @@ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ): AddOnPu ( slug ) => siteFeatures.data?.active?.includes( slug ) ); - /* - * Order matters below: - * 1. Check if purchased. If storage add-on, check matching quantity. - * 2. If storage add-on, check if quantity is greater than the available upgrade. - * 3. Check if site already has this feature. Check this last since `siteFeatures.active` - * involves both purchases and plan features. + /** + * First, check if the add-on has a matching purchase. If storage add-on, check matching + * quantity. Secondly, check if the feature is active on the site. If there's no matching + * purchase but `siteFeatures.active` still contains the feature, it's because the feature is + * included in the plan. */ - if ( matchingPurchases ) { if ( addOnMeta.quantity ) { const purchase: Purchases.Purchase = Object.values( matchingPurchases )[ 0 ]; @@ -50,17 +45,6 @@ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ): AddOnPu } } - if ( addOnMeta.productSlug === PRODUCT_1GB_SPACE ) { - const available = isStorageQuantityAvailable( - addOnMeta.quantity ?? 0, - mediaStorage.data?.maxStorageBytes - ); - - if ( ! available ) { - return { available: false, hidden: true }; - } - } - if ( isSiteFeature ) { return { available: false, text: translate( 'Included in your plan' ) }; } diff --git a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts new file mode 100644 index 0000000000000..3209edf4570bf --- /dev/null +++ b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts @@ -0,0 +1,25 @@ +import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; +import { useSiteMediaStorage } from '../../site'; +import { AddOnMeta } from '../types'; +import { isStorageQuantityAvailable } from './use-available-storage-add-ons'; + +interface Props { + addOnMeta: AddOnMeta; + selectedSiteId?: number | null; +} + +/** + * Check if an add-on is a storage add-on, and if so, if the quantity is available for purchase. + */ +export default function useStorageAddOnAvailability( { + addOnMeta, + selectedSiteId, +}: Props ): boolean { + const mediaStorage = useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); + + if ( addOnMeta.productSlug !== PRODUCT_1GB_SPACE ) { + return true; + } + + return isStorageQuantityAvailable( addOnMeta.quantity ?? 0, mediaStorage.data?.maxStorageBytes ); +} diff --git a/packages/data-stores/src/add-ons/index.ts b/packages/data-stores/src/add-ons/index.ts index 905886d6b7090..00439fd6ffea1 100644 --- a/packages/data-stores/src/add-ons/index.ts +++ b/packages/data-stores/src/add-ons/index.ts @@ -4,6 +4,7 @@ export { default as useAddOnCheckoutLink } from './hooks/use-add-on-checkout-lin export { default as useAddOnPurchaseStatus } from './hooks/use-add-on-purchase-status'; export { default as useStorageAddOns } from './hooks/use-storage-add-ons'; export { default as useAvailableStorageAddOns } from './hooks/use-available-storage-add-ons'; +export { default as useStorageAddOnAvailability } from './hooks/use-storage-add-on-availability'; export * from './constants'; /** Types */ From f267d23f4623bfaa8def7538e825a81ca79f7792 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 13 Dec 2024 13:16:04 +0100 Subject: [PATCH 04/12] Fix regression --- client/sites/overview/components/plan-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/sites/overview/components/plan-card.tsx b/client/sites/overview/components/plan-card.tsx index 4cda07a26ed0d..1ce9d45ea134f 100644 --- a/client/sites/overview/components/plan-card.tsx +++ b/client/sites/overview/components/plan-card.tsx @@ -328,7 +328,7 @@ const PlanCard = () => { siteId={ site?.ID } storageBarComponent={ PlanStorageBar } > - { availableStorageAddOns.length ? ( + { availableStorageAddOns.length && ! isAgencyPurchase ? (
From 3a40d561f59300ba0101ca4687ba6856121f6496 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 13 Dec 2024 13:38:41 +0100 Subject: [PATCH 05/12] Fix type errors --- .../src/add-ons/hooks/use-available-storage-add-ons.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index 4672e6ed0bcd2..ffa71695c5d0f 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -30,8 +30,8 @@ const useAvailableStorageAddOns = ( { siteId }: Props ): AddOnMeta[] => { return useMemo( () => { return storageAddOns .filter( ( addOn ) => addOn !== null ) - .filter( ( addOn ) => - isStorageQuantityAvailable( addOn.quantity ?? 0, siteMediaStorage.data?.maxStorageBytes ) + .filter( ( addOn ): addOn is AddOnMeta => + isStorageQuantityAvailable( addOn?.quantity ?? 0, siteMediaStorage.data?.maxStorageBytes ) ); }, [ siteMediaStorage, storageAddOns ] ); }; From 9545898e67a4c7c644a7fc4ef69f4a4165fbe8bb Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 13 Dec 2024 16:12:24 +0100 Subject: [PATCH 06/12] Take new API data into account --- .../hooks/use-available-storage-add-ons.ts | 27 +++++++++++++------ .../hooks/use-storage-add-on-availability.ts | 2 +- .../site/queries/use-site-media-storage.ts | 1 + packages/data-stores/src/site/types.ts | 2 ++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index ffa71695c5d0f..94fb420c81a6e 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -1,6 +1,5 @@ import { useMemo } from '@wordpress/element'; -import { useSiteMediaStorage } from '../../site'; -import { STORAGE_LIMIT } from '../constants'; +import { SiteMediaStorage, useSiteMediaStorage } from '../../site'; import { AddOnMeta } from '../types'; import useStorageAddOns from './use-storage-add-ons'; @@ -8,12 +7,24 @@ interface Props { siteId?: number | null; } -export function isStorageQuantityAvailable( quantity: number, maxStorageBytes?: number ) { - const currentMaxStorage = - maxStorageBytes !== undefined ? maxStorageBytes / Math.pow( 1024, 3 ) : 0; - const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; +/** + * Check if the quantity for a storage add-on is available for purchase. + * @param quantity The number of gigabytes the given add-on adds to the site's storage + * @param storage Data returned from + */ +export function isStorageQuantityAvailable( + quantity: number, + storage?: SiteMediaStorage +): boolean { + if ( ! storage ) { + return true; + } + + const currentMaxStorage = storage.maxStorageBytes / Math.pow( 1024, 3 ); + const maxStorageExcludingAddons = storage.maxStorageBytesExcludingAddons / Math.pow( 1024, 3 ); + const existingAddOnStorage = currentMaxStorage - maxStorageExcludingAddons; - return quantity <= availableStorageUpgrade; + return existingAddOnStorage < quantity; } /** @@ -31,7 +42,7 @@ const useAvailableStorageAddOns = ( { siteId }: Props ): AddOnMeta[] => { return storageAddOns .filter( ( addOn ) => addOn !== null ) .filter( ( addOn ): addOn is AddOnMeta => - isStorageQuantityAvailable( addOn?.quantity ?? 0, siteMediaStorage.data?.maxStorageBytes ) + isStorageQuantityAvailable( addOn?.quantity ?? 0, siteMediaStorage.data ) ); }, [ siteMediaStorage, storageAddOns ] ); }; diff --git a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts index 3209edf4570bf..1efc988cb9790 100644 --- a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts +++ b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts @@ -21,5 +21,5 @@ export default function useStorageAddOnAvailability( { return true; } - return isStorageQuantityAvailable( addOnMeta.quantity ?? 0, mediaStorage.data?.maxStorageBytes ); + return isStorageQuantityAvailable( addOnMeta.quantity ?? 0, mediaStorage.data ); } diff --git a/packages/data-stores/src/site/queries/use-site-media-storage.ts b/packages/data-stores/src/site/queries/use-site-media-storage.ts index 321bf6b023c38..163b3ea4377ec 100644 --- a/packages/data-stores/src/site/queries/use-site-media-storage.ts +++ b/packages/data-stores/src/site/queries/use-site-media-storage.ts @@ -25,6 +25,7 @@ function useSiteMediaStorage( { } ); return { + maxStorageBytesExcludingAddons: Number( mediaStorage.max_storage_bytes_excluding_addons ), maxStorageBytes: Number( mediaStorage.max_storage_bytes ), storageUsedBytes: Number( mediaStorage.storage_used_bytes ), }; diff --git a/packages/data-stores/src/site/types.ts b/packages/data-stores/src/site/types.ts index e000503895324..8028332a91fa8 100644 --- a/packages/data-stores/src/site/types.ts +++ b/packages/data-stores/src/site/types.ts @@ -675,6 +675,7 @@ export interface AssembleSiteOptions { * Site media storage from `/sites/[ siteIdOrSlug ]/media-storage` endpoint */ export interface RawSiteMediaStorage { + max_storage_bytes_excluding_addons: number; max_storage_bytes: number; storage_used_bytes: number; } @@ -683,6 +684,7 @@ export interface RawSiteMediaStorage { * Site media storage transformed for frontend use */ export interface SiteMediaStorage { + maxStorageBytesExcludingAddons: number; maxStorageBytes: number; storageUsedBytes: number; } From a77e5e5d6e86ba2c89975dcf598e33a7afd51c61 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 16 Dec 2024 09:44:00 +0100 Subject: [PATCH 07/12] Fix merge --- .../data-stores/src/add-ons/hooks/use-add-ons.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/data-stores/src/add-ons/hooks/use-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-add-ons.ts index 349c1da917cff..5e60b93366ec2 100644 --- a/packages/data-stores/src/add-ons/hooks/use-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-add-ons.ts @@ -8,7 +8,6 @@ import { import { useMemo } from '@wordpress/element'; import { useTranslate } from 'i18n-calypso'; import * as ProductsList from '../../products-list'; -import * as Purchases from '../../purchases'; import * as Site from '../../site'; import { ADD_ON_100GB_STORAGE, @@ -120,8 +119,7 @@ const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] => const activeAddOns = useActiveAddOnsDefs( selectedSiteId ); const productSlugs = activeAddOns.map( ( item ) => item.productSlug ); const productsList = ProductsList.useProducts( productSlugs ); - const siteFeatures = Site.useSiteFeatures( { siteIdOrSlug: selectedSiteId } ); - const sitePurchases = Purchases.useSitePurchases( { siteId: selectedSiteId } ); + const mediaStorage = Site.useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); return useMemo( () => @@ -131,7 +129,7 @@ const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] => const description = addOn.description ?? ( product?.description || '' ); /** - * If siteFeatures, sitePurchases, or productsList are still loading, show the add-on as loading. + * If data required by the `/add-ons` page is still loading, show the add-on as loading. * TODO: Potentially another candidate for migrating to `use-add-on-purchase-status`, and attach * that to the add-on's meta if need to. */ @@ -162,13 +160,7 @@ const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] => description, }; } ), - [ - activeAddOns, - productsList.data, - productsList.isLoading, - siteFeatures.isLoading, - sitePurchases.isLoading, - ] + [ activeAddOns, mediaStorage.isLoading, productsList.data, productsList.isLoading ] ); }; From 8e577d2e096c7a4d4ea1134e1318da580712b65d Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 16 Dec 2024 10:01:40 +0100 Subject: [PATCH 08/12] Address review comments --- .../add-ons/components/add-ons-card.tsx | 9 +++++-- .../hooks/use-add-on-purchase-status.ts | 1 - .../hooks/use-available-storage-add-ons.ts | 25 ++++++++----------- .../hooks/use-storage-add-on-availability.ts | 8 ++++-- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/client/my-sites/add-ons/components/add-ons-card.tsx b/client/my-sites/add-ons/components/add-ons-card.tsx index b29b891d6b357..f312d035fadfc 100644 --- a/client/my-sites/add-ons/components/add-ons-card.tsx +++ b/client/my-sites/add-ons/components/add-ons-card.tsx @@ -1,3 +1,4 @@ +import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; import { Badge, Gridicon, Spinner } from '@automattic/components'; import { useAddOnPurchaseStatus, @@ -107,7 +108,7 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur const translate = useTranslate(); const selectedSiteId = useSelector( getSelectedSiteId ); const purchaseStatus = useAddOnPurchaseStatus( { selectedSiteId, addOnMeta } ); - const available = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } ); + const isStorageAvailable = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } ); const onActionPrimary = () => { actionPrimary?.handler( addOnMeta.productSlug, addOnMeta.quantity ); @@ -120,7 +121,11 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur const shouldRenderPrimaryAction = purchaseStatus?.available && ! shouldRenderLoadingState; const shouldRenderSecondaryAction = ! purchaseStatus?.available && ! shouldRenderLoadingState; - if ( ! available && purchaseStatus.available ) { + if ( + addOnMeta.productSlug === PRODUCT_1GB_SPACE && + ! isStorageAvailable && + purchaseStatus.available + ) { return null; } diff --git a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts index b919909eabd66..8a31ac0dca8b1 100644 --- a/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts +++ b/packages/data-stores/src/add-ons/hooks/use-add-on-purchase-status.ts @@ -10,7 +10,6 @@ interface Props { type AddOnPurchaseStatus = { available: boolean; - hidden?: boolean; text?: ReturnType< typeof i18n.translate >; }; diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index 94fb420c81a6e..565449110e446 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -12,14 +12,7 @@ interface Props { * @param quantity The number of gigabytes the given add-on adds to the site's storage * @param storage Data returned from */ -export function isStorageQuantityAvailable( - quantity: number, - storage?: SiteMediaStorage -): boolean { - if ( ! storage ) { - return true; - } - +export function isStorageQuantityAvailable( quantity: number, storage: SiteMediaStorage ): boolean { const currentMaxStorage = storage.maxStorageBytes / Math.pow( 1024, 3 ); const maxStorageExcludingAddons = storage.maxStorageBytesExcludingAddons / Math.pow( 1024, 3 ); const existingAddOnStorage = currentMaxStorage - maxStorageExcludingAddons; @@ -30,7 +23,6 @@ export function isStorageQuantityAvailable( /** * Returns the storage add-ons that are available for purchase considering the current site when present. * Conditions: - * - If the user has not purchased the storage add-on. * - If the storage add-on does not exceed the site storage limits. * - If the quantity of the storage add-on is less than or equal to the available storage upgrade. */ @@ -39,11 +31,16 @@ const useAvailableStorageAddOns = ( { siteId }: Props ): AddOnMeta[] => { const siteMediaStorage = useSiteMediaStorage( { siteIdOrSlug: siteId } ); return useMemo( () => { - return storageAddOns - .filter( ( addOn ) => addOn !== null ) - .filter( ( addOn ): addOn is AddOnMeta => - isStorageQuantityAvailable( addOn?.quantity ?? 0, siteMediaStorage.data ) - ); + const nonNullAddOns = storageAddOns.filter( ( addOn ) => addOn !== null ); + const siteMediaStorageData = siteMediaStorage.data; + + if ( ! siteMediaStorageData ) { + return nonNullAddOns; + } + + return nonNullAddOns.filter( ( addOn ): addOn is AddOnMeta => + isStorageQuantityAvailable( addOn?.quantity ?? 0, siteMediaStorageData ) + ); }, [ siteMediaStorage, storageAddOns ] ); }; diff --git a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts index 1efc988cb9790..d2765992d5454 100644 --- a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts +++ b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts @@ -14,11 +14,15 @@ interface Props { export default function useStorageAddOnAvailability( { addOnMeta, selectedSiteId, -}: Props ): boolean { +}: Props ): boolean | undefined { const mediaStorage = useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); if ( addOnMeta.productSlug !== PRODUCT_1GB_SPACE ) { - return true; + return undefined; + } + + if ( ! mediaStorage.data ) { + return false; } return isStorageQuantityAvailable( addOnMeta.quantity ?? 0, mediaStorage.data ); From e58c31f07b86833be7f197ca85177617f969dbb4 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 16 Dec 2024 10:33:06 +0100 Subject: [PATCH 09/12] Return enum from useStorageAddOnAvailability --- .../add-ons/components/add-ons-card.tsx | 12 +++++------- .../hooks/use-storage-add-on-availability.ts | 17 +++++++++++++---- packages/data-stores/src/add-ons/index.ts | 5 ++++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/client/my-sites/add-ons/components/add-ons-card.tsx b/client/my-sites/add-ons/components/add-ons-card.tsx index f312d035fadfc..57942bc121e30 100644 --- a/client/my-sites/add-ons/components/add-ons-card.tsx +++ b/client/my-sites/add-ons/components/add-ons-card.tsx @@ -1,8 +1,8 @@ -import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products'; import { Badge, Gridicon, Spinner } from '@automattic/components'; import { useAddOnPurchaseStatus, useStorageAddOnAvailability, + StorageAddOnAvailability, } from '@automattic/data-stores/src/add-ons'; import styled from '@emotion/styled'; import { Card, CardBody, CardFooter, CardHeader, Button } from '@wordpress/components'; @@ -108,7 +108,7 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur const translate = useTranslate(); const selectedSiteId = useSelector( getSelectedSiteId ); const purchaseStatus = useAddOnPurchaseStatus( { selectedSiteId, addOnMeta } ); - const isStorageAvailable = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } ); + const storageAvailability = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } ); const onActionPrimary = () => { actionPrimary?.handler( addOnMeta.productSlug, addOnMeta.quantity ); @@ -121,11 +121,9 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur const shouldRenderPrimaryAction = purchaseStatus?.available && ! shouldRenderLoadingState; const shouldRenderSecondaryAction = ! purchaseStatus?.available && ! shouldRenderLoadingState; - if ( - addOnMeta.productSlug === PRODUCT_1GB_SPACE && - ! isStorageAvailable && - purchaseStatus.available - ) { + // Return null if the add-on isn't already purchased and the amount of storage isn't available + // for purchase + if ( storageAvailability === StorageAddOnAvailability.Unavailable && purchaseStatus.available ) { return null; } diff --git a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts index d2765992d5454..8b78d0205a7c7 100644 --- a/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts +++ b/packages/data-stores/src/add-ons/hooks/use-storage-add-on-availability.ts @@ -8,22 +8,31 @@ interface Props { selectedSiteId?: number | null; } +export enum StorageAddOnAvailability { + NotAStorageAddOn, + DataLoading, + Unavailable, + Available, +} + /** * Check if an add-on is a storage add-on, and if so, if the quantity is available for purchase. */ export default function useStorageAddOnAvailability( { addOnMeta, selectedSiteId, -}: Props ): boolean | undefined { +}: Props ): StorageAddOnAvailability { const mediaStorage = useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } ); if ( addOnMeta.productSlug !== PRODUCT_1GB_SPACE ) { - return undefined; + return StorageAddOnAvailability.NotAStorageAddOn; } if ( ! mediaStorage.data ) { - return false; + return StorageAddOnAvailability.DataLoading; } - return isStorageQuantityAvailable( addOnMeta.quantity ?? 0, mediaStorage.data ); + return isStorageQuantityAvailable( addOnMeta.quantity ?? 0, mediaStorage.data ) + ? StorageAddOnAvailability.Available + : StorageAddOnAvailability.Unavailable; } diff --git a/packages/data-stores/src/add-ons/index.ts b/packages/data-stores/src/add-ons/index.ts index 00439fd6ffea1..bf4232f80ee82 100644 --- a/packages/data-stores/src/add-ons/index.ts +++ b/packages/data-stores/src/add-ons/index.ts @@ -4,7 +4,10 @@ export { default as useAddOnCheckoutLink } from './hooks/use-add-on-checkout-lin export { default as useAddOnPurchaseStatus } from './hooks/use-add-on-purchase-status'; export { default as useStorageAddOns } from './hooks/use-storage-add-ons'; export { default as useAvailableStorageAddOns } from './hooks/use-available-storage-add-ons'; -export { default as useStorageAddOnAvailability } from './hooks/use-storage-add-on-availability'; +export { + default as useStorageAddOnAvailability, + StorageAddOnAvailability, +} from './hooks/use-storage-add-on-availability'; export * from './constants'; /** Types */ From 3b2d1240af089db28b2c692fa18ebfb11cb7e4dd Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 16 Dec 2024 10:35:48 +0100 Subject: [PATCH 10/12] Appease TS --- .../src/add-ons/hooks/use-available-storage-add-ons.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index 565449110e446..9ce300e5717c2 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -31,14 +31,14 @@ const useAvailableStorageAddOns = ( { siteId }: Props ): AddOnMeta[] => { const siteMediaStorage = useSiteMediaStorage( { siteIdOrSlug: siteId } ); return useMemo( () => { - const nonNullAddOns = storageAddOns.filter( ( addOn ) => addOn !== null ); + const nonNullAddOns = storageAddOns.filter( ( addOn ): addOn is AddOnMeta => addOn !== null ); const siteMediaStorageData = siteMediaStorage.data; if ( ! siteMediaStorageData ) { return nonNullAddOns; } - return nonNullAddOns.filter( ( addOn ): addOn is AddOnMeta => + return nonNullAddOns.filter( ( addOn ) => isStorageQuantityAvailable( addOn?.quantity ?? 0, siteMediaStorageData ) ); }, [ siteMediaStorage, storageAddOns ] ); From 862f5c11ef852d41114649e55c4ea27d4807999a Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 16 Dec 2024 11:00:14 +0100 Subject: [PATCH 11/12] Ensure storage quantity doesn't exceed STORAGE_LIMIT --- .../src/add-ons/hooks/use-available-storage-add-ons.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index 9ce300e5717c2..8a7e43684ff52 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -1,5 +1,6 @@ import { useMemo } from '@wordpress/element'; import { SiteMediaStorage, useSiteMediaStorage } from '../../site'; +import { STORAGE_LIMIT } from '../constants'; import { AddOnMeta } from '../types'; import useStorageAddOns from './use-storage-add-ons'; @@ -16,8 +17,9 @@ export function isStorageQuantityAvailable( quantity: number, storage: SiteMedia const currentMaxStorage = storage.maxStorageBytes / Math.pow( 1024, 3 ); const maxStorageExcludingAddons = storage.maxStorageBytesExcludingAddons / Math.pow( 1024, 3 ); const existingAddOnStorage = currentMaxStorage - maxStorageExcludingAddons; + const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; - return existingAddOnStorage < quantity; + return existingAddOnStorage < quantity && quantity <= availableStorageUpgrade; } /** From fc2b76d2fe4172013ddc23c4c98c88afda599464 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Mon, 16 Dec 2024 11:15:17 +0100 Subject: [PATCH 12/12] Adapt front-end to back-end --- .../src/add-ons/hooks/use-available-storage-add-ons.ts | 3 +-- .../data-stores/src/site/queries/use-site-media-storage.ts | 2 +- packages/data-stores/src/site/types.ts | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts index 8a7e43684ff52..69a4c9312f671 100644 --- a/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts +++ b/packages/data-stores/src/add-ons/hooks/use-available-storage-add-ons.ts @@ -14,9 +14,8 @@ interface Props { * @param storage Data returned from */ export function isStorageQuantityAvailable( quantity: number, storage: SiteMediaStorage ): boolean { + const existingAddOnStorage = storage.maxStorageBytesFromAddOns / Math.pow( 1024, 3 ); const currentMaxStorage = storage.maxStorageBytes / Math.pow( 1024, 3 ); - const maxStorageExcludingAddons = storage.maxStorageBytesExcludingAddons / Math.pow( 1024, 3 ); - const existingAddOnStorage = currentMaxStorage - maxStorageExcludingAddons; const availableStorageUpgrade = STORAGE_LIMIT - currentMaxStorage; return existingAddOnStorage < quantity && quantity <= availableStorageUpgrade; diff --git a/packages/data-stores/src/site/queries/use-site-media-storage.ts b/packages/data-stores/src/site/queries/use-site-media-storage.ts index 163b3ea4377ec..048ee21fb5ed7 100644 --- a/packages/data-stores/src/site/queries/use-site-media-storage.ts +++ b/packages/data-stores/src/site/queries/use-site-media-storage.ts @@ -25,7 +25,7 @@ function useSiteMediaStorage( { } ); return { - maxStorageBytesExcludingAddons: Number( mediaStorage.max_storage_bytes_excluding_addons ), + maxStorageBytesFromAddOns: Number( mediaStorage.max_storage_bytes_from_add_ons ), maxStorageBytes: Number( mediaStorage.max_storage_bytes ), storageUsedBytes: Number( mediaStorage.storage_used_bytes ), }; diff --git a/packages/data-stores/src/site/types.ts b/packages/data-stores/src/site/types.ts index 8028332a91fa8..6134500317e08 100644 --- a/packages/data-stores/src/site/types.ts +++ b/packages/data-stores/src/site/types.ts @@ -675,7 +675,7 @@ export interface AssembleSiteOptions { * Site media storage from `/sites/[ siteIdOrSlug ]/media-storage` endpoint */ export interface RawSiteMediaStorage { - max_storage_bytes_excluding_addons: number; + max_storage_bytes_from_add_ons: number; max_storage_bytes: number; storage_used_bytes: number; } @@ -684,7 +684,7 @@ export interface RawSiteMediaStorage { * Site media storage transformed for frontend use */ export interface SiteMediaStorage { - maxStorageBytesExcludingAddons: number; + maxStorageBytesFromAddOns: number; maxStorageBytes: number; storageUsedBytes: number; }