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

Refactor logic for checking storage add-on availability #97450

Merged
merged 14 commits into from
Dec 17, 2024
Merged
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 >;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

A side note that I think the user-facing text we display shouldn't have been part of this hook. The text is part of UI, so should be derived from the calypso side based on the status value.

It's not related to this PR, though; just wanted to raise it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. This is already a meaty PR, though, so I'll leave it for another one


/**
* 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
Loading