From e4088070b42f912ba70b7f7537fe1377ce8da533 Mon Sep 17 00:00:00 2001
From: Addison Stavlo
Date: Tue, 24 Feb 2026 10:45:36 -0500
Subject: [PATCH 1/6] add previous send awareness to newsletter panel
---
.../memberships/subscribers-affirmation.js | 376 +++++++++++++++---
.../store/membership-products/actions.js | 10 +-
.../store/membership-products/reducer.js | 14 +-
.../store/membership-products/resolvers.js | 81 ++--
.../store/membership-products/selectors.js | 9 +-
.../membership-products/test/actions-test.js | 12 -
.../membership-products/test/reducer-test.js | 15 -
.../test/selectors-test.js | 9 -
8 files changed, 393 insertions(+), 133 deletions(-)
diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
index 9ced31e5f14d..7a1333a976ce 100644
--- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
+++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
@@ -3,7 +3,7 @@ import { isComingSoon } from '@automattic/jetpack-shared-extension-utils';
import { Animate } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
-import { createInterpolateElement } from '@wordpress/element';
+import { createInterpolateElement, useRef, useEffect } from '@wordpress/element';
import { sprintf, __, _n } from '@wordpress/i18n';
import paywallBlockMetadata from '../../blocks/paywall/block.json';
import {
@@ -164,36 +164,193 @@ export const getCopyForSubscribers = ( {
);
};
+const SENDING_IN_PROGRESS_WINDOW_MS = 15 * 60 * 1000;
+
+/**
+ * Format sent date from Unix timestamp or MySQL timestamp string.
+ *
+ * @param {number|null} emailSentAt - Unix timestamp from email_notification meta
+ * @param {string|null} statsTimestamp - MySQL timestamp from stats_on_send
+ * @return {string} Formatted date string
+ */
+function formatSentDate( emailSentAt, statsTimestamp ) {
+ let date = null;
+ if ( emailSentAt ) {
+ date = new Date( emailSentAt * 1000 );
+ } else if ( statsTimestamp ) {
+ date = new Date( statsTimestamp );
+ }
+ if ( ! date || isNaN( date.getTime() ) ) {
+ return '';
+ }
+ return date.toLocaleDateString( undefined, {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ } );
+}
+
+/**
+ * Get category names from stats post_categories (term IDs) using newsletter categories list.
+ *
+ * @param {Array} postCategories - Term IDs from stats_on_send
+ * @param {Array} newsletterCategories - Site newsletter categories with id, name
+ * @return {string} Formatted category list (e.g. "Category A and Category B" or "All content")
+ */
+function getCategoryNamesFromStats( postCategories, newsletterCategories ) {
+ if ( ! postCategories?.length ) {
+ return '';
+ }
+ const categoryNames = postCategories
+ .map( termId => {
+ const cat = newsletterCategories.find( c => c.id === termId );
+ return cat ? cat.name : null;
+ } )
+ .filter( Boolean );
+ const hasUnknown = postCategories.some(
+ termId => ! newsletterCategories.some( c => c.id === termId )
+ );
+ if ( hasUnknown ) {
+ categoryNames.push( __( 'All content', 'jetpack' ) );
+ }
+ const formatted = categoryNames.map( n => `${ n }` );
+ if ( formatted.length === 1 ) return formatted[ 0 ];
+ if ( formatted.length === 2 ) {
+ return sprintf(
+ /* translators: %1$s: first category, %2$s: second category */
+ __( '%1$s and %2$s', 'jetpack' ),
+ formatted[ 0 ],
+ formatted[ 1 ]
+ );
+ }
+ const allButLast = formatted.slice( 0, -1 ).join( `${ __( ',', 'jetpack' ) } ` );
+ return sprintf(
+ /* translators: %1$s: comma-separated categories, %2$s: last category */
+ __( '%1$s, and %2$s', 'jetpack' ),
+ allButLast,
+ formatted[ formatted.length - 1 ]
+ );
+}
+
+/**
+ * Get access level label for "was emailed to X" copy from stats access_level string.
+ *
+ * @param {string} accessLevel - e.g. 'everybody', 'subscribers', 'paid_subscribers'
+ * @return {string} Access level label for display (e.g. "all subscribers", "paid subscribers").
+ */
+function getAccessLevelLabelFromStats( accessLevel ) {
+ if ( ! accessLevel ) return __( 'all subscribers', 'jetpack' );
+ const key = accessLevel.startsWith( 'paid_subscribers' ) ? 'paid_subscribers' : accessLevel;
+ switch ( key ) {
+ case 'everybody':
+ return __( 'all subscribers', 'jetpack' );
+ case 'subscribers':
+ return __( 'all subscribers', 'jetpack' );
+ case 'paid_subscribers':
+ return __( 'paid subscribers', 'jetpack' );
+ default:
+ return __( 'all subscribers', 'jetpack' );
+ }
+}
+
+/**
+ * Build "was sent" or "is being sent" copy for access + categories.
+ *
+ * @param {object} opts
+ * @param {string} opts.accessLabel - "all subscribers" or "paid subscribers"
+ * @param {string} opts.categoryNames - Formatted category list (or empty)
+ * @param {boolean} opts.pastTense - "was emailed" vs "is being emailed"
+ * @param {string} opts.dateStr - For past tense only
+ * @return {string} Formatted sentence for "was sent" or "is being sent" copy.
+ */
+function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) {
+ if ( categoryNames ) {
+ if ( pastTense ) {
+ return sprintf(
+ /* translators: %1$s: access (e.g. "all subscribers"), %2$s: category list, %3$s: date */
+ __(
+ 'This post was emailed to %1$s subscribers of %2$s on %3$s. View delivery details on your email stats page.',
+ 'jetpack'
+ ),
+ accessLabel,
+ categoryNames,
+ dateStr
+ );
+ }
+ return sprintf(
+ /* translators: %1$s: access, %2$s: category list */
+ __(
+ 'This post is being emailed to %1$s subscribers of %2$s. Delivery details can be seen on your email stats page shortly.',
+ 'jetpack'
+ ),
+ accessLabel,
+ categoryNames
+ );
+ }
+ if ( pastTense ) {
+ return sprintf(
+ /* translators: %1$s: access, %2$s: date */
+ __(
+ 'This post was emailed to %1$s subscribers on %2$s. View delivery details on your email stats page.',
+ 'jetpack'
+ ),
+ accessLabel,
+ dateStr
+ );
+ }
+ return sprintf(
+ /* translators: %s: access level */
+ __(
+ 'This post is being emailed to %s subscribers. Delivery details can be seen on your email stats page shortly.',
+ 'jetpack'
+ ),
+ accessLabel
+ );
+}
+
/*
* Determines copy to show in pre/post-publish panels to confirm number and type of subscribers receiving the post as email.
*/
function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
+ const wasPublishedOnLoad = useRef( undefined );
+ const transitionedToPublishInSession = useRef( false );
+
const postHasPaywallBlock = useSelect( select =>
select( 'core/block-editor' )
.getBlocks()
.some( block => block.name === paywallBlockMetadata.name )
);
- const { isScheduledPost, isPostOlderThanADay, postCategories, postId, postMeta } = useSelect(
+ const { isScheduledPost, postCategories, postId, postMeta, publishDate, status } = useSelect(
select => {
const { isCurrentPostScheduled, getEditedPostAttribute, getCurrentPost } =
select( editorStore );
- const status = getCurrentPost()?.status;
- const publishTime = new Date( getCurrentPost()?.date );
- const time24HoursAgo = new Date( Date.now() - 24 * 60 * 60 * 1000 );
+ const post = getCurrentPost();
+ const statusVal = post?.status;
+ const dateVal = post?.date;
+ const publishTime = dateVal ? new Date( dateVal ) : null;
return {
isScheduledPost: isCurrentPostScheduled(),
- isPostOlderThanADay: status === 'publish' && publishTime < time24HoursAgo,
postCategories: getEditedPostAttribute( 'categories' ),
- postId: getCurrentPost()?.id,
+ postId: post?.id,
postMeta: getEditedPostAttribute( 'meta' ),
+ publishDate: publishTime,
+ status: statusVal,
};
}
);
+ useEffect( () => {
+ if ( status === 'publish' ) {
+ if ( wasPublishedOnLoad.current === undefined ) {
+ wasPublishedOnLoad.current = true;
+ }
+ transitionedToPublishInSession.current = true;
+ }
+ }, [ status ] );
+
const isSendEmailEnabled = () => {
- // Meta value is negated, "don't send", but toggle is truthy when enabled "send"
return ! postMeta?.[ META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS ];
};
@@ -205,35 +362,47 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
newsletterCategoriesEnabled,
newsletterCategorySubscriberCount,
paidSubscribersCount,
- totalEmailsSentCount, // To display stats from Jetpack. It is necessary to provide the accurate count for historical posts.
- } = useSelect( select => {
- const {
- getNewsletterCategories,
- getNewsletterCategoriesEnabled,
- getNewsletterCategoriesSubscriptionsCount,
- getSubscriberCounts,
- getTotalEmailsSentCount,
- hasFinishedResolution,
- } = select( membershipProductsStore );
-
- // Free and paid subscriber counts
- const { emailSubscribers, paidSubscribers } = getSubscriberCounts();
-
- return {
- hasFinishedLoading: [
- // getNewsletterCategoriesEnabled state is set by getNewsletterCategories so no need to check for it here.
- hasFinishedResolution( 'getSubscriberCounts' ),
- hasFinishedResolution( 'getNewsletterCategories' ),
- hasFinishedResolution( 'getNewsletterCategoriesSubscriptionsCount' ),
- ].every( Boolean ),
- emailSubscribersCount: emailSubscribers,
- newsletterCategories: getNewsletterCategories(),
- newsletterCategoriesEnabled: getNewsletterCategoriesEnabled(),
- newsletterCategorySubscriberCount: getNewsletterCategoriesSubscriptionsCount(),
- paidSubscribersCount: paidSubscribers,
- totalEmailsSentCount: isPostOlderThanADay ? getTotalEmailsSentCount( blogId, postId ) : null,
- };
- } );
+ postEmailSentState,
+ } = useSelect(
+ select => {
+ const {
+ getNewsletterCategories,
+ getNewsletterCategoriesEnabled,
+ getNewsletterCategoriesSubscriptionsCount,
+ getPostEmailSentState,
+ getSubscriberCounts,
+ hasFinishedResolution,
+ } = select( membershipProductsStore );
+
+ const { emailSubscribers, paidSubscribers } = getSubscriberCounts();
+
+ // Trigger fetch when post is published so we have email_sent_at / stats_on_send
+ if ( status === 'publish' && postId ) {
+ getPostEmailSentState( postId );
+ }
+
+ const postEmailResolved =
+ status !== 'publish' ||
+ ! postId ||
+ hasFinishedResolution( 'getPostEmailSentState', [ postId ] );
+
+ return {
+ hasFinishedLoading: [
+ hasFinishedResolution( 'getSubscriberCounts' ),
+ hasFinishedResolution( 'getNewsletterCategories' ),
+ hasFinishedResolution( 'getNewsletterCategoriesSubscriptionsCount' ),
+ postEmailResolved,
+ ].every( Boolean ),
+ emailSubscribersCount: emailSubscribers,
+ newsletterCategories: getNewsletterCategories(),
+ newsletterCategoriesEnabled: getNewsletterCategoriesEnabled(),
+ newsletterCategorySubscriberCount: getNewsletterCategoriesSubscriptionsCount(),
+ paidSubscribersCount: paidSubscribers,
+ postEmailSentState: status === 'publish' && postId ? getPostEmailSentState( postId ) : null,
+ };
+ },
+ [ status, postId ]
+ );
if ( ! hasFinishedLoading ) {
return (
@@ -248,38 +417,154 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
}
const isPaidPost = accessLevel === accessOptions.paid_subscribers.key;
-
- // Show all copy in future tense
const futureTense = prePublish || isScheduledPost;
+ const emailSentAt = postEmailSentState?.email_sent_at ?? null;
+ const statsOnSend = postEmailSentState?.stats_on_send ?? null;
+
+ const isAlreadySent = emailSentAt != null;
+ const isSendingInProgress =
+ status === 'publish' &&
+ isSendEmailEnabled() &&
+ emailSentAt == null &&
+ ( transitionedToPublishInSession.current ||
+ ( wasPublishedOnLoad.current &&
+ publishDate &&
+ publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) );
+ const isPublishedNotSent =
+ status === 'publish' && isSendEmailEnabled() && emailSentAt == null && ! isSendingInProgress;
+
const reachForAccessLevel = getReachForAccessLevelKey( {
accessLevel,
- subscribers: isPostOlderThanADay ? totalEmailsSentCount : emailSubscribersCount,
+ subscribers: emailSubscribersCount,
paidSubscribers: paidSubscribersCount,
postHasPaywallBlock,
} );
let text;
+ let append = '';
if ( ! isSendEmailEnabled() ) {
- text = __( 'Not sent via email.', 'jetpack' );
+ // "Post only" but already emailed: show "was sent" copy, not "Not sent via email"
+ if ( isAlreadySent && statsOnSend ) {
+ const dateStr = formatSentDate( emailSentAt, statsOnSend.timestamp );
+ const accessLabel = getAccessLevelLabelFromStats( statsOnSend.access_level );
+ const categoryNames = getCategoryNamesFromStats(
+ statsOnSend.post_categories,
+ newsletterCategories
+ );
+ text = getSentCopyLine( {
+ accessLabel,
+ categoryNames,
+ pastTense: true,
+ dateStr,
+ } );
+ } else if ( isAlreadySent ) {
+ text = sprintf(
+ /* translators: %s: formatted date */
+ __(
+ 'This post was emailed on %s. View delivery details on your email stats page.',
+ 'jetpack'
+ ),
+ formatSentDate( emailSentAt, null )
+ );
+ } else {
+ text = __( 'Not sent via email.', 'jetpack' );
+ }
} else if ( isComingSoon() ) {
text = __(
'Your site is in Coming Soon mode. Emails are sent only when your site is public.',
'jetpack'
);
+ } else if ( isAlreadySent ) {
+ const dateStr = formatSentDate( emailSentAt, statsOnSend?.timestamp ?? null );
+ if ( statsOnSend ) {
+ const accessLabel = getAccessLevelLabelFromStats( statsOnSend.access_level );
+ const categoryNames = getCategoryNamesFromStats(
+ statsOnSend.post_categories,
+ newsletterCategories
+ );
+ text = getSentCopyLine( {
+ accessLabel,
+ categoryNames,
+ pastTense: true,
+ dateStr,
+ } );
+ } else {
+ text = sprintf(
+ /* translators: %s: formatted date */
+ __(
+ 'This post was emailed on %s. View delivery details on your email stats page.',
+ 'jetpack'
+ ),
+ dateStr
+ );
+ }
+ if ( transitionedToPublishInSession.current ) {
+ append = __( 'Updating or republishing does not send a new email.', 'jetpack' );
+ }
+ const statsAccess = statsOnSend?.access_level;
+ const statsCats = statsOnSend?.post_categories ?? [];
+ const accessMatches =
+ ! statsAccess || statsAccess === accessLevel || statsAccess.startsWith( accessLevel );
+ const categoriesMatch =
+ ! statsOnSend?.has_newsletter_categories ||
+ ( Array.isArray( postCategories ) &&
+ statsCats.length === postCategories.length &&
+ statsCats.every( ( id, i ) => postCategories[ i ] === id ) );
+ if ( ! accessMatches || ! categoriesMatch ) {
+ append = append
+ ? append + ' ' + __( 'Changing access settings does not resend the email.', 'jetpack' )
+ : __( 'Changing access settings does not resend the email.', 'jetpack' );
+ }
+ } else if ( isSendingInProgress ) {
+ const accessLabel = getAccessLevelLabelFromStats( accessLevel );
+ const categoryNames =
+ newsletterCategoriesEnabled && newsletterCategories?.length && postCategories?.length
+ ? getFormattedCategories( postCategories, newsletterCategories )
+ : '';
+ text = getSentCopyLine( {
+ accessLabel,
+ categoryNames,
+ pastTense: false,
+ dateStr: '',
+ } );
+ } else if ( isPublishedNotSent ) {
+ text = __(
+ "This post was published without sending an email. To send, move the post to draft, enable 'Post and email,' and republish.",
+ 'jetpack'
+ );
+ } else if ( futureTense ) {
+ // Pre-send: "will be sent" with subscriber counts
+ if ( newsletterCategoriesEnabled && newsletterCategories.length > 0 && ! isPaidPost ) {
+ text = getCopyForCategorySubscribers( {
+ futureTense: true,
+ isPaidPost,
+ newsletterCategories,
+ postCategories,
+ reachCount: newsletterCategorySubscriberCount,
+ } );
+ } else {
+ text = getCopyForSubscribers( {
+ futureTense: true,
+ isPaidPost,
+ postHasPaywallBlock,
+ reachCount: reachForAccessLevel,
+ } );
+ }
} else if ( newsletterCategoriesEnabled && newsletterCategories.length > 0 && ! isPaidPost ) {
- // Get newsletter category copy & count separately, unless post is paid
+ // Published, not sent, not sending in progress (fallback) — category subscribers
text = getCopyForCategorySubscribers( {
- futureTense,
+ futureTense: false,
isPaidPost,
newsletterCategories,
postCategories,
- reachCount: isPostOlderThanADay ? totalEmailsSentCount : newsletterCategorySubscriberCount,
+ reachCount: newsletterCategorySubscriberCount,
} );
} else {
+ // Published, not sent, not sending in progress (fallback) — all/paid subscribers
text = getCopyForSubscribers( {
- futureTense,
+ futureTense: false,
isPaidPost,
postHasPaywallBlock,
reachCount: reachForAccessLevel,
@@ -292,6 +577,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
strong: ,
link: ,
} ) }
+ { append ? <> { createInterpolateElement( append, { strong: } ) }> : null }
);
}
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/actions.js b/projects/plugins/jetpack/extensions/store/membership-products/actions.js
index 69074677ac4e..75bdc8018c59 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/actions.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/actions.js
@@ -110,11 +110,6 @@ export const setSubscriberCounts = subscriberCounts => ( {
subscriberCounts,
} );
-export const setTotalEmailsSentCount = totalEmailsSentCount => ( {
- type: 'SET_TOTAL_EMAILS_SENT_COUNT',
- totalEmailsSentCount,
-} );
-
export const setNewsletterCategories = newsletterCategories => ( {
type: 'SET_NEWSLETTER_CATEGORIES',
newsletterCategories,
@@ -125,3 +120,8 @@ export const setNewsletterCategoriesSubscriptionsCount =
type: 'SET_NEWSLETTER_CATEGORIES_SUBSCRIPTIONS_COUNT',
newsletterCategoriesSubscriptionsCount,
} );
+
+export const setPostEmailSentState = ( { email_sent_at, stats_on_send } ) => ( {
+ type: 'SET_POST_EMAIL_SENT_STATE',
+ payload: { email_sent_at, stats_on_send },
+} );
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js
index e85fbf9fc528..47f6d2ed0d96 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js
@@ -16,6 +16,10 @@ export const DEFAULT_STATE = {
enabled: false,
categories: [],
},
+ postEmailSentState: {
+ email_sent_at: null,
+ stats_on_send: null,
+ },
};
export default function reducer( state = DEFAULT_STATE, action ) {
@@ -38,11 +42,6 @@ export default function reducer( state = DEFAULT_STATE, action ) {
...state,
subscriberCounts: action.subscriberCounts,
};
- case 'SET_TOTAL_EMAILS_SENT_COUNT':
- return {
- ...state,
- totalEmailsSentCount: action.totalEmailsSentCount,
- };
case 'SET_NEWSLETTER_CATEGORIES':
return {
...state,
@@ -53,6 +52,11 @@ export default function reducer( state = DEFAULT_STATE, action ) {
...state,
newsletterCategoriesSubscriptionsCount: action.newsletterCategoriesSubscriptionsCount,
};
+ case 'SET_POST_EMAIL_SENT_STATE':
+ return {
+ ...state,
+ postEmailSentState: action.payload,
+ };
}
return state;
}
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js
index d943c52a1d8f..a7e557d8b8aa 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js
@@ -1,4 +1,3 @@
-import { isSimpleSite } from '@automattic/jetpack-script-data';
import apiFetch from '@wordpress/api-fetch';
import { store as editorStore } from '@wordpress/editor';
import { addQueryArgs, getQueryArg } from '@wordpress/url';
@@ -16,19 +15,19 @@ import {
setSubscriberCounts,
setNewsletterCategories,
setNewsletterCategoriesSubscriptionsCount,
- setTotalEmailsSentCount,
+ setPostEmailSentState,
} from './actions';
import { API_STATE_CONNECTED, API_STATE_NOTCONNECTED } from './constants';
import { onError } from './utils';
const EXECUTION_KEY = 'membership-products-resolver-getProducts';
const SUBSCRIBER_COUNT_EXECUTION_KEY = 'membership-products-resolver-getSubscriberCounts';
-const TOTAL_EMAILS_SENT_COUNT_EXECUTION_KEY =
- 'membership-products-resolver-getTotalEmailsSentCount';
const GET_NEWSLETTER_CATEGORIES_EXECUTION_KEY =
'membership-products-resolver-getNewsletterCategories';
const GET_NEWSLETTER_CATEGORIES_SUBSCRIPTIONS_COUNT_EXECUTION_KEY =
'membership-products-resolver-getNewsletterCategoriesSubscriptionsCount';
+const GET_POST_EMAIL_SENT_STATE_EXECUTION_KEY =
+ 'membership-products-resolver-getPostEmailSentState';
let hydratedFromAPI = false;
const fetchMemberships = async () => {
@@ -101,14 +100,9 @@ const fetchSubscriberCounts = async () => {
return response;
};
-const fetchTotalEmailsSentCount = async ( blogId, postId ) => {
- if ( ! blogId || ! postId ) {
- return;
- }
-
- const baseUrl = isSimpleSite() ? '/rest/v1.1/sites' : '/jetpack/v4/stats-app/sites';
+const fetchNewsletterCategories = async () => {
const response = await apiFetch( {
- path: baseUrl + `/${ blogId }/stats/opens/emails/${ postId }/rate`,
+ path: '/wpcom/v2/newsletter-categories',
} );
if ( ! response || typeof response !== 'object' ) {
@@ -131,9 +125,10 @@ const fetchTotalEmailsSentCount = async ( blogId, postId ) => {
return response;
};
-const fetchNewsletterCategories = async () => {
+export const fetchNewsletterCategoriesSubscriptionsCount = async termIds => {
const response = await apiFetch( {
- path: '/wpcom/v2/newsletter-categories',
+ path: `/wpcom/v2/newsletter-categories/count?term_ids=${ termIds.join( ',' ) }`,
+ method: 'GET',
} );
if ( ! response || typeof response !== 'object' ) {
@@ -156,9 +151,13 @@ const fetchNewsletterCategories = async () => {
return response;
};
-export const fetchNewsletterCategoriesSubscriptionsCount = async termIds => {
+const fetchPostEmailSentState = async postId => {
+ if ( ! postId ) {
+ return { email_sent_at: null, stats_on_send: null };
+ }
+
const response = await apiFetch( {
- path: `/wpcom/v2/newsletter-categories/count?term_ids=${ termIds.join( ',' ) }`,
+ path: `/wpcom/v2/newsletter-email-sent-status?post_id=${ postId }`,
method: 'GET',
} );
@@ -166,14 +165,6 @@ export const fetchNewsletterCategoriesSubscriptionsCount = async termIds => {
throw new Error( 'Unexpected API response' );
}
- /**
- * WP_Error returns a list of errors with custom names:
- * `errors: { foo: [ 'message' ], bar: [ 'message' ] }`
- * Since we don't know their names, to get the message, we transform the object
- * into an array, and just pick the first message of the first error.
- *
- * @see https://developer.wordpress.org/reference/classes/wp_error/
- */
const wpError = response?.errors && Object.values( response.errors )?.[ 0 ]?.[ 0 ];
if ( wpError ) {
throw new Error( wpError );
@@ -288,23 +279,6 @@ export const getSubscriberCounts =
}
};
-export const getTotalEmailsSentCount =
- ( blogId, postId ) =>
- async ( { dispatch, registry } ) => {
- await executionLock.blockExecution( TOTAL_EMAILS_SENT_COUNT_EXECUTION_KEY );
-
- const lock = executionLock.acquire( TOTAL_EMAILS_SENT_COUNT_EXECUTION_KEY );
- try {
- const response = await fetchTotalEmailsSentCount( blogId, postId );
- dispatch( setTotalEmailsSentCount( response?.total_sends ) );
- } catch ( error ) {
- dispatch( setApiState( API_STATE_NOTCONNECTED ) );
- onError( error.message, registry );
- } finally {
- executionLock.release( lock );
- }
- };
-
export const getNewsletterCategories =
() =>
async ( { dispatch, registry } ) => {
@@ -349,3 +323,30 @@ export const getNewsletterCategoriesSubscriptionsCount =
executionLock.release( lock );
}
};
+
+export const getPostEmailSentState =
+ postId =>
+ async ( { dispatch, registry } ) => {
+ if ( ! postId ) {
+ dispatch( setPostEmailSentState( { email_sent_at: null, stats_on_send: null } ) );
+ return;
+ }
+
+ await executionLock.blockExecution( GET_POST_EMAIL_SENT_STATE_EXECUTION_KEY );
+
+ const lock = executionLock.acquire( GET_POST_EMAIL_SENT_STATE_EXECUTION_KEY );
+ try {
+ const response = await fetchPostEmailSentState( postId );
+ dispatch(
+ setPostEmailSentState( {
+ email_sent_at: response.email_sent_at ?? null,
+ stats_on_send: response.stats_on_send ?? null,
+ } )
+ );
+ } catch ( error ) {
+ dispatch( setApiState( API_STATE_NOTCONNECTED ) );
+ onError( error.message, registry );
+ } finally {
+ executionLock.release( lock );
+ }
+ };
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js
index 6207c7803a0c..125d3cd27f1a 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js
@@ -30,8 +30,6 @@ export const isInvalidProduct = ( state, productId ) =>
export const getSubscriberCounts = state => state.subscriberCounts;
-export const getTotalEmailsSentCount = state => state.totalEmailsSentCount;
-
export const getNewsletterCategories = state => state.newsletterCategories.categories;
export const getNewsletterCategoriesEnabled = state => state.newsletterCategories.enabled;
@@ -39,6 +37,13 @@ export const getNewsletterCategoriesEnabled = state => state.newsletterCategorie
export const getNewsletterCategoriesSubscriptionsCount = state =>
state.newsletterCategoriesSubscriptionsCount;
+export const getPostEmailSentState = ( state, postId ) => {
+ if ( ! postId ) {
+ return { email_sent_at: null, stats_on_send: null };
+ }
+ return state.postEmailSentState || { email_sent_at: null, stats_on_send: null };
+};
+
export const hasInvalidProducts = ( state, selectedProductIds ) => {
return (
!! selectedProductIds &&
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js b/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js
index 665e3e531dcb..3096d91d11dd 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/test/actions-test.js
@@ -11,7 +11,6 @@ import {
setConnectedAccountDefaultCurrency,
setNewsletterCategories,
setNewsletterCategoriesSubscriptionsCount,
- setTotalEmailsSentCount,
} from '../actions';
import * as utils from '../utils';
@@ -331,15 +330,4 @@ describe( 'Membership Products Actions', () => {
// Then
expect( result ).toStrictEqual( anyValidNewsletterCategoriesSubscriptionsCountWithType );
} );
-
- test( 'Set total emails sent count works as expected', () => {
- const anyValidTotalEmailsSentCountWithType = {
- type: 'SET_TOTAL_EMAILS_SENT_COUNT',
- totalEmailsSentCount: ANY_VALID_DATA,
- };
-
- const result = setTotalEmailsSentCount( ANY_VALID_DATA );
-
- expect( result ).toStrictEqual( anyValidTotalEmailsSentCountWithType );
- } );
} );
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js b/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js
index 3eab2ac47b10..4769f6eeecb1 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/test/reducer-test.js
@@ -148,19 +148,4 @@ describe( 'Membership products reducer testing', () => {
newsletterCategoriesSubscriptionsCount: anyNewsletterCategoriesSubscriptionsCount,
} );
} );
-
- test( 'SET_TOTAL_EMAILS_SENT_COUNT action adds the total emails sent count to the returned state.', () => {
- const anyTotalEmailsSentCount = 10;
- const anySetTotalEmailsSentCountAction = {
- type: 'SET_TOTAL_EMAILS_SENT_COUNT',
- totalEmailsSentCount: anyTotalEmailsSentCount,
- };
-
- const returnedState = reducer( DEFAULT_STATE, anySetTotalEmailsSentCountAction );
-
- expect( returnedState ).toStrictEqual( {
- ...DEFAULT_STATE,
- totalEmailsSentCount: anyTotalEmailsSentCount,
- } );
- } );
} );
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js b/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js
index 070eef4a65b2..3844e55f4a95 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/test/selectors-test.js
@@ -4,7 +4,6 @@ import {
getNewsletterTierProducts,
getNewsletterCategoriesSubscriptionsCount,
getProducts,
- getTotalEmailsSentCount,
} from '../selectors';
describe( 'Membership Products Selectors', () => {
@@ -57,12 +56,4 @@ describe( 'Membership Products Selectors', () => {
state.newsletterCategoriesSubscriptionsCount
);
} );
-
- test( 'getTotalEmailsSentCount works as expected', () => {
- const state = {
- totalEmailsSentCount: 100,
- };
-
- expect( getTotalEmailsSentCount( state ) ).toStrictEqual( state.totalEmailsSentCount );
- } );
} );
From cb477dc9bdebe6f7545e210f783a7b8c42ae9d1c Mon Sep 17 00:00:00 2001
From: Addison Stavlo
Date: Tue, 24 Feb 2026 10:47:28 -0500
Subject: [PATCH 2/6] Add changelog entries.
---
.../changelog/update-newsletter-editor-panel-sent-awareness | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness
diff --git a/projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness b/projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness
new file mode 100644
index 000000000000..5af6b0e5fbc7
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/update-newsletter-editor-panel-sent-awareness
@@ -0,0 +1,4 @@
+Significance: minor
+Type: other
+
+Newsletter editor panel: update to reflect previous email sends and update copies.
From 176b570f1a4a17bc65a37fba73b40c8e6d76bf28 Mon Sep 17 00:00:00 2001
From: Addison Stavlo
Date: Wed, 25 Feb 2026 09:16:07 -0500
Subject: [PATCH 3/6] add stats page links, fix state bugs
---
.../memberships/subscribers-affirmation.js | 21 +++++++++++--------
.../store/membership-products/resolvers.js | 1 -
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
index 7a1333a976ce..349fad0cfd93 100644
--- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
+++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
@@ -269,7 +269,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) {
return sprintf(
/* translators: %1$s: access (e.g. "all subscribers"), %2$s: category list, %3$s: date */
__(
- 'This post was emailed to %1$s subscribers of %2$s on %3$s. View delivery details on your email stats page.',
+ 'This post was emailed to %1$s of %2$s on %3$s. View delivery details on your email stats page.',
'jetpack'
),
accessLabel,
@@ -280,7 +280,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) {
return sprintf(
/* translators: %1$s: access, %2$s: category list */
__(
- 'This post is being emailed to %1$s subscribers of %2$s. Delivery details can be seen on your email stats page shortly.',
+ 'This post is being emailed to %1$s of %2$s. Delivery details can be seen on your email stats page shortly.',
'jetpack'
),
accessLabel,
@@ -291,7 +291,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) {
return sprintf(
/* translators: %1$s: access, %2$s: date */
__(
- 'This post was emailed to %1$s subscribers on %2$s. View delivery details on your email stats page.',
+ 'This post was emailed to %1$s on %2$s. View delivery details on your email stats page.',
'jetpack'
),
accessLabel,
@@ -301,7 +301,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) {
return sprintf(
/* translators: %s: access level */
__(
- 'This post is being emailed to %s subscribers. Delivery details can be seen on your email stats page shortly.',
+ 'This post is being emailed to %s. Delivery details can be seen on your email stats page shortly.',
'jetpack'
),
accessLabel
@@ -314,6 +314,7 @@ function getSentCopyLine( { accessLabel, categoryNames, pastTense, dateStr } ) {
function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
const wasPublishedOnLoad = useRef( undefined );
const transitionedToPublishInSession = useRef( false );
+ const prevStatusRef = useRef( null );
const postHasPaywallBlock = useSelect( select =>
select( 'core/block-editor' )
@@ -346,8 +347,11 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
if ( wasPublishedOnLoad.current === undefined ) {
wasPublishedOnLoad.current = true;
}
- transitionedToPublishInSession.current = true;
+ if ( prevStatusRef.current !== null && prevStatusRef.current !== 'publish' ) {
+ transitionedToPublishInSession.current = true;
+ }
}
+ prevStatusRef.current = status;
}, [ status ] );
const isSendEmailEnabled = () => {
@@ -383,8 +387,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
const postEmailResolved =
status !== 'publish' ||
- ! postId ||
- hasFinishedResolution( 'getPostEmailSentState', [ postId ] );
+ ( !! postId && hasFinishedResolution( 'getPostEmailSentState', [ postId ] ) );
return {
hasFinishedLoading: [
@@ -463,7 +466,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
text = sprintf(
/* translators: %s: formatted date */
__(
- 'This post was emailed on %s. View delivery details on your email stats page.',
+ 'This post was emailed on %s. View delivery details on your email stats page.',
'jetpack'
),
formatSentDate( emailSentAt, null )
@@ -494,7 +497,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
text = sprintf(
/* translators: %s: formatted date */
__(
- 'This post was emailed on %s. View delivery details on your email stats page.',
+ 'This post was emailed on %s. View delivery details on your email stats page.',
'jetpack'
),
dateStr
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js
index a7e557d8b8aa..6a111d1ed26a 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js
@@ -328,7 +328,6 @@ export const getPostEmailSentState =
postId =>
async ( { dispatch, registry } ) => {
if ( ! postId ) {
- dispatch( setPostEmailSentState( { email_sent_at: null, stats_on_send: null } ) );
return;
}
From 59a0df61a29c82d795aad8b8dc2707e87ef0c24d Mon Sep 17 00:00:00 2001
From: Addison Stavlo
Date: Wed, 25 Feb 2026 10:19:00 -0500
Subject: [PATCH 4/6] use redux store to persist info and fix more bugs
---
.../memberships/subscribers-affirmation.js | 63 ++++++++++++++-----
.../store/membership-products/actions.js | 10 +++
.../store/membership-products/reducer.js | 18 ++++++
.../store/membership-products/selectors.js | 10 +++
4 files changed, 84 insertions(+), 17 deletions(-)
diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
index 349fad0cfd93..74aa619ac087 100644
--- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
+++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
@@ -1,7 +1,7 @@
import { getAdminUrl } from '@automattic/jetpack-script-data';
import { isComingSoon } from '@automattic/jetpack-shared-extension-utils';
import { Animate } from '@wordpress/components';
-import { useSelect } from '@wordpress/data';
+import { useDispatch, useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { createInterpolateElement, useRef, useEffect } from '@wordpress/element';
import { sprintf, __, _n } from '@wordpress/i18n';
@@ -12,6 +12,10 @@ import {
} from '../../shared/memberships/constants';
import { getReachForAccessLevelKey } from '../../shared/memberships/settings';
import { store as membershipProductsStore } from '../../store/membership-products';
+import {
+ setPublishedWithEmailEnabledInSession,
+ setRepublishedAlreadySentPostInSession,
+} from '../../store/membership-products/actions';
/**
* Get the formatted list of categories for a post.
@@ -342,23 +346,12 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
}
);
- useEffect( () => {
- if ( status === 'publish' ) {
- if ( wasPublishedOnLoad.current === undefined ) {
- wasPublishedOnLoad.current = true;
- }
- if ( prevStatusRef.current !== null && prevStatusRef.current !== 'publish' ) {
- transitionedToPublishInSession.current = true;
- }
- }
- prevStatusRef.current = status;
- }, [ status ] );
-
const isSendEmailEnabled = () => {
return ! postMeta?.[ META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS ];
};
const blogId = window.Jetpack_Editor_Initial_State?.wpcomBlogId;
+ const dispatch = useDispatch( membershipProductsStore );
const {
emailSubscribersCount,
hasFinishedLoading,
@@ -367,6 +360,8 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
newsletterCategorySubscriberCount,
paidSubscribersCount,
postEmailSentState,
+ republishedAlreadySentInSession,
+ publishedWithEmailEnabledInSession,
} = useSelect(
select => {
const {
@@ -374,14 +369,16 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
getNewsletterCategoriesEnabled,
getNewsletterCategoriesSubscriptionsCount,
getPostEmailSentState,
+ getPublishedWithEmailEnabledInSession,
+ getRepublishedAlreadySentPostInSession,
getSubscriberCounts,
hasFinishedResolution,
} = select( membershipProductsStore );
const { emailSubscribers, paidSubscribers } = getSubscriberCounts();
- // Trigger fetch when post is published so we have email_sent_at / stats_on_send
- if ( status === 'publish' && postId ) {
+ // Trigger fetch when we have a postId so we have email_sent_at / stats_on_send (including for draft)
+ if ( postId ) {
getPostEmailSentState( postId );
}
@@ -401,12 +398,38 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
newsletterCategoriesEnabled: getNewsletterCategoriesEnabled(),
newsletterCategorySubscriberCount: getNewsletterCategoriesSubscriptionsCount(),
paidSubscribersCount: paidSubscribers,
- postEmailSentState: status === 'publish' && postId ? getPostEmailSentState( postId ) : null,
+ postEmailSentState: postId ? getPostEmailSentState( postId ) : null,
+ republishedAlreadySentInSession: postId
+ ? getRepublishedAlreadySentPostInSession( postId )
+ : false,
+ publishedWithEmailEnabledInSession: postId
+ ? getPublishedWithEmailEnabledInSession( postId )
+ : false,
};
},
[ status, postId ]
);
+ useEffect( () => {
+ if ( status === 'publish' ) {
+ if ( wasPublishedOnLoad.current === undefined ) {
+ wasPublishedOnLoad.current = true;
+ }
+ if ( prevStatusRef.current !== null && prevStatusRef.current !== 'publish' ) {
+ transitionedToPublishInSession.current = true;
+ if ( postId ) {
+ if ( postEmailSentState?.email_sent_at != null ) {
+ dispatch( setRepublishedAlreadySentPostInSession( postId ) );
+ }
+ if ( ! postMeta?.[ META_NAME_FOR_POST_DONT_EMAIL_TO_SUBS ] ) {
+ dispatch( setPublishedWithEmailEnabledInSession( postId ) );
+ }
+ }
+ }
+ }
+ prevStatusRef.current = status;
+ }, [ status, postId, postEmailSentState?.email_sent_at, dispatch, postMeta ] );
+
if ( ! hasFinishedLoading ) {
return (
@@ -431,6 +454,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
isSendEmailEnabled() &&
emailSentAt == null &&
( transitionedToPublishInSession.current ||
+ publishedWithEmailEnabledInSession ||
( wasPublishedOnLoad.current &&
publishDate &&
publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) );
@@ -471,6 +495,11 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
),
formatSentDate( emailSentAt, null )
);
+ } else if ( status === 'publish' ) {
+ text = __(
+ "This post was published without sending an email. To send, move the post to draft, enable 'Post and email,' and republish.",
+ 'jetpack'
+ );
} else {
text = __( 'Not sent via email.', 'jetpack' );
}
@@ -503,7 +532,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
dateStr
);
}
- if ( transitionedToPublishInSession.current ) {
+ if ( transitionedToPublishInSession.current || republishedAlreadySentInSession ) {
append = __( 'Updating or republishing does not send a new email.', 'jetpack' );
}
const statsAccess = statsOnSend?.access_level;
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/actions.js b/projects/plugins/jetpack/extensions/store/membership-products/actions.js
index 75bdc8018c59..b163d4b41991 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/actions.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/actions.js
@@ -125,3 +125,13 @@ export const setPostEmailSentState = ( { email_sent_at, stats_on_send } ) => ( {
type: 'SET_POST_EMAIL_SENT_STATE',
payload: { email_sent_at, stats_on_send },
} );
+
+export const setRepublishedAlreadySentPostInSession = postId => ( {
+ type: 'SET_REPUBLISHED_ALREADY_SENT_POST_IN_SESSION',
+ postId,
+} );
+
+export const setPublishedWithEmailEnabledInSession = postId => ( {
+ type: 'SET_PUBLISHED_WITH_EMAIL_ENABLED_IN_SESSION',
+ postId,
+} );
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js
index 47f6d2ed0d96..8cb56fbd1b4c 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js
@@ -20,6 +20,8 @@ export const DEFAULT_STATE = {
email_sent_at: null,
stats_on_send: null,
},
+ republishedAlreadySentPostInSession: {},
+ publishedWithEmailEnabledInSession: {},
};
export default function reducer( state = DEFAULT_STATE, action ) {
@@ -57,6 +59,22 @@ export default function reducer( state = DEFAULT_STATE, action ) {
...state,
postEmailSentState: action.payload,
};
+ case 'SET_REPUBLISHED_ALREADY_SENT_POST_IN_SESSION':
+ return {
+ ...state,
+ republishedAlreadySentPostInSession: {
+ ...state.republishedAlreadySentPostInSession,
+ [ action.postId ]: true,
+ },
+ };
+ case 'SET_PUBLISHED_WITH_EMAIL_ENABLED_IN_SESSION':
+ return {
+ ...state,
+ publishedWithEmailEnabledInSession: {
+ ...state.publishedWithEmailEnabledInSession,
+ [ action.postId ]: true,
+ },
+ };
}
return state;
}
diff --git a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js
index 125d3cd27f1a..6f401b32590f 100644
--- a/projects/plugins/jetpack/extensions/store/membership-products/selectors.js
+++ b/projects/plugins/jetpack/extensions/store/membership-products/selectors.js
@@ -44,6 +44,16 @@ export const getPostEmailSentState = ( state, postId ) => {
return state.postEmailSentState || { email_sent_at: null, stats_on_send: null };
};
+export const getRepublishedAlreadySentPostInSession = ( state, postId ) =>
+ !! (
+ state.republishedAlreadySentPostInSession && state.republishedAlreadySentPostInSession[ postId ]
+ );
+
+export const getPublishedWithEmailEnabledInSession = ( state, postId ) =>
+ !! (
+ state.publishedWithEmailEnabledInSession && state.publishedWithEmailEnabledInSession[ postId ]
+ );
+
export const hasInvalidProducts = ( state, selectedProductIds ) => {
return (
!! selectedProductIds &&
From 35797c531920033351ae69678c2731be8d3c4cd9 Mon Sep 17 00:00:00 2001
From: Addison Stavlo
Date: Wed, 25 Feb 2026 10:49:01 -0500
Subject: [PATCH 5/6] dummy arg to avoid bad minifcation of translated
strings...
---
.../extensions/shared/memberships/subscribers-affirmation.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
index 74aa619ac087..dfe12f70f50e 100644
--- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
+++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
@@ -498,7 +498,8 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
} else if ( status === 'publish' ) {
text = __(
"This post was published without sending an email. To send, move the post to draft, enable 'Post and email,' and republish.",
- 'jetpack'
+ 'jetpack',
+ 0 // dummy arg to avoid bad minification - https://github.com/Automattic/i18n-check-webpack-plugin?tab=readme-ov-file#conditional-function-call-compaction
);
} else {
text = __( 'Not sent via email.', 'jetpack' );
From d3bc57b8242be08a705db82f9678d9b1fc64224e Mon Sep 17 00:00:00 2001
From: Addison Stavlo
Date: Wed, 25 Feb 2026 15:10:54 -0500
Subject: [PATCH 6/6] use useLayoutEffect to resolve wrong state flicker
---
.../memberships/subscribers-affirmation.js | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
index dfe12f70f50e..7f6a9e07d319 100644
--- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
+++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js
@@ -3,7 +3,7 @@ import { isComingSoon } from '@automattic/jetpack-shared-extension-utils';
import { Animate } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
-import { createInterpolateElement, useRef, useEffect } from '@wordpress/element';
+import { createInterpolateElement, useRef, useLayoutEffect } from '@wordpress/element';
import { sprintf, __, _n } from '@wordpress/i18n';
import paywallBlockMetadata from '../../blocks/paywall/block.json';
import {
@@ -410,7 +410,7 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
[ status, postId ]
);
- useEffect( () => {
+ useLayoutEffect( () => {
if ( status === 'publish' ) {
if ( wasPublishedOnLoad.current === undefined ) {
wasPublishedOnLoad.current = true;
@@ -430,6 +430,17 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
prevStatusRef.current = status;
}, [ status, postId, postEmailSentState?.email_sent_at, dispatch, postMeta ] );
+ useLayoutEffect( () => {
+ if (
+ postId &&
+ status === 'publish' &&
+ transitionedToPublishInSession.current &&
+ postEmailSentState?.email_sent_at != null
+ ) {
+ dispatch( setRepublishedAlreadySentPostInSession( postId ) );
+ }
+ }, [ postId, status, postEmailSentState?.email_sent_at, dispatch ] );
+
if ( ! hasFinishedLoading ) {
return (
@@ -457,7 +468,8 @@ function SubscribersAffirmation( { accessLevel, prePublish = false } ) {
publishedWithEmailEnabledInSession ||
( wasPublishedOnLoad.current &&
publishDate &&
- publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) );
+ publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) ||
+ ( publishDate && publishDate.getTime() >= Date.now() - SENDING_IN_PROGRESS_WINDOW_MS ) );
const isPublishedNotSent =
status === 'publish' && isSendEmailEnabled() && emailSentAt == null && ! isSendingInProgress;