diff --git a/projects/packages/newsletter/_inc/subscribers/components/modals/comp-modal.tsx b/projects/packages/newsletter/_inc/subscribers/components/modals/comp-modal.tsx index e366eb5f537..cd1f26132ac 100644 --- a/projects/packages/newsletter/_inc/subscribers/components/modals/comp-modal.tsx +++ b/projects/packages/newsletter/_inc/subscribers/components/modals/comp-modal.tsx @@ -225,16 +225,6 @@ export default function CompModal( { subscriber, onClose }: Props ): JSX.Element ) : null } - { ! productsQuery.isLoading && products.length === 0 && ! productsQuery.isError ? ( - - - { __( - 'You don’t have any paid newsletter plans configured on this site yet.', - 'jetpack-newsletter' - ) } - - - ) : null } { allComped ? ( diff --git a/projects/packages/newsletter/_inc/subscribers/components/subscribers-data-views.tsx b/projects/packages/newsletter/_inc/subscribers/components/subscribers-data-views.tsx index 4cd4eacf0a9..0995ebc9286 100644 --- a/projects/packages/newsletter/_inc/subscribers/components/subscribers-data-views.tsx +++ b/projects/packages/newsletter/_inc/subscribers/components/subscribers-data-views.tsx @@ -2,6 +2,7 @@ import { DataViews } from '@wordpress/dataviews'; import { useCallback, useMemo, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Notice } from '@wordpress/ui'; +import { useMembershipsProducts } from '../data/use-memberships-products'; import { useSubscriberRemoveMutation } from '../data/use-subscriber-remove-mutation'; import { useSubscribers } from '../data/use-subscribers'; import { @@ -92,6 +93,20 @@ export default function SubscribersDataViews( { const { data, isLoading, error } = useSubscribers( queryParams ); const removeMutation = useSubscriberRemoveMutation(); + // Fetch the site's paid products once for the whole table (not per row) so the "Comp a + // subscription" action can be hidden when there's nothing to comp onto — otherwise it opens a + // dead-end modal that only reports "no paid plans". The Subscribers tab is already gated behind + // a WordPress.com connection, so the proxied request is safe to fire eagerly. + const { data: membershipsProducts, isError: membershipsProductsError } = + useMembershipsProducts( true ); + const hasPaidProducts = ( membershipsProducts?.length ?? 0 ) > 0; + // Offer the action when we know there's a paid product to comp onto, OR when we couldn't + // determine it because the products request errored. Failing open on error preserves the + // capability and lets the modal surface the fetch error, rather than silently removing the + // action on a transient failure. It stays hidden only while the request is still loading and + // when the site genuinely has zero paid products (the dead-end case this fix targets). + const canShowCompAction = hasPaidProducts || membershipsProductsError; + // Fired off `onChangeView` rather than per-control handlers because DataViews owns // the controls — we diff the previous view against the next to mirror Calypso's // per-interaction Tracks events. @@ -216,11 +231,13 @@ export default function SubscribersDataViews( { { id: 'comp', label: __( 'Comp a subscription', 'jetpack-newsletter' ), - // We need a wpcom user id to attach the comp to (Calypso's - // `hasUncompedPlans` also checks the plans list, but that requires the site's - // products to be loaded — we let the modal handle the "all comped" / - // "no paid plans" edge cases instead). - isEligible: ( subscriber: Subscriber ) => !! subscriber.user_id, + // Needs a wpcom user id to attach the comp to, plus a paid product to comp onto — + // otherwise the modal is a dead-end that only reports "no paid plans". + // `canShowCompAction` comes from a single table-level fetch (see above: true when + // products exist or the fetch errored, false while loading or on a genuinely empty + // site), so this stays cheap per row. The modal still handles the per-subscriber + // "already comped on every plan" edge case. + isEligible: ( subscriber: Subscriber ) => !! subscriber.user_id && canShowCompAction, callback: ( items: Subscriber[] ) => { const target = items[ 0 ]; if ( ! target ) { @@ -261,7 +278,7 @@ export default function SubscribersDataViews( { }, }, ], - [ onViewSubscriber ] + [ onViewSubscriber, canShowCompAction ] ); const handleConfirmRemoval = useCallback( async () => { diff --git a/projects/packages/newsletter/_inc/subscribers/data/use-memberships-products.ts b/projects/packages/newsletter/_inc/subscribers/data/use-memberships-products.ts index b5ba6b9a559..5a0c43c93db 100644 --- a/projects/packages/newsletter/_inc/subscribers/data/use-memberships-products.ts +++ b/projects/packages/newsletter/_inc/subscribers/data/use-memberships-products.ts @@ -4,9 +4,10 @@ import type { MembershipsProduct } from './api'; /** * Fetch the paid newsletter / membership products configured on this site. Lazy — only runs - * when the consumer asks for it (the Comp modal needs it; nothing else does). + * when the consumer asks for it. The Subscribers table fetches it to decide whether to offer the + * "Comp a subscription" action; the Comp modal reuses the same cached result. * - * @param enabled - Whether the request should run (typically `true` once the modal is open). + * @param enabled - Whether the request should run. * @return React Query handle. */ export function useMembershipsProducts( enabled: boolean ) { diff --git a/projects/packages/newsletter/changelog/change-newsletter-comp-gate-paid-plans b/projects/packages/newsletter/changelog/change-newsletter-comp-gate-paid-plans new file mode 100644 index 00000000000..9329d6478ba --- /dev/null +++ b/projects/packages/newsletter/changelog/change-newsletter-comp-gate-paid-plans @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Subscribers: Hide the "Comp a subscription" action when the site has no paid newsletter plans.