From cbcf644437e169c79f9af821fb2863acd843a1d0 Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Wed, 10 Jun 2026 12:43:13 -0300 Subject: [PATCH] Newsletter: hide Comp action when the site has no paid plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Comp a subscription" Subscribers action previously appeared for any subscriber with a wpcom user id, then opened a modal that only reported "no paid plans" when the site had none — a dead end. Fetch the site's membership products once at the table level and gate the action's availability on whether any paid product exists, so the action is only offered when there's something to comp onto. The per-row eligibility check reuses that single result, adding no per-row network requests. Remove the now-unreachable "no paid plans" notice from the Comp modal. --- .../components/modals/comp-modal.tsx | 10 ------- .../components/subscribers-data-views.tsx | 29 +++++++++++++++---- .../data/use-memberships-products.ts | 5 ++-- .../change-newsletter-comp-gate-paid-plans | 4 +++ 4 files changed, 30 insertions(+), 18 deletions(-) create mode 100644 projects/packages/newsletter/changelog/change-newsletter-comp-gate-paid-plans 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 e366eb5f537f..cd1f26132ac1 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 4cd4eacf0a9c..0995ebc9286a 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 b5ba6b9a559c..5a0c43c93db5 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 000000000000..9329d6478ba6 --- /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.