Repository: Automattic/wp-calypso
Pull request #102505: Untangled Plans: show the add-ons in a modal instead
Author: login: fushar
Created: 2025-04-09T14:58:18Z | Merged: 2025-04-11T03:34:28Z

Fixes TMPFRG-597

## Proposed Changes

This PR proposes to show the add-ons in a modal, instead of redirecting the user to the `/add-ons/:siteSlug` page, in the following screens:

#### (Untangled) Plans page (`/plans/:siteSlug?flags=untangling/plans`)

<img width="1135" alt="image" src="https://github.com/user-attachments/assets/16c91c7b-2fc8-4d88-afb4-fc4beb7eea45" />

#### Overview card (`/overview/:siteSlug?flags=untangling/plans`)

<img width="645" alt="image" src="https://github.com/user-attachments/assets/44b2006c-34ab-41e9-a41c-d1884e0c1a21" />

#### Note to @matt-west 

The `Manage add-ons` in the Overview card, is currently only shown when:

1. The site is on Free plan, and
2. The resolution is Desktop.

I'm not sure why it is like this in production. Maybe this is to avoid showing both `Manage add-ons` and `Manage plan` links in paid plans, which can't fit in this narrow card.

For now, in this PR, I propose to keep (1) but not (2); i.e., show to Free plans, in all resolution. We can iterate later. What do you think?

## Why are these changes being made?

pbxlJb-6Xi-p2

## Testing Instructions

1. Go to `/plans/:siteSlug?flags=untangling/plans`
2. Click the `Manage add-ons` button
3. Verify you can see the modal
4. Verify you can buy add-ons
5. Repeat similarly in `/overview/:siteSlug?flags=untangling/plans`

## Regression testing instructions

- Verify that you can see and buy add-ons on the untangled page: `/add-ons/:siteSlug`.

## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [x] Has the general commit checklist been followed? (PCYsg-hS-p2)
- [x] [Have you written new tests](https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md) for your changes?
- [x] Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
- [x] Have you checked for TypeScript, React or other console errors?
- [x] Have you used memoizing on expensive computations? More info in [Memoizing with create-selector](https://github.com/Automattic/wp-calypso/blob/trunk/packages/state-utils/src/create-selector/README.md) and [Using memoizing selectors](https://react-redux.js.org/api/hooks#using-memoizing-selectors) and [Our Approach to Data](https://github.com/Automattic/wp-calypso/blob/trunk/docs/our-approach-to-data.md)
- [x] Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    - [x] For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages. 
- [x] For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?


Diff:
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 b4d2b5cb5bb7..427991ab4540 100644
--- a/client/my-sites/add-ons/components/add-ons-grid.tsx
+++ b/client/my-sites/add-ons/components/add-ons-grid.tsx
@@ -1,9 +1,9 @@
 import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products';
 import styled from '@emotion/styled';
-import AddOnCard from './add-ons-card';
+import AddOnCard from 'calypso/sites/components/add-ons/add-ons-card';
 import StorageAddOnCard from './storage-add-ons-card';
-import type { Props as CardProps } from './add-ons-card';
 import type { AddOnMeta } from '@automattic/data-stores';
+import type { Props as CardProps } from 'calypso/sites/components/add-ons/add-ons-card';
 import type { SiteId } from 'calypso/types';
 
 interface Props extends Omit< CardProps, 'addOnMeta' > {
diff --git a/client/my-sites/add-ons/components/storage-add-ons-card.tsx b/client/my-sites/add-ons/components/storage-add-ons-card.tsx
index 89c9a0b7e618..245b0f463c51 100644
--- a/client/my-sites/add-ons/components/storage-add-ons-card.tsx
+++ b/client/my-sites/add-ons/components/storage-add-ons-card.tsx
@@ -17,10 +17,7 @@ import { SiteId } from 'calypso/types';
 
 export interface Props {
 	siteId: SiteId;
-	actionPrimary?: {
-		text: string;
-		handler: ( productSlug: string, quantity?: number ) => void;
-	};
+	actionPrimary?: ( productSlug: string, quantity?: number ) => void;
 }
 
 const Container = styled.div`
@@ -211,8 +208,8 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 	}
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler(
-			availableStorageAddOns[ 0 ].productSlug,
+		actionPrimary?.(
+			availableStorageAddOns[ 0 ]?.productSlug,
 			selectedStorageAddOnStorageQuantity
 		);
 	};
@@ -259,7 +256,7 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 				<CardFooter isBorderless className="storage-add-ons-card__footer">
 					{ Boolean( selectControlOptions.length ) && actionPrimary && (
 						<Button onClick={ onActionPrimary } variant="primary">
-							{ actionPrimary.text }
+							{ translate( 'Buy add-on' ) }
 						</Button>
 					) }
 				</CardFooter>
diff --git a/client/my-sites/add-ons/main.tsx b/client/my-sites/add-ons/main.tsx
index 992cbeb17eb9..9f861bb17f3e 100644
--- a/client/my-sites/add-ons/main.tsx
+++ b/client/my-sites/add-ons/main.tsx
@@ -95,7 +95,6 @@ const NoAccess = () => {
 };
 
 const AddOnsMain = () => {
-	const translate = useTranslate();
 	const selectedSite = useSelector( getSelectedSite ) ?? null;
 	const addOns = AddOns.useAddOns( { selectedSiteId: selectedSite?.ID } );
 
@@ -134,8 +133,8 @@ const AddOnsMain = () => {
 			<PageViewTracker path="/add-ons/:site" title="Add-Ons" />
 			<ContentWithHeader>
 				<AddOnsGrid
-					actionPrimary={ { text: translate( 'Buy add-on' ), handler: handleActionPrimary } }
-					actionSecondary={ { text: translate( 'Manage add-on' ), handler: handleActionSelected } }
+					actionPrimary={ handleActionPrimary }
+					actionSecondary={ handleActionSelected }
 					addOns={ addOns }
 					siteId={ selectedSite?.ID }
 					highlightFeatured
diff --git a/client/my-sites/add-ons/components/add-ons-card.tsx b/client/sites/components/add-ons/add-ons-card/index.tsx
similarity index 85%
rename from client/my-sites/add-ons/components/add-ons-card.tsx
rename to client/sites/components/add-ons/add-ons-card/index.tsx
index 5f2e32a71108..515abaaa4256 100644
--- a/client/my-sites/add-ons/components/add-ons-card.tsx
+++ b/client/sites/components/add-ons/add-ons-card/index.tsx
@@ -13,14 +13,8 @@ import { getSelectedSiteId } from 'calypso/state/ui/selectors';
 import type { AddOnMeta } from '@automattic/data-stores';
 
 export interface Props {
-	actionPrimary?: {
-		text: string;
-		handler: ( productSlug: string, quantity?: number ) => void;
-	};
-	actionSecondary?: {
-		text: string;
-		handler: ( productSlug: string ) => void;
-	};
+	actionPrimary?: ( productSlug: string, quantity?: number ) => void;
+	actionSecondary?: ( productSlug: string ) => void;
 	highlightFeatured: boolean;
 	addOnMeta: AddOnMeta;
 }
@@ -49,6 +43,9 @@ const Container = styled.div`
 
 		.add-ons-card__icon {
 			display: flex;
+			padding: 10px;
+			border-radius: 4px;
+			background-color: var( --studio-blue-5 );
 		}
 
 		.add-ons-card__name-and-billing {
@@ -96,6 +93,7 @@ const Container = styled.div`
 			display: flex;
 			align-items: center;
 			gap: 0.5em;
+			font-size: 13px;
 
 			.add-ons-card__checkmark {
 				color: var( --studio-green-30 );
@@ -111,10 +109,10 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur
 	const storageAvailability = useStorageAddOnAvailability( { selectedSiteId, addOnMeta } );
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler( addOnMeta.productSlug, addOnMeta.quantity );
+		actionPrimary?.( addOnMeta.productSlug, addOnMeta.quantity );
 	};
 	const onActionSecondary = () => {
-		actionSecondary?.handler( addOnMeta.productSlug );
+		actionSecondary?.( addOnMeta.productSlug );
 	};
 
 	const shouldRenderLoadingState = addOnMeta.isLoading;
@@ -132,7 +130,7 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur
 			<Card className="add-ons-card">
 				<CardHeader isBorderless className="add-ons-card__header">
 					<div className="add-ons-card__icon">
-						{ addOnMeta.icon && <Icon icon={ addOnMeta.icon } size={ 44 } /> }
+						{ addOnMeta.icon && <Icon icon={ addOnMeta.icon } size={ 24 } /> }
 					</div>
 					<div className="add-ons-card__name-and-billing">
 						<div className="add-ons-card__name-tag">
@@ -155,27 +153,20 @@ const AddOnCard = ( { addOnMeta, actionPrimary, actionSecondary, highlightFeatur
 						<>
 							{ actionSecondary && (
 								<Button onClick={ onActionSecondary } variant="secondary">
-									{ actionSecondary.text }
+									{ translate( 'Manage add-on' ) }
 								</Button>
 							) }
 							{ purchaseStatus?.text && (
 								<div className="add-ons-card__selected-tag">
-									<Gridicon icon="checkmark" className="add-ons-card__checkmark" />
+									<Gridicon size={ 16 } icon="checkmark" className="add-ons-card__checkmark" />
 									<span>{ purchaseStatus.text }</span>
 								</div>
 							) }
 						</>
 					) }
 					{ shouldRenderPrimaryAction && actionPrimary && (
-						<Button
-							className="add-ons-card__action-button"
-							onClick={ onActionPrimary }
-							variant="link"
-							icon={ <Gridicon icon="chevron-right" /> }
-							iconPosition="right"
-							iconSize={ 16 }
-						>
-							{ actionPrimary.text }
+						<Button variant="primary" onClick={ onActionPrimary }>
+							{ translate( 'Buy add-on' ) }
 						</Button>
 					) }
 				</CardFooter>
diff --git a/client/sites/components/add-ons/add-ons-modal/index.tsx b/client/sites/components/add-ons/add-ons-modal/index.tsx
new file mode 100644
index 000000000000..91d292fa6843
--- /dev/null
+++ b/client/sites/components/add-ons/add-ons-modal/index.tsx
@@ -0,0 +1,70 @@
+import { PRODUCT_1GB_SPACE } from '@automattic/calypso-products';
+import page from '@automattic/calypso-router';
+import { AddOns } from '@automattic/data-stores';
+import { Modal } from '@wordpress/components';
+import { useTranslate } from 'i18n-calypso';
+import { useSelector } from 'react-redux';
+import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
+import { getSelectedSite } from 'calypso/state/ui/selectors';
+import AddOnCard from '../add-ons-card';
+
+import './style.scss';
+
+type AddOnsModalProps = {
+	isOpen: boolean;
+	onClose: () => void;
+};
+
+export default function AddOnsModal( { isOpen, onClose }: AddOnsModalProps ) {
+	const translate = useTranslate();
+	const site = useSelector( getSelectedSite );
+	const addOns = AddOns.useAddOns( { selectedSiteId: site?.ID } );
+	const nonStorageAddOns = addOns.filter( ( addOn ) => addOn?.productSlug !== PRODUCT_1GB_SPACE );
+
+	const checkoutLink = AddOns.useAddOnCheckoutLink();
+
+	const handleActionPrimary = ( addOnSlug: string, quantity?: number ) => {
+		recordTracksEvent( 'calypso_add_ons_action_primary_click', {
+			add_on_slug_with_quantity: `${ addOnSlug }${ quantity ? `:${ quantity }` : '' }`,
+			add_on_slug: addOnSlug,
+			quantity,
+		} );
+
+		page.redirect( `${ checkoutLink( site?.ID ?? null, addOnSlug, quantity ) }` );
+	};
+
+	const handleActionSecondary = () => {
+		page.redirect( `/purchases/subscriptions/${ site?.slug }` );
+	};
+
+	if ( ! isOpen ) {
+		return null;
+	}
+
+	return (
+		<Modal
+			className="add-ons-modal"
+			title={ translate( 'Boost your plan with add-ons' ) }
+			onRequestClose={ onClose }
+		>
+			<span className="add-ons-modal__description">
+				{ translate(
+					'Expand the functionality of your WordPress.com site by enabling any of the following features.'
+				) }
+			</span>
+			<div className="add-ons-modal__cards">
+				{ nonStorageAddOns.map( ( addOn ) =>
+					addOn ? (
+						<AddOnCard
+							key={ addOn.productSlug }
+							actionPrimary={ handleActionPrimary }
+							actionSecondary={ handleActionSecondary }
+							addOnMeta={ addOn }
+							highlightFeatured={ false }
+						/>
+					) : null
+				) }
+			</div>
+		</Modal>
+	);
+}
diff --git a/client/sites/components/add-ons/add-ons-modal/style.scss b/client/sites/components/add-ons/add-ons-modal/style.scss
new file mode 100644
index 000000000000..a2d48b48a598
--- /dev/null
+++ b/client/sites/components/add-ons/add-ons-modal/style.scss
@@ -0,0 +1,23 @@
+@import "@wordpress/base-styles/variables";
+
+.add-ons-modal {
+	.components-modal__content {
+		max-width: 512px;
+
+		> div:last-child {
+			display: flex;
+			flex-direction: column;
+			gap: $grid-unit-30;
+		}
+	}
+
+	&__description {
+		font-size: rem(13px);
+	}
+
+	&__cards {
+		display: flex;
+		flex-direction: column;
+		gap: $grid-unit-20;
+	}
+}
diff --git a/client/sites/components/plan-stats/index.tsx b/client/sites/components/plan-stats/index.tsx
index a3e9a2e45e29..27e7e007fc09 100644
--- a/client/sites/components/plan-stats/index.tsx
+++ b/client/sites/components/plan-stats/index.tsx
@@ -8,7 +8,7 @@ import { useStorageAddOnAvailable } from 'calypso/lib/plans/use-storage-add-on-a
 import { recordTracksEvent } from 'calypso/state/analytics/actions';
 import { getCurrentPlan } from 'calypso/state/sites/plans/selectors';
 import { getSelectedSite, getSelectedPurchase } from 'calypso/state/ui/selectors';
-import StorageAddOnsModal from '../storage-add-ons/modal';
+import StorageAddOnsModal from '../storage-add-ons/storage-add-ons-modal';
 import { PlanBandwidth } from './plan-bandwidth';
 import { PlanSiteVisits } from './plan-site-visits';
 import PlanStorageBar from './plan-storage-bar';
@@ -62,7 +62,7 @@ export default function PlanStats() {
 	const isStorageAddOnAvailable = useStorageAddOnAvailable( site?.ID );
 
 	const isUntangled = useSelector( isPlansPageUntangled );
-	const [ isOpen, setIsOpen ] = useState( false );
+	const [ isStorageAddOnsModalOpen, setIsStorageAddOnsModalOpen ] = useState( false );
 
 	if ( isLoading ) {
 		return <LoadingPlaceholder width="400px" height="100px" />;
@@ -78,7 +78,16 @@ export default function PlanStats() {
 				>
 					{ isStorageAddOnAvailable ? (
 						<div className="plan-storage-footer">
-							<NeedMoreStorage noLink={ footerWrapperIsLink } onClick={ () => setIsOpen( true ) } />
+							<NeedMoreStorage
+								noLink={ footerWrapperIsLink }
+								onClick={ () => setIsStorageAddOnsModalOpen( true ) }
+							/>
+							{ isUntangled && (
+								<StorageAddOnsModal
+									isOpen={ isStorageAddOnsModalOpen }
+									onClose={ () => setIsStorageAddOnsModalOpen( false ) }
+								/>
+							) }
 						</div>
 					) : null }
 				</PlanStorage>
@@ -90,9 +99,6 @@ export default function PlanStats() {
 					</div>
 				) }
 			</div>
-			{ isUntangled && (
-				<StorageAddOnsModal isOpen={ isOpen } setIsOpen={ setIsOpen } siteId={ site?.ID } />
-			) }
 		</>
 	);
 }
diff --git a/client/sites/components/storage-add-ons/dropdown.tsx b/client/sites/components/storage-add-ons/storage-add-ons-dropdown/index.tsx
similarity index 84%
rename from client/sites/components/storage-add-ons/dropdown.tsx
rename to client/sites/components/storage-add-ons/storage-add-ons-dropdown/index.tsx
index 62210f333aa0..08536e3fcc9d 100644
--- a/client/sites/components/storage-add-ons/dropdown.tsx
+++ b/client/sites/components/storage-add-ons/storage-add-ons-dropdown/index.tsx
@@ -4,6 +4,8 @@ import { CustomSelectControl } from '@wordpress/components';
 import { formatCurrency, useTranslate } from 'i18n-calypso';
 import { useCallback, useEffect } from 'react';
 
+import './style.scss';
+
 const StorageDropdownOption = ( {
 	price,
 	totalStorage,
@@ -13,22 +15,22 @@ const StorageDropdownOption = ( {
 } ) => {
 	const translate = useTranslate();
 
+	if ( ! price ) {
+		return null;
+	}
+
 	return (
-		<>
-			{ price ? (
-				<div className="storage-add-ons-card__storage-dropdown-option">
-					<span className="storage-add-ons-card__storage-dropdown-option-storage">
-						{ translate( '+ %(totalStorage)dGB', { args: { totalStorage } } ) }
-					</span>
-					<span className="storage-add-ons-card__storage-dropdown-option-price">
-						{ translate( '%(price)s/month, billed yearly', {
-							args: { price },
-							comment: 'The cost of a storage add on per month. Example reads as "$50/month"',
-						} ) }
-					</span>
-				</div>
-			) : null }
-		</>
+		<div className="storage-add-ons-dropdown__option">
+			<span className="storage-add-ons-dropdown__option--storage">
+				{ translate( '%(totalStorage)d GB Storage', { args: { totalStorage } } ) }
+			</span>
+			<span className="storage-add-ons-dropdown__option--price">
+				{ translate( '(%(price)s/month, billed yearly)', {
+					args: { price },
+					comment: 'The cost of a storage add on per month. Example reads as "$50/month"',
+				} ) }
+			</span>
+		</div>
 	);
 };
 
@@ -41,6 +43,7 @@ export const StorageAddOnsDropdown = ( {
 	selectedStorageAddOnSlug: StorageAddOnSlug | null;
 	setSelectedStorageAddOnSlug: ( slug: StorageAddOnSlug ) => void;
 } ) => {
+	const translate = useTranslate();
 	const storageAddOns = AddOns.useStorageAddOns( { siteId } );
 	const selectedStorageAddOn = storageAddOns?.find(
 		( addOn ) => addOn?.addOnSlug === selectedStorageAddOnSlug
@@ -112,13 +115,11 @@ export const StorageAddOnsDropdown = ( {
 
 	return (
 		<CustomSelectControl
-			__next40pxDefaultSize
-			hideLabelFromVision
+			label={ translate( 'Storage add-on' ) }
 			options={ selectControlOptions || [] }
 			// @ts-expect-error ts complains about selectedOption possibly being null
 			value={ selectedOption }
 			onChange={ handleOnChange }
-			label=""
 		/>
 	);
 };
diff --git a/client/sites/components/storage-add-ons/storage-add-ons-dropdown/style.scss b/client/sites/components/storage-add-ons/storage-add-ons-dropdown/style.scss
new file mode 100644
index 000000000000..ab91212287dd
--- /dev/null
+++ b/client/sites/components/storage-add-ons/storage-add-ons-dropdown/style.scss
@@ -0,0 +1,3 @@
+.storage-add-ons-dropdown__option--price {
+	margin-inline-start: 4px;
+}
diff --git a/client/sites/components/storage-add-ons/modal.tsx b/client/sites/components/storage-add-ons/storage-add-ons-modal/index.tsx
similarity index 50%
rename from client/sites/components/storage-add-ons/modal.tsx
rename to client/sites/components/storage-add-ons/storage-add-ons-modal/index.tsx
index 0a140a1763ff..694a53e91abb 100644
--- a/client/sites/components/storage-add-ons/modal.tsx
+++ b/client/sites/components/storage-add-ons/storage-add-ons-modal/index.tsx
@@ -1,25 +1,26 @@
 import page from '@automattic/calypso-router';
 import { Site, StorageAddOnSlug, AddOns } from '@automattic/data-stores';
-import { Modal, __experimentalVStack as VStack, Flex, Button } from '@wordpress/components';
+import { Modal, Button } from '@wordpress/components';
 import { useTranslate } from 'i18n-calypso';
 import { useState } from 'react';
+import { useSelector } from 'react-redux';
 import { recordTracksEvent } from 'calypso/lib/analytics/tracks';
 import { useStorageLimitOverride } from 'calypso/lib/plans/use-storage-limit-override';
-import { StorageAddOnsDropdown } from './dropdown';
-import StorageAddOnIndicator from './storage-indicator';
+import { getSelectedSiteId } from 'calypso/state/ui/selectors';
+import { StorageAddOnsDropdown } from '../storage-add-ons-dropdown';
+import StorageAddOnIndicator from '../storage-indicator';
+
+import './style.scss';
 
 type StorageAddOnsModalProps = {
 	isOpen: boolean;
-	siteId: number;
-	setIsOpen: ( isOpen: boolean ) => void;
+	onClose: () => void;
 };
 
-const StorageAddOnsModal: React.FC< StorageAddOnsModalProps > = ( {
-	isOpen,
-	siteId,
-	setIsOpen,
-} ) => {
+export default function StorageAddOnsModal( { isOpen, onClose }: StorageAddOnsModalProps ) {
 	const translate = useTranslate();
+	const siteId = useSelector( getSelectedSiteId );
+
 	const { data: mediaStorage } = Site.useSiteMediaStorage( { siteIdOrSlug: siteId } );
 
 	const availableStorageAddOns = AddOns.useAvailableStorageAddOns( { siteId } );
@@ -31,7 +32,7 @@ const StorageAddOnsModal: React.FC< StorageAddOnsModalProps > = ( {
 
 	const checkoutLink = AddOns.useAddOnCheckoutLink();
 	const onBuyStorage = () => {
-		setIsOpen( false );
+		onClose();
 
 		const slug = selectedStorageAddOn?.productSlug ?? '';
 		const quantity = selectedStorageAddOn?.quantity ?? 0;
@@ -44,8 +45,8 @@ const StorageAddOnsModal: React.FC< StorageAddOnsModalProps > = ( {
 		page.redirect( `${ checkoutLink( siteId, slug, quantity ) }` );
 	};
 
-	const onClose = () => {
-		setIsOpen( false );
+	const handleClose = () => {
+		onClose();
 		recordTracksEvent( 'calypso_storage_add_on_modal_action_cancel_click' );
 	};
 
@@ -57,41 +58,40 @@ const StorageAddOnsModal: React.FC< StorageAddOnsModalProps > = ( {
 		mediaStorage.maxStorageBytes = maxStorageBytesOverride;
 	}
 
-	if ( ! mediaStorage ) {
+	if ( ! isOpen || ! siteId || ! mediaStorage ) {
 		return null;
 	}
 
-	return isOpen ? (
-		<Modal title={ translate( 'Add more storage' ) } onRequestClose={ onClose }>
-			<VStack spacing={ 8 }>
-				<div>
-					<p>
-						{ translate( 'Make more space for high-quality photos, videos, and other media.' ) }
-					</p>
-					<h2>{ translate( 'Storage add-on' ) }</h2>
-					<StorageAddOnsDropdown
-						selectedStorageAddOnSlug={ selectedStorageAddOnSlug }
-						setSelectedStorageAddOnSlug={ setSelectedStorageAddOnSlug }
-						siteId={ siteId }
-					/>
-					<h2>{ translate( 'New storage capacity' ) }</h2>
-					<StorageAddOnIndicator
-						mediaStorage={ mediaStorage }
-						selectedStorageAddOnSlug={ selectedStorageAddOnSlug }
-						siteId={ siteId }
-					/>
-				</div>
-				<Flex direction="row" justify="flex-end">
-					<Button __next40pxDefaultSize variant="tertiary" onClick={ onClose }>
-						{ translate( 'Cancel' ) }
-					</Button>
-					<Button __next40pxDefaultSize variant="primary" onClick={ onBuyStorage }>
-						{ translate( 'Buy storage' ) }
-					</Button>
-				</Flex>
-			</VStack>
+	return (
+		<Modal
+			className="storage-add-ons-modal"
+			title={ translate( 'Add more storage' ) }
+			onRequestClose={ handleClose }
+		>
+			<span className="storage-add-ons-modal__description">
+				{ translate( 'Make more space for high-quality photos, videos, and other media.' ) }
+			</span>
+			<StorageAddOnsDropdown
+				selectedStorageAddOnSlug={ selectedStorageAddOnSlug }
+				setSelectedStorageAddOnSlug={ setSelectedStorageAddOnSlug }
+				siteId={ siteId }
+			/>
+			<div>
+				<div className="storage-add-ons-modal__label">{ translate( 'New storage capacity' ) }</div>
+				<StorageAddOnIndicator
+					mediaStorage={ mediaStorage }
+					selectedStorageAddOnSlug={ selectedStorageAddOnSlug }
+					siteId={ siteId }
+				/>
+			</div>
+			<div className="storage-add-ons-modal__buttons">
+				<Button variant="tertiary" onClick={ handleClose }>
+					{ translate( 'Cancel' ) }
+				</Button>
+				<Button variant="primary" onClick={ onBuyStorage }>
+					{ translate( 'Buy storage' ) }
+				</Button>
+			</div>
 		</Modal>
-	) : null;
-};
-
-export default StorageAddOnsModal;
+	);
+}
diff --git a/client/sites/components/storage-add-ons/storage-add-ons-modal/style.scss b/client/sites/components/storage-add-ons/storage-add-ons-modal/style.scss
new file mode 100644
index 000000000000..4187c759383e
--- /dev/null
+++ b/client/sites/components/storage-add-ons/storage-add-ons-modal/style.scss
@@ -0,0 +1,30 @@
+@import "@wordpress/base-styles/variables";
+
+.storage-add-ons-modal {
+	.components-modal__content {
+		max-width: 512px;
+
+		> div:last-child {
+			display: flex;
+			flex-direction: column;
+			gap: $grid-unit-30;
+		}
+	}
+
+	&__description {
+		font-size: rem(13px);
+	}
+
+	&__buttons {
+		display: flex;
+		justify-content: flex-end;
+		gap: $grid-unit-10;
+	}
+
+	&__label {
+		font-size: rem(11px);
+		font-weight: 500;
+		text-transform: uppercase;
+		margin-bottom: $grid-unit-15;
+	}
+}
diff --git a/client/sites/components/storage-add-ons/storage-indicator/index.tsx b/client/sites/components/storage-add-ons/storage-indicator/index.tsx
index 24fc05fbdcdf..3220ed0e9832 100644
--- a/client/sites/components/storage-add-ons/storage-indicator/index.tsx
+++ b/client/sites/components/storage-add-ons/storage-indicator/index.tsx
@@ -38,23 +38,29 @@ const StorageAddOnIndicator: React.FC< Props > = ( {
 
 	return (
 		<div className="storage-indicator">
-			<div>
-				<div>{ newMaxStorage }</div>
-				<div>{ translate( '%(usedStorage)s used', { args: { usedStorage } } ) }</div>
+			<div className="storage-indicator__usage">
+				<span className="storage-indicator__usage--capacity">{ newMaxStorage }</span>
+				<span className="storage-indicator__usage--used">
+					{ translate( '%(usedStorage)s used', { args: { usedStorage } } ) }
+				</span>
 			</div>
 			<div className="storage-indicator__bar">
 				<div
-					className="storage-indicator__bar-existing"
+					className="storage-indicator__bar--existing"
 					style={ { width: `${ planStorageRatio * 100 }%` } }
 				/>
 				<div
-					className="storage-indicator__bar-add-on"
+					className="storage-indicator__bar--add-on"
 					style={ { width: `${ ( 1 - planStorageRatio ) * 100 }%` } }
 				/>
 			</div>
-			<div>
-				<div>{ translate( '%(planStorage)s plan storage', { args: { planStorage } } ) }</div>
-				<div>{ translate( '%(addOnStorage)s add-on storage', { args: { addOnStorage } } ) }</div>
+			<div className="storage-indicator__quota">
+				<div className="storage-indicator__quota--plan">
+					{ translate( '%(planStorage)s plan storage', { args: { planStorage } } ) }
+				</div>
+				<div className="storage-indicator__quota--add-on">
+					{ translate( '%(addOnStorage)s storage add-on', { args: { addOnStorage } } ) }
+				</div>
 			</div>
 		</div>
 	);
diff --git a/client/sites/components/storage-add-ons/storage-indicator/style.scss b/client/sites/components/storage-add-ons/storage-indicator/style.scss
index ed042d0f0766..08097bd2f5ff 100644
--- a/client/sites/components/storage-add-ons/storage-indicator/style.scss
+++ b/client/sites/components/storage-add-ons/storage-indicator/style.scss
@@ -1,4 +1,28 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/variables";
+
 .storage-indicator {
+	display: flex;
+	flex-direction: column;
+	gap: $grid-unit-15;
+
+	&__usage {
+		display: flex;
+		gap: 2px;
+
+		&--capacity {
+			font-size: rem(15px);
+		}
+
+		&--used {
+			align-self: center;
+			margin-left: auto;
+			color: $gray-700;
+			font-size: rem(12px);
+			line-height: rem(16px);
+		}
+	}
+
 	&__bar {
 		display: flex;
 
@@ -6,11 +30,43 @@
 			height: 1.5px;
 		}
 
-		&-existing {
+		&--existing {
+			background-color: #3858E9;
+		}
+
+		&--add-on {
+			background-color: var(--studio-green-50);
+		}
+	}
+
+	&__quota {
+		display: flex;
+		gap: $grid-unit-15;
+		color: $gray-700;
+		font-size: rem(12px);
+		line-height: rem(16px);
+
+		&--plan, &--add-on {
+			position: relative;
+			padding-left: 10px;
+
+			&::before {
+				content: '';
+				position: absolute;
+				left: 0;
+				top: 50%;
+				transform: translateY(-50%);
+				width: 6px;
+				height: 6px;
+				border-radius: 100%;
+			}
+		}
+
+		&--plan::before {
 			background-color: #3858E9;
 		}
 
-		&-add-on {
+		&--add-on::before {
 			background-color: var(--studio-green-50);
 		}
 	}
diff --git a/client/sites/overview/components/plan-card.tsx b/client/sites/overview/components/plan-card.tsx
index 68dea76d186e..4545f86b4bd0 100644
--- a/client/sites/overview/components/plan-card.tsx
+++ b/client/sites/overview/components/plan-card.tsx
@@ -1,9 +1,11 @@
 import { LoadingPlaceholder, Badge } from '@automattic/components';
+import { Button } from '@wordpress/components';
 import { useTranslate } from 'i18n-calypso';
 import { useState } from 'react';
 import { useSelector } from 'react-redux';
 import QuerySitePlans from 'calypso/components/data/query-site-plans';
 import { HostingCard, HostingCardLinkButton } from 'calypso/components/hosting-card';
+import { isPlansPageUntangled } from 'calypso/lib/plans/untangling-plans-experiment';
 import { isPartnerPurchase, purchaseType } from 'calypso/lib/purchases';
 import { getManagePurchaseUrlFor } from 'calypso/my-sites/purchases/paths';
 import SitePreviewModal from 'calypso/sites-dashboard/components/site-preview-modal';
@@ -14,6 +16,7 @@ import getCurrentPlanPurchaseId from 'calypso/state/selectors/get-current-plan-p
 import { isJetpackSite } from 'calypso/state/sites/selectors';
 import { getSelectedPurchase, getSelectedSite } from 'calypso/state/ui/selectors';
 import { AppState } from 'calypso/types';
+import AddOnsModal from '../../components/add-ons/add-ons-modal';
 import PlanPricing from '../../components/plan-pricing';
 import PlanStats from '../../components/plan-stats';
 import { LaunchIcon, ShareLinkIcon } from './icons';
@@ -81,11 +84,31 @@ const PlanCard = () => {
 	const planPurchaseLoading = ! isFreePlan && planPurchase === null;
 	const isLoading = ! planDetails || planPurchaseLoading;
 
+	const plansPageIsUntangled = useSelector( isPlansPageUntangled );
+
+	const [ isManageAddOnsModalOpen, setIsManageAddOnsModalOpen ] = useState( false );
+	const onManageAddOnsButtonClick = () => {
+		setIsManageAddOnsModalOpen( true );
+	};
+
 	const renderManageButton = () => {
 		if ( isJetpack || ! site || isStaging || isAgencyPurchase || isDevelopmentSite ) {
 			return false;
 		}
 		if ( isFreePlan ) {
+			if ( plansPageIsUntangled ) {
+				return (
+					<>
+						<Button variant="tertiary" onClick={ onManageAddOnsButtonClick }>
+							{ translate( 'Manage add-ons' ) }
+						</Button>
+						<AddOnsModal
+							isOpen={ isManageAddOnsModalOpen }
+							onClose={ () => setIsManageAddOnsModalOpen( false ) }
+						/>
+					</>
+				);
+			}
 			return (
 				<HostingCardLinkButton to={ `/add-ons/${ site?.slug }` } hideOnMobile>
 					{ translate( 'Manage add-ons' ) }
diff --git a/client/sites/plan/components/current-plan-panel/index.tsx b/client/sites/plan/components/current-plan-panel/index.tsx
index 3033f4c60698..9ec6c64686b8 100644
--- a/client/sites/plan/components/current-plan-panel/index.tsx
+++ b/client/sites/plan/components/current-plan-panel/index.tsx
@@ -2,10 +2,12 @@ import { is100Year } from '@automattic/calypso-products';
 import { LoadingPlaceholder } from '@automattic/components';
 import { Button } from '@wordpress/components';
 import { useTranslate } from 'i18n-calypso';
+import { useState } from 'react';
 import { useSelector } from 'react-redux';
 import CoreBadge from 'calypso/components/core/badge';
 import { isPartnerPurchase, purchaseType } from 'calypso/lib/purchases';
 import { getMyPurchaseUrlFor } from 'calypso/my-sites/purchases/paths';
+import AddOnsModal from 'calypso/sites/components/add-ons/add-ons-modal';
 import PlanPricing from 'calypso/sites/components/plan-pricing';
 import PlanStats from 'calypso/sites/components/plan-stats';
 import { isA4AUser } from 'calypso/state/partner-portal/partner/selectors';
@@ -57,14 +59,25 @@ export default function CurrentPlanPanel() {
 		return <PlanPricing inline />;
 	};
 
+	const [ isManageAddOnsModalOpen, setIsManageAddOnsModalOpen ] = useState( false );
+	const onManageAddOnsButtonClick = () => {
+		setIsManageAddOnsModalOpen( true );
+	};
+
 	const renderManageAddOnsButton = () => {
 		if ( isA4APlan || is100YearPlan ) {
 			return null;
 		}
 		return (
-			<Button variant="tertiary" href={ `/add-ons/${ site?.slug }` }>
-				{ translate( 'Manage add-ons' ) }
-			</Button>
+			<>
+				<Button variant="tertiary" onClick={ onManageAddOnsButtonClick }>
+					{ translate( 'Manage add-ons' ) }
+				</Button>
+				<AddOnsModal
+					isOpen={ isManageAddOnsModalOpen }
+					onClose={ () => setIsManageAddOnsModalOpen( false ) }
+				/>
+			</>
 		);
 	};
 
diff --git a/client/sites/plan/test/index.tsx b/client/sites/plan/test/index.tsx
new file mode 100644
index 000000000000..56eb915bef69
--- /dev/null
+++ b/client/sites/plan/test/index.tsx
@@ -0,0 +1,117 @@
+/** @jest-environment jsdom */
+
+import { isEnabled } from '@automattic/calypso-config';
+import { PLAN_FREE } from '@automattic/calypso-products';
+import { useCurrentPlan } from '@automattic/data-stores/src/plans';
+import { screen, within, act } from '@testing-library/react';
+import React from 'react';
+import PlansWrapper from 'calypso/my-sites/plans/main';
+import purchasesReducer from 'calypso/state/purchases/reducer';
+import uiReducer from 'calypso/state/ui/reducer';
+import { renderWithProvider } from 'calypso/test-helpers/testing-library';
+
+jest.mock( '@automattic/calypso-config' );
+jest.mock( 'calypso/components/data/document-head', () => () => null );
+jest.mock( 'calypso/components/data/query-active-promotions', () => jest.fn() );
+jest.mock( 'calypso/components/data/query-sites', () => jest.fn() );
+jest.mock( 'calypso/components/data/query-site-purchases', () => jest.fn() );
+jest.mock( 'calypso/components/data/query-products-list', () => jest.fn() );
+jest.mock( '@automattic/data-stores/src/plans/hooks/use-current-plan', () => jest.fn() );
+
+const mockPlan = {
+	planSlug: PLAN_FREE,
+	is_free: true,
+};
+
+const mockSite = {
+	ID: 1,
+	plan: mockPlan,
+};
+
+const initialState = {
+	sites: {
+		items: {
+			[ mockSite.ID ]: {
+				ID: mockSite.ID,
+				slug: 'test-site',
+				plan: mockPlan,
+			},
+		},
+	},
+	ui: {
+		selectedSiteId: mockSite.ID,
+	},
+	currentUser: {
+		capabilities: {
+			[ mockSite.ID ]: {
+				manage_options: true,
+			},
+		},
+	},
+};
+
+describe( '/plans/:siteSlug', () => {
+	let originalScrollTo;
+
+	beforeAll( () => {
+		originalScrollTo = window.scrollTo;
+		window.scrollTo = () => null;
+		window.IntersectionObserver = jest.fn( () => ( {
+			observe: jest.fn(),
+			unobserve: jest.fn(),
+			disconnect: jest.fn(),
+		} ) );
+	} );
+
+	afterAll( () => {
+		window.scrollTo = originalScrollTo;
+	} );
+
+	const renderPage = () => {
+		renderWithProvider( <PlansWrapper context={ { path: '/plans' } } />, {
+			initialState,
+			reducers: {
+				ui: uiReducer,
+				purchases: purchasesReducer,
+			},
+		} );
+	};
+
+	beforeEach( () => {
+		jest.clearAllMocks();
+
+		const configMock = ( values ) => ( key ) => values[ key ];
+		( isEnabled as jest.Mock ).mockImplementation( configMock( { 'untangling/plans': true } ) );
+
+		( useCurrentPlan as jest.Mock ).mockReturnValue( mockPlan );
+
+		renderPage();
+	} );
+
+	describe( "'Manage add-ons' button", () => {
+		it( 'should be visible', () => {
+			expect( screen.getByRole( 'button', { name: 'Manage add-ons' } ) ).toBeInTheDocument();
+		} );
+
+		it( 'should hide the modal initially', () => {
+			expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
+		} );
+
+		describe( 'when the button is clicked', () => {
+			beforeEach( () => {
+				const button = screen.getByRole( 'button', { name: 'Manage add-ons' } );
+				act( () => {
+					button.click();
+				} );
+			} );
+
+			it( 'should show the modal', () => {
+				const dialog = screen.getByRole( 'dialog' );
+				expect( dialog ).toBeInTheDocument();
+				expect( within( dialog ).getByRole( 'heading' ) ).toHaveTextContent(
+					'Boost your plan with add-ons'
+				);
+			} );
+		} );
+	} );
+} );
diff --git a/packages/data-stores/src/add-ons/icons/custom-design.tsx b/packages/data-stores/src/add-ons/icons/custom-design.tsx
index 70811e065ccc..66ceac5d4f1f 100644
--- a/packages/data-stores/src/add-ons/icons/custom-design.tsx
+++ b/packages/data-stores/src/add-ons/icons/custom-design.tsx
@@ -1,18 +1,10 @@
 import { SVG, Path } from '@wordpress/primitives';
 
 const customDesign = (
-	<SVG width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+	<SVG width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 		<Path
-			d="M0 6.44C0 4.16204 0 3.02306 0.450346 2.1561C0.829848 1.42553 1.42553 0.829848 2.1561 0.450346C3.02306 0 4.16204 0 6.44 0H37.56C39.838 0 40.9769 0 41.8439 0.450346C42.5745 0.829848 43.1702 1.42553 43.5497 2.1561C44 3.02306 44 4.16204 44 6.44V37.56C44 39.838 44 40.9769 43.5497 41.8439C43.1702 42.5745 42.5745 43.1702 41.8439 43.5497C40.9769 44 39.838 44 37.56 44H6.44C4.16204 44 3.02306 44 2.1561 43.5497C1.42553 43.1702 0.829848 42.5745 0.450346 41.8439C0 40.9769 0 39.838 0 37.56V6.44Z"
-			fill="#E9EFF5"
-		/>
-		<Path
-			d="M26 27L30.2942 22.6902C30.684 22.299 30.6828 21.6658 30.2916 21.276L26 17"
-			fill="#0675C4"
-		/>
-		<Path
-			d="M18 27L13.7058 22.6902C13.316 22.299 13.3172 21.6658 13.7084 21.276L18 17"
-			fill="#0675C4"
+			d="M20.8008 11.2004L16.5008 6.90039L15.4008 8.00039L19.7008 12.3004C19.8008 12.4004 19.8008 12.6004 19.7008 12.7004L15.4008 17.0004L16.5008 18.1004L20.8008 13.8004C21.5008 13.0004 21.5008 11.9004 20.8008 11.2004ZM4.20078 12.3004L8.50078 8.00039L7.50078 7.00039L3.20078 11.3004C2.50078 12.0004 2.50078 13.1004 3.20078 13.8004L7.50078 18.1004L8.60078 17.0004L4.30078 12.7004C4.10078 12.6004 4.10078 12.4004 4.20078 12.3004Z"
+			fill="#3858E9"
 		/>
 	</SVG>
 );
diff --git a/packages/data-stores/src/add-ons/icons/unlimited-themes.tsx b/packages/data-stores/src/add-ons/icons/unlimited-themes.tsx
index af84966f9f32..6527a56e5acd 100644
--- a/packages/data-stores/src/add-ons/icons/unlimited-themes.tsx
+++ b/packages/data-stores/src/add-ons/icons/unlimited-themes.tsx
@@ -1,19 +1,10 @@
 import { SVG, Path } from '@wordpress/primitives';
 
 const unlimitedThemes = (
-	<SVG width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
+	<SVG width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 		<Path
-			d="M0 6.44C0 4.16204 0 3.02306 0.450346 2.1561C0.829848 1.42553 1.42553 0.829848 2.1561 0.450346C3.02306 0 4.16204 0 6.44 0H37.56C39.838 0 40.9769 0 41.8439 0.450346C42.5745 0.829848 43.1702 1.42553 43.5497 2.1561C44 3.02306 44 4.16204 44 6.44V37.56C44 39.838 44 40.9769 43.5497 41.8439C43.1702 42.5745 42.5745 43.1702 41.8439 43.5497C40.9769 44 39.838 44 37.56 44H6.44C4.16204 44 3.02306 44 2.1561 43.5497C1.42553 43.1702 0.829848 42.5745 0.450346 41.8439C0 40.9769 0 39.838 0 37.56V6.44Z"
-			fill="#E9EFF5"
-		/>
-		<Path d="M11.3333 31.9167, L21.9999 31.9167" stroke="#0675C4" strokeWidth="1.5" />
-		<Path
-			d="M21.1243 23.8018L31.2452 13.5665C32.0278 12.7751 32.0064 11.4899 31.2066 10.7159C30.419 9.95372 29.1574 9.96042 28.3866 10.7396L17.8223 21.4193L21.1243 23.8018Z"
-			fill="#0675C4"
-		/>
-		<Path
-			d="M14 28L13.3428 27.6386L12.7315 28.75H14V28ZM14.6572 28.3615C14.771 28.1544 14.8833 27.9815 15.0277 27.7486C15.1684 27.5213 15.323 27.2624 15.4873 26.9291C15.8158 26.2625 16.1642 25.3375 16.5622 23.7804L15.109 23.4088C14.7269 24.9034 14.4098 25.7222 14.1418 26.2659C14.0078 26.5378 13.8813 26.7507 13.7525 26.9587C13.6272 27.1609 13.4814 27.3867 13.3428 27.6386L14.6572 28.3615ZM16.5622 23.7804C16.7297 23.1254 16.9851 22.7308 17.2391 22.4943C17.4901 22.2605 17.7797 22.1436 18.0867 22.1083C18.7444 22.0326 19.4388 22.3387 19.7566 22.6565L20.8172 21.5958C20.1943 20.9728 19.0531 20.4873 17.9153 20.6181C17.3246 20.6861 16.7265 20.9219 16.2168 21.3966C15.71 21.8685 15.3316 22.5379 15.109 23.4088L16.5622 23.7804ZM19.7566 22.6565C20.33 23.2298 20.4426 24.7912 19.1458 25.773L20.0513 26.9689C21.9669 25.5185 22.1253 22.9039 20.8172 21.5958L19.7566 22.6565ZM19.1458 25.773C17.712 26.8586 16.1012 27.25 14 27.25V28.75C16.3042 28.75 18.2728 28.3154 20.0513 26.9689L19.1458 25.773Z"
-			fill="#0675C4"
+			d="M4 20.5008H12V19.0008H4V20.5008ZM18.9 4.00078C18.3 3.40078 17.4 3.40078 16.8 4.00078L9.6 11.2008C9.2 11.1008 8.9 11.2008 8.5 11.3008C8 11.5008 7 12.0008 6.6 13.5008C6.2 15.2008 5.8 15.7008 5.5 16.2008C5.4 16.3008 5.3 16.5008 5.2 16.6008L4.6 17.7008H6C8 17.7008 9.4 17.3008 10.7 16.3008C11.5 15.7008 11.9 14.9008 12 14.0008C12 13.7008 12 13.5008 11.9 13.3008L19 6.20078C19.5 5.60078 19.5 4.60078 18.9 4.00078ZM9.7 15.2008C9 15.7008 8.2 16.0008 7.3 16.2008C7.5 15.7008 7.8 15.0008 8.1 13.9008C8.3 13.3008 8.5 12.9008 8.9 12.8008C9.4 12.7008 9.9 12.9008 10.2 13.1008C10.4 13.3008 10.5 13.6008 10.4 13.9008C10.4 14.2008 10.3 14.8008 9.7 15.2008Z"
+			fill="#3858E9"
 		/>
 	</SVG>
 );


Reviews:
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T23:00:17Z
user: login: fushar
body: > Since this modal is not part of the core experience, can/should we lazy load it?

Looking at the codebase, I've never seen we lazy-load a modal 🤔 Is this a common practice? Will we get significant improvement gain, since this is a simple modal? (genuinely asking)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T01:29:35Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T07:15:39Z
user: login: fushar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:55:44Z
user: login: fushar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T08:57:41Z
user: login: matt-west
body: Thanks @fushar.

Can we update the icon for the CSS add-on to use the `code` icon?

<img width="303" alt="Screenshot 2025-04-10 at 16 53 23" src="https://github.com/user-attachments/assets/bce77e6e-2519-4c25-bfe0-c0b993a779ef" />

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T15:53:29Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T23:56:40Z

Review comments:
diff_hunk: @@ -211,10 +208,7 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 	}
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler(
-			availableStorageAddOns[ 0 ].productSlug,
-			selectedStorageAddOnStorageQuantity
-		);
+		actionPrimary?.( availableStorageAddOns[ 0 ].productSlug, selectedStorageAddOnStorageQuantity );
path: client/my-sites/add-ons/components/storage-add-ons-card.tsx
user: login: StevenDufresne
body: Looking at adjacent code, we are using the optional chaining operator liberally, expecting nullish values. 

I recognize this is pre-existing but noting that we are not doing so here. Re: `availableStorageAddOns[ 0 ].productSlug`
created_at: 2025-04-09T23:00:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,10 +208,7 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 	}
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler(
-			availableStorageAddOns[ 0 ].productSlug,
-			selectedStorageAddOnStorageQuantity
-		);
+		actionPrimary?.( availableStorageAddOns[ 0 ].productSlug, selectedStorageAddOnStorageQuantity );
path: client/my-sites/add-ons/components/storage-add-ons-card.tsx
user: login: fushar
body: Sorry, could you expand what you meant? Which adjacent code and what are we not doing here? 🤔 
created_at: 2025-04-10T01:21:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,10 +208,7 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 	}
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler(
-			availableStorageAddOns[ 0 ].productSlug,
-			selectedStorageAddOnStorageQuantity
-		);
+		actionPrimary?.( availableStorageAddOns[ 0 ].productSlug, selectedStorageAddOnStorageQuantity );
path: client/my-sites/add-ons/components/storage-add-ons-card.tsx
user: login: arthur791004
body: I guess it's related to the following line that uses `availableStorageAddOns?.map`

```js
const selectControlOptions = availableStorageAddOns?.map( ( addOn ) => {
```

After taking a closer look, the `useAvailableStorageAddOns` hook should always return an array, though it might be empty. So we should be careful when accessing `availableStorageAddOns[0].productSlug` directly. To avoid potential issues, it's better to use optional chaining like `availableStorageAddOns[0]?.productSlug`.
created_at: 2025-04-10T07:12:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,14 +59,25 @@ export default function CurrentPlanPanel() {
 		return <PlanPricing inline />;
 	};
 
+	const [ isManageAddOnsModalOpen, setIsManageAddOnsModalOpen ] = useState( false );
+	const onManageAddOnsButtonClick = () => {
+		setIsManageAddOnsModalOpen( true );
+	};
+
 	const renderManageAddOnsButton = () => {
 		if ( isA4APlan || is100YearPlan ) {
 			return null;
 		}
 		return (
-			<Button variant="tertiary" href={ `/add-ons/${ site?.slug }` }>
-				{ translate( 'Manage add-ons' ) }
-			</Button>
+			<>
+				<Button variant="tertiary" onClick={ onManageAddOnsButtonClick }>
+					{ translate( 'Manage add-ons' ) }
+				</Button>
path: client/sites/plan/components/current-plan-panel/index.tsx
user: login: arthur791004
body: [nit] This seems to be a bit duplicate. How about creating a `ManageAddOn` component that displays the button and manages the modal internally to avoid repetition?
created_at: 2025-04-10T07:14:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,10 +208,7 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 	}
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler(
-			availableStorageAddOns[ 0 ].productSlug,
-			selectedStorageAddOnStorageQuantity
-		);
+		actionPrimary?.( availableStorageAddOns[ 0 ].productSlug, selectedStorageAddOnStorageQuantity );
path: client/my-sites/add-ons/components/storage-add-ons-card.tsx
user: login: fushar
body: Got it, it's fixed in the latest commits.
created_at: 2025-04-10T08:55:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -57,14 +59,25 @@ export default function CurrentPlanPanel() {
 		return <PlanPricing inline />;
 	};
 
+	const [ isManageAddOnsModalOpen, setIsManageAddOnsModalOpen ] = useState( false );
+	const onManageAddOnsButtonClick = () => {
+		setIsManageAddOnsModalOpen( true );
+	};
+
 	const renderManageAddOnsButton = () => {
 		if ( isA4APlan || is100YearPlan ) {
 			return null;
 		}
 		return (
-			<Button variant="tertiary" href={ `/add-ons/${ site?.slug }` }>
-				{ translate( 'Manage add-ons' ) }
-			</Button>
+			<>
+				<Button variant="tertiary" onClick={ onManageAddOnsButtonClick }>
+					{ translate( 'Manage add-ons' ) }
+				</Button>
path: client/sites/plan/components/current-plan-panel/index.tsx
user: login: fushar
body: Oh, good idea. I am actually planning to add Track events later; I'll do that in that PR instead!
created_at: 2025-04-10T08:57:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -211,10 +208,7 @@ export default function StorageAddOnCard( { siteId, actionPrimary }: Props ) {
 	}
 
 	const onActionPrimary = () => {
-		actionPrimary?.handler(
-			availableStorageAddOns[ 0 ].productSlug,
-			selectedStorageAddOnStorageQuantity
-		);
+		actionPrimary?.( availableStorageAddOns[ 0 ].productSlug, selectedStorageAddOnStorageQuantity );
path: client/my-sites/add-ons/components/storage-add-ons-card.tsx
user: login: StevenDufresne
body: Thanks @arthur791004. I'll be more thorough next time. :)
created_at: 2025-04-10T23:56:40Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-09T14:58:36Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-140260">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-140260%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-140260">https://calypso.live?image=registry.a8c.com/calypso/app:build-140260</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-140260&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-140260%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-140260&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-140260&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-140260&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-140260%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-140260&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-140260&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-04-09T15:05:00Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**App Entrypoints** (~40 bytes removed 📉 [gzipped])
<details>

```
name        parsed_size           gzip_size
entry-main       +334 B  (+0.0%)      -40 B  (-0.0%)
```

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.
</details>

**Sections** (~1134 bytes added 📈 [gzipped])
<details>

```
name                             parsed_size           gzip_size
overview                            +16837 B  (+0.7%)    +5193 B  (+0.8%)
plans                               +16672 B  (+0.8%)    +5145 B  (+0.8%)
themes                               -1433 B  (-0.1%)     -291 B  (-0.1%)
theme                                -1433 B  (-0.2%)     -291 B  (-0.1%)
stats                                -1433 B  (-0.1%)     -304 B  (-0.1%)
site-purchases                       -1433 B  (-0.1%)     -285 B  (-0.0%)
signup                               -1433 B  (-0.5%)     -279 B  (-0.5%)
settings-performance                 -1433 B  (-0.2%)     -291 B  (-0.2%)
scan                                 -1433 B  (-0.2%)     -291 B  (-0.1%)
purchases                            -1433 B  (-0.1%)     -285 B  (-0.0%)
posts-custom                         -1433 B  (-0.2%)     -266 B  (-0.1%)
posts                                -1433 B  (-0.2%)     -266 B  (-0.1%)
plugins                              -1433 B  (-0.0%)     -291 B  (-0.0%)
migrate                              -1433 B  (-0.4%)     -309 B  (-0.3%)
media                                -1433 B  (-0.1%)     -291 B  (-0.1%)
marketplace                          -1433 B  (-0.2%)     -291 B  (-0.1%)
jetpack-cloud-plugin-management      -1433 B  (-0.1%)     -291 B  (-0.0%)
jetpack-app                          -1433 B  (-0.3%)     -289 B  (-0.2%)
hosting                              -1433 B  (-0.1%)     -291 B  (-0.1%)
home                                 -1433 B  (-0.1%)     -279 B  (-0.1%)
google-my-business                   -1433 B  (-0.3%)     -307 B  (-0.2%)
domains                              -1433 B  (-0.1%)     -313 B  (-0.0%)
checkout                             -1433 B  (-0.1%)     -307 B  (-0.1%)
backup                               -1433 B  (-0.1%)     -291 B  (-0.1%)
async-step-unified-plans             -1433 B  (-0.2%)     -279 B  (-0.1%)
async-step-unified-domains           -1433 B  (-0.1%)     -279 B  (-0.1%)
a8c-for-agencies-sites               -1433 B  (-0.1%)     -291 B  (-0.0%)
a8c-for-agencies-plugins             -1433 B  (-0.1%)     -291 B  (-0.0%)
add-ons                               -469 B  (-0.1%)     -201 B  (-0.2%)
```

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

**Async-loaded Components** (~291 bytes removed 📉 [gzipped])
<details>

```
name                                               parsed_size           gzip_size
async-load-design-blocks                               -1433 B  (-0.1%)     -291 B  (-0.0%)
async-load-calypso-post-editor-media-modal             -1433 B  (-0.1%)     -291 B  (-0.1%)
async-load-calypso-post-editor-editor-media-modal      -1433 B  (-0.1%)     -291 B  (-0.1%)
```

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

**Legend**
<details>
<summary>What is parsed and gzip size?</summary>

**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.
</details>

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: matticbot
created_at: 2025-04-09T15:07:39Z
body: **This PR modifies the release build for the following Calypso Apps:**

_For info about this notification, see here: PCYsg-OT6-p2_

* odyssey-stats

To test WordPress.com changes, run `install-plugin.sh $pluginSlug untangled-plans/add-ons-modal` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-04-09T23:18:43Z
body: Nice! This is working as expected. I left a minor comment inline.

Generally, two questions:
- Should we add unit tests?
- Since this modal is not part of the core experience, can/should we lazy load it?
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-04-10T01:42:02Z
body: > Should we add unit tests?

Let me timebox some time today to add a basic test to the new Plans page 🙇 
author_association: CONTRIBUTOR
user: login: arthur791004
created_at: 2025-04-10T07:20:08Z
body: > Looking at the codebase, I've never seen we lazy-load a modal 🤔 Is this a common practice? Will we get significant improvement gain, since this is a simple modal? (genuinely asking)

Not a major improvement, but it could be beneficial to lazy-load components that aren’t immediately needed on the page. That said, I don’t have a strong preference either way.
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-04-10T08:42:55Z
body: > Should we add unit tests?

@StevenDufresne I added a basic smoke test for the whole page. It should be good for now, we can add more in the future as the new Hosting Dashboard page is built 😄 
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-04-10T08:59:32Z
body: > > Looking at the codebase, I've never seen we lazy-load a modal 🤔 Is this a common practice? Will we get significant improvement gain, since this is a simple modal? (genuinely asking)
> 
> Not a major improvement, but it could be beneficial to lazy-load components that aren’t immediately needed on the page. That said, I don’t have a strong preference either way.

Cool, my take is that it's not needed for now, unless it's proven to have performance issue. 😄 
author_association: CONTRIBUTOR
user: login: matt-west
created_at: 2025-04-10T15:50:16Z
body: > For now, in this PR, I propose to keep (1) but not (2); i.e., show to Free plans, in all resolution. We can iterate later. What do you think?

That's okay with me. We’re changing this card in the dashboard redesign anyway.
author_association: CONTRIBUTOR
user: login: StevenDufresne
created_at: 2025-04-11T00:02:08Z
body: > Cool, my take is that it's not needed for now, unless it's proven to have performance issue. 😄

That's fine. 👍 

I mention it because it looks like we're moving toward a paradigm where supplementary user actions are handled in modals. It's easy for this to lead to a growing number of complex modals, and since modals are often treated as secondary UI, they’re typically not considered good candidates for lazy loading—though maybe they should be.
 
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-04-11T02:03:37Z
body: > I mention it because it looks like we're moving toward a paradigm where supplementary user actions are handled in modals. It's easy for this to lead to a growing number of complex modals, and since modals are often treated as secondary UI, they’re typically not considered good candidates for lazy loading—though maybe they should be.

Interesting take, thanks, this is a good learning. Let's see how the new hosting dashboard's IA plays out.

In any case, for now I think it's fine, because adding add-ons is not something you do in a daily basis 😄 
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-04-11T02:56:35Z
body: > Can we update the icon for the CSS add-on to use the code icon?

Sure, I've updated both icons based on Figma:

<img width="457" alt="image" src="https://github.com/user-attachments/assets/3ce68f38-a116-4268-b5f3-a86a442f98b5" />

author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-04-11T03:34:17Z
body: Thanks all. I believe I have addressed all the feedback, so I'm going to 🚢 this and we can iterate as needed.
author_association: CONTRIBUTOR
user: login: a8ci18n
created_at: 2025-04-11T05:20:33Z
body: This Pull Request is now available for translation here: https://translate.wordpress.com/deliverables/17378355

Some locales (Hebrew, Japanese) have been temporarily machine-translated due to translator availability. All other translations are usually ready within a few days. Untranslated and machine-translated strings will be sent for translation next Monday and are expected to be completed by the following Friday.

Thank you @fushar for including a screenshot in the description! This is really helpful for our translators.
author_association: NONE