Skip to content

Commit

Permalink
Refactor logic for checking storage add-on availability (#97450)
Browse files Browse the repository at this point in the history
* WIP - Refactor availability logic for storage add-ons

* Simplify

* Simplify

* Fix regression

* Fix type errors

* Take new API data into account

* Fix merge

* Address review comments

* Return enum from useStorageAddOnAvailability

* Appease TS

* Ensure storage quantity doesn't exceed STORAGE_LIMIT

* Adapt front-end to back-end
  • Loading branch information
fredrikekelund authored Dec 17, 2024
1 parent c62eed7 commit 3de2470
Show file tree
Hide file tree
Showing 13 changed files with 143 additions and 157 deletions.
48 changes: 17 additions & 31 deletions client/my-sites/add-ons/components/add-ons-card.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
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';
import { Icon } from '@wordpress/icons';
Expand All @@ -17,16 +21,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;
}
Expand Down Expand Up @@ -110,16 +104,11 @@ 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 purchaseStatus = useAddOnPurchaseStatus( { selectedSiteId, addOnMeta } );
const storageAvailability = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } );

const onActionPrimary = () => {
actionPrimary?.handler( addOnMeta.productSlug, addOnMeta.quantity );
Expand All @@ -129,17 +118,14 @@ const AddOnCard = ( {
};

const shouldRenderLoadingState = addOnMeta.isLoading;
const shouldRenderPrimaryAction = purchaseStatus?.available && ! shouldRenderLoadingState;
const shouldRenderSecondaryAction = ! purchaseStatus?.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;
// 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;
}

return (
<Container>
Expand Down Expand Up @@ -172,10 +158,10 @@ const AddOnCard = ( {
{ actionSecondary.text }
</Button>
) }
{ availabilityStatus?.text && (
{ purchaseStatus?.text && (
<div className="add-ons-card__selected-tag">
<Gridicon icon="checkmark" className="add-ons-card__checkmark" />
<span>{ availabilityStatus.text }</span>
<span>{ purchaseStatus.text }</span>
</div>
) }
</>
Expand Down
9 changes: 1 addition & 8 deletions client/my-sites/add-ons/components/add-ons-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ const Container = styled.div`
}
`;

const AddOnsGrid = ( {
addOns,
actionPrimary,
actionSecondary,
useAddOnAvailabilityStatus,
highlightFeatured,
}: Props ) => {
const AddOnsGrid = ( { addOns, actionPrimary, actionSecondary, highlightFeatured }: Props ) => {
return (
<Container>
{ addOns.map( ( addOn ) =>
Expand All @@ -35,7 +29,6 @@ const AddOnsGrid = ( {
}
actionPrimary={ actionPrimary }
actionSecondary={ actionSecondary }
useAddOnAvailabilityStatus={ useAddOnAvailabilityStatus }
addOnMeta={ addOn }
highlightFeatured={ highlightFeatured }
/>
Expand Down
4 changes: 1 addition & 3 deletions client/my-sites/add-ons/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -131,8 +130,7 @@ const AddOnsMain = () => {
<AddOnsGrid
actionPrimary={ { text: translate( 'Buy add-on' ), handler: handleActionPrimary } }
actionSecondary={ { text: translate( 'Manage add-on' ), handler: handleActionSelected } }
useAddOnAvailabilityStatus={ AddOns.useAddOnPurchaseStatus }
addOns={ filteredAddOns }
addOns={ addOns }
highlightFeatured
/>
</ContentWithHeader>
Expand Down
57 changes: 27 additions & 30 deletions client/sites/overview/components/plan-card.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
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 { 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';
Expand Down Expand Up @@ -193,27 +188,30 @@ const PricingSection = () => {
);
};

function PlanStorageFooter( { children }: PropsWithChildren ) {
type NeedMoreStorageProps = {
noLink?: boolean;
};

function NeedMoreStorage( { noLink = false }: NeedMoreStorageProps ) {
const translate = useTranslate();
const site = useSelector( getSelectedSite );
const dispatch = useDispatch();
const wrapperIsLink = useDisplayUpgradeLink( site?.ID ?? null );
const text = translate( 'Need more storage?' );

if ( wrapperIsLink ) {
return <div className="hosting-overview__plan-storage-footer">{ children }</div>;
if ( noLink ) {
return text;
}

return (
<div className="hosting-overview__plan-storage-footer">
<Button
plain
href={ `/add-ons/${ site?.slug }` }
onClick={ () => {
dispatch( recordTracksEvent( 'calypso_hosting_overview_need_more_storage_click' ) );
} }
>
{ children }
</Button>
</div>
<Button
plain
href={ `/add-ons/${ site?.slug }` }
onClick={ () => {
dispatch( recordTracksEvent( 'calypso_hosting_overview_need_more_storage_click' ) );
} }
>
{ text }
</Button>
);
}

Expand Down Expand Up @@ -242,11 +240,8 @@ const PlanCard = () => {
const planPurchaseLoading = ! isFreePlan && planPurchase === null;
const isLoading = ! planDetails || planPurchaseLoading;

// Check for storage addons available for purchase.
const addOns = AddOns.useAddOns( { selectedSiteId: site?.ID } );
const storageAddons = addOns.filter(
( addOn ) => addOn?.productSlug === PRODUCT_1GB_SPACE && ! addOn?.exceedsSiteStorageLimits
);
const footerWrapperIsLink = useDisplayUpgradeLink( site?.ID ?? null );
const availableStorageAddOns = AddOns.useAvailableStorageAddOns( { siteId: site?.ID } );

const renderManageButton = () => {
if ( isJetpack || ! site || isStaging || isAgencyPurchase || isDevelopmentSite ) {
Expand Down Expand Up @@ -331,9 +326,11 @@ const PlanCard = () => {
siteId={ site?.ID }
storageBarComponent={ PlanStorageBar }
>
{ storageAddons.length > 0 && ! isAgencyPurchase && (
<PlanStorageFooter>{ translate( 'Need more storage?' ) }</PlanStorageFooter>
) }
{ availableStorageAddOns.length && ! isAgencyPurchase ? (
<div className="hosting-overview__plan-storage-footer">
<NeedMoreStorage noLink={ footerWrapperIsLink } />
</div>
) : null }
</PlanStorage>

{ site && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { useTranslate } from 'i18n-calypso';
import i18n, { useTranslate } from 'i18n-calypso';
import * as Purchases from '../../purchases';
import * as Site from '../../site';
import type { AddOnMeta } from '../types';

interface Props {
addOnMeta: AddOnMeta;
selectedSiteId?: number | null | undefined;
selectedSiteId?: number | null;
}

type AddOnPurchaseStatus = {
available: 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 matchingPurchases = Purchases.useSitePurchasesByProductSlug( {
siteId: selectedSiteId,
Expand All @@ -22,15 +27,21 @@ const useAddOnPurchaseStatus = ( { addOnMeta, selectedSiteId }: Props ) => {
( slug ) => siteFeatures.data?.active?.includes( slug )
);

/*
* Order matters below:
* 1. Check if purchased first.
* 2. Check if site feature next.
* Reason: `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 ) {
return { available: false, text: translate( 'Purchased' ) };
if ( addOnMeta.quantity ) {
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 ( isSiteFeature ) {
Expand Down
58 changes: 2 additions & 56 deletions packages/data-stores/src/add-ons/hooks/use-add-ons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@ 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,
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';
Expand Down Expand Up @@ -81,7 +79,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 ),
},
{
Expand All @@ -97,7 +94,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,
Expand All @@ -124,10 +120,6 @@ const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] =>
const productSlugs = activeAddOns.map( ( item ) => item.productSlug );
const productsList = ProductsList.useProducts( productSlugs );
const mediaStorage = Site.useSiteMediaStorage( { siteIdOrSlug: selectedSiteId } );
const spaceUpgradesPurchased = Purchases.useSitePurchasesByProductSlug( {
siteId: selectedSiteId,
productSlug: PRODUCT_1GB_SPACE,
} );

return useMemo(
() =>
Expand All @@ -137,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.
*/
Expand All @@ -159,45 +151,6 @@ const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] =>
return null;
}

/**
* If it's a storage add-on.
*/
if ( addOn.productSlug === PRODUCT_1GB_SPACE ) {
/**
* 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.
*/
Expand All @@ -207,14 +160,7 @@ const useAddOns = ( { selectedSiteId }: Props = {} ): ( AddOnMeta | null )[] =>
description,
};
} ),
[
activeAddOns,
mediaStorage.data?.maxStorageBytes,
mediaStorage.isLoading,
productsList.data,
productsList.isLoading,
spaceUpgradesPurchased,
]
[ activeAddOns, mediaStorage.isLoading, productsList.data, productsList.isLoading ]
);
};

Expand Down
Loading

0 comments on commit 3de2470

Please sign in to comment.