Skip to content

Newsletter editor panel - make aware of previous sends, update copies to be accurate#47301

Open
Addison-Stavlo wants to merge 19 commits intotrunkfrom
update/newsletter-editor-panel-sent-awareness
Open

Newsletter editor panel - make aware of previous sends, update copies to be accurate#47301
Addison-Stavlo wants to merge 19 commits intotrunkfrom
update/newsletter-editor-panel-sent-awareness

Conversation

@Addison-Stavlo
Copy link
Contributor

@Addison-Stavlo Addison-Stavlo commented Feb 24, 2026

Fixes # https://linear.app/a8c/issue/NL-449/newsletter-panel-update-the-states-and-messaging

STILL TODOs - but any testing and review is appreciated at this point.

  • add tests
  • apply/test recent code pilot review suggestions
  • bring email stats query back in and add a fallback copy for an edge case (comment below)
  • test specific paid tiers (and changing them) in messaging. I recently made some updates there that I have not been able to test yet for reasons...

Proposed changes:

Updates the newsletter panel in the editor to have messaging consistent with the state of sent emails.

  • We fetch status of email sending details from wpcom with newsletter-email-sent-status, we have added fetch methods and registered the proxy endpoint on the jetpack side.
  • We use this information to display if an email has been sent, when it has been sent, and what access level and newsletter categories it was sent to.
    • We provide fallback translations as well in the case that the information about access level and categories is not present (more recently added meta, will only be true for posts published >2months-ish ago).
  • We append edge case messages to the already sent copy in certain cases, such as if the user is making updates to the content, republishing, or changing categories or access settings.
    • Since this information lives in 3 areas (main panel, pre publish panel, post publish panel), and resets/remounts on opening/closing these, some of this information is persisted in redux store and a NewsletterRepublishTracker is added to the dom to watch and set values in redux for this purpose.

Panel states updated:

  • The main pre-publish state/copy is unchanged. However, if a previous send was detected we now show that message instead.
  • The main "post only" copy is unchanged. However, if a previous send was detected we now show that message instead.
  • The immediate post-publish state is now updated with a sending/in-progress worded message containing the access settings and categories.
    • If a post is published with the "post and email" setting and we do not detect an email send, we continue to show this message for 15minutes past the publish date.
    • If a post has been published for more than 15minutes and no send is detected, we show a message that the post was published without an email send and that the user can republish to trigger email sends.
  • Later post-email state (visiting the editor after emails have been sent) is now updated as previously described to show when and to which settings it was emailed to.

Some screenshots of various states:
Screenshot 2026-02-26 at 4 09 59 PM
Screenshot 2026-02-26 at 4 10 21 PM
Screenshot 2026-02-26 at 4 11 21 PM

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

Does this pull request change what data or activity we track or use?

Testing instructions:

  • Install this build on your wpcom sandbox (for simple site) or on a jetpack test site with the beta tester plugin. (details in comment below)
    • if using wpcom sandbox, sandbox both the public-api and site-url
    • also if using wpcom sandbox and recently been working on emails/async jobs, ensure you disable sandboxing jobs or run the watcher
  • Ensure newsletters are enabled and you have subscriebers, a couple newsletter categories, etc.
  • Go to the editor and publish a post to send to subscribers.
  • pay attention to the various newsletter panels throughout the process (full panel to open, pre-publish panel, post-publish panel). Verify the copies make sense for the situation.
  • after publishing, verify you have the sending/in-progress worded message.
  • after a minute, reload the editor. check the panel and you should see a message indicating the time, access level, categories (if applicable) that the emails were sent.
  • update the post content or publish date and save, verify you see an extra appended message in the newsletter panel that it will not trigger emails.
  • reload the editor. move the post to a draft and try to publish it again. ensure the newsletter panel notes the same appended message as above.
  • smoke test various states, publishing with "post only" first, or moving to "post only". play around with things and see if the new panel states appear in any situations where they are not expected.

Changelog

  • Generate changelog entries for this PR (using AI).

@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the update/newsletter-editor-panel-sent-awareness branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/newsletter-editor-panel-sent-awareness

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions github-actions bot added [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Status] In Progress [Tests] Includes Tests labels Feb 24, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 24, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: March 3, 2026
    • Code freeze: March 3, 2026

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@github-actions github-actions bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label Feb 24, 2026
@jp-launch-control
Copy link

jp-launch-control bot commented Feb 24, 2026

Code Coverage Summary

Coverage changed in 6 files. Only the first 5 are listed here.

File Coverage Δ% Δ Uncovered
projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js 0/164 (0.00%) 0.00% 97 💔
projects/plugins/jetpack/extensions/blocks/subscriptions/panel.js 0/50 (0.00%) 0.00% 24 💔
projects/plugins/jetpack/extensions/store/membership-products/selectors.js 26/32 (81.25%) -14.75% 5 💔
projects/plugins/jetpack/extensions/store/membership-products/reducer.js 10/14 (71.43%) -20.24% 3 ❤️‍🩹
projects/plugins/jetpack/extensions/store/membership-products/resolvers.js 53/129 (41.09%) -0.32% 1 ❤️‍🩹

1 file is newly checked for coverage.

File Coverage
projects/plugins/jetpack/_inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-newsletter-email-sent-status.php 19/39 (48.72%) ❤️‍🩹

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

Copy link
Contributor Author

@Addison-Stavlo Addison-Stavlo left a comment

Choose a reason for hiding this comment

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

Self-review, because I like the interface here for reading changes.

@Addison-Stavlo Addison-Stavlo self-assigned this Feb 26, 2026
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`,
Copy link
Contributor Author

@Addison-Stavlo Addison-Stavlo Feb 26, 2026

Choose a reason for hiding this comment

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

Note I may end up bringing this request for email stats back into the diff. related to https://linear.app/a8c/issue/NL-477/ensure-fallback-for-sent-information-for-potentially-missing-after / https://linear.app/a8c/issue/NL-476/newsletters-losing-wpcom-specific-post-meta-on-atomic-transfer

Fetching email stats would be a fallback for those other post metas that may have gotten lost for sites in past transfers. We would then create another fallback message that emails were already sent, but have no information about date, access, category, etc. This would be better than falsely stating the email was never sent.

@Addison-Stavlo Addison-Stavlo marked this pull request as ready for review February 26, 2026 21:01
Copilot AI review requested due to automatic review settings February 26, 2026 21:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the newsletter editor panel to display accurate messaging about email send status. It replaces the previous getTotalEmailsSentCount approach with a more comprehensive newsletter-email-sent-status endpoint that provides detailed information about when emails were sent, to which access levels, and to which categories.

Changes:

  • Adds new Redux state management for tracking post email send status and session-based modification flags
  • Introduces a new REST API endpoint /wpcom/v2/newsletter-email-sent-status to fetch email send metadata
  • Refactors SubscribersAffirmation component to display contextual messages based on email send state (pre-publish, sending in progress, already sent, or not sent)

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
test/selectors-test.js Removes tests for deprecated getTotalEmailsSentCount selector
test/reducer-test.js Removes tests for deprecated SET_TOTAL_EMAILS_SENT_COUNT reducer case
test/actions-test.js Removes tests for deprecated setTotalEmailsSentCount action
selectors.js Removes getTotalEmailsSentCount; adds three new selectors for email send state tracking
resolvers.js Removes fetchTotalEmailsSentCount and resolver; adds fetchPostEmailSentState and resolver
reducer.js Removes totalEmailsSentCount state; adds postEmailSentState, alreadySentPostModifiedInSession, and publishedWithEmailEnabledInSession state with corresponding reducer cases
actions.js Removes setTotalEmailsSentCount; adds three new action creators for email send state management
subscribers-affirmation.js Major refactor adding utility functions for date formatting, category name formatting, access level parsing, and conditional message display based on email send state
panel.js Adds NewsletterRepublishTracker component to track status transitions and persist modification flags across panel remounts
class-wpcom-rest-api-v2-endpoint-newsletter-email-sent-status.php New PHP endpoint class that proxies requests to WordPress.com for email send status data
update-newsletter-editor-panel-sent-awareness Changelog entry documenting the changes

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +77 to +86
public function permission_check() {
if ( current_user_can( 'edit_posts' ) || current_user_can( 'manage_options' ) ) {
return true;
}
return new WP_Error(
'rest_forbidden',
__( 'Sorry, you are not allowed to access this endpoint.', 'jetpack' ),
array( 'status' => rest_authorization_required_code() )
);
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The permission check allows access if the user has either 'edit_posts' or 'manage_options' capability, but it doesn't verify if the user has permission to access the specific post being queried. Consider checking if the user can edit the specific post using something like current_user_can('edit_post', $post_id) where $post_id is extracted from the request parameters. This would ensure users can only view email sent status for posts they have permission to edit.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +69
const { postId, postMeta, postEmailSentState, status, isSavingPost } = useSelect( select => {
const { getCurrentPost, getEditedPostAttribute } = select( editorStore );
const { getPostEmailSentState } = select( membershipProductsStore );
const post = getCurrentPost();
const id = post?.id;
if ( id ) {
getPostEmailSentState( id );
}
return {
postId: id,
postMeta: getEditedPostAttribute( 'meta' ),
postEmailSentState: id ? getPostEmailSentState( id ) : null,
status: post?.status,
isSavingPost: select( editorStore ).isSavingPost(),
};
}, [] );
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The useSelect hook has an empty dependency array, which means it will only run once when the component mounts. However, the selector accesses dynamic values like postId, status, and postEmailSentState that can change over time. This could cause the component to miss updates when the post changes or when the email sent state is fetched. Consider removing the empty dependency array to allow useSelect to track dependencies automatically, or explicitly include the necessary dependencies if there's a specific reason to limit re-renders.

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +44
if ( ! postId ) {
return { email_sent_at: null, stats_on_send: null };
}
return state.postEmailSentState || { email_sent_at: null, stats_on_send: null };
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The selector accepts a postId parameter but doesn't use it to retrieve post-specific state. The state structure stores postEmailSentState as a single object rather than keyed by postId, so this selector will return the same email sent state regardless of which postId is requested. If the editor can have multiple posts loaded (or switch between posts), this will cause the wrong email sent state to be displayed. Consider restructuring the state to be keyed by postId (similar to alreadySentPostModifiedInSession and publishedWithEmailEnabledInSession), or if only one post is ever active at a time, remove the postId parameter from the selector signature.

Suggested change
if ( ! postId ) {
return { email_sent_at: null, stats_on_send: null };
}
return state.postEmailSentState || { email_sent_at: null, stats_on_send: null };
const defaultState = { email_sent_at: null, stats_on_send: null };
// If no postId is provided, we cannot look up a per-post entry.
if ( ! postId ) {
return defaultState;
}
const { postEmailSentState } = state;
// If postEmailSentState is an object keyed by postId, prefer that.
if ( postEmailSentState && typeof postEmailSentState === 'object' && postId in postEmailSentState ) {
return postEmailSentState[ postId ] || defaultState;
}
// Fallback: support legacy shape where postEmailSentState is a single object.
return postEmailSentState || defaultState;

Copilot uses AI. Check for mistakes.
getNewsletterCategoriesSubscriptionsCount,
getProducts,
getTotalEmailsSentCount,
} from '../selectors';
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The new selectors getPostEmailSentState, getAlreadySentPostModifiedInSession, and getPublishedWithEmailEnabledInSession lack test coverage. While tests for the deprecated getTotalEmailsSentCount selector were removed, no tests were added for the replacement selectors. This creates a gap in test coverage for critical functionality that tracks email send state.

Copilot uses AI. Check for mistakes.
setNewsletterCategories,
setNewsletterCategoriesSubscriptionsCount,
setTotalEmailsSentCount,
} from '../actions';
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The new actions setPostEmailSentState, setAlreadySentPostModifiedInSession, and setPublishedWithEmailEnabledInSession lack test coverage. While tests for the deprecated setTotalEmailsSentCount action were removed, no tests were added for the replacement actions. This creates a gap in test coverage for critical functionality that manages email send state in the Redux store.

Copilot uses AI. Check for mistakes.
@@ -148,19 +148,4 @@ describe( 'Membership products reducer testing', () => {
newsletterCategoriesSubscriptionsCount: anyNewsletterCategoriesSubscriptionsCount,
} );
} );
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The new reducer cases SET_POST_EMAIL_SENT_STATE, SET_ALREADY_SENT_POST_MODIFIED_IN_SESSION, and SET_PUBLISHED_WITH_EMAIL_ENABLED_IN_SESSION lack test coverage. While the test for the deprecated SET_TOTAL_EMAILS_SENT_COUNT case was removed, no tests were added for the replacement reducer cases. This creates a gap in test coverage for critical state management logic.

Suggested change
} );
} );
test( 'set post email sent state action type adds the post email sent state to the returned state.', () => {
// Given
const anyPostEmailSentState = true;
const anySetPostEmailSentStateAction = {
type: 'SET_POST_EMAIL_SENT_STATE',
postEmailSentState: anyPostEmailSentState,
};
// When
const returnedState = reducer( DEFAULT_STATE, anySetPostEmailSentStateAction );
// Then
expect( returnedState ).toStrictEqual( {
...DEFAULT_STATE,
postEmailSentState: anyPostEmailSentState,
} );
} );
test( 'set already sent post modified in session action type adds the flag to the returned state.', () => {
// Given
const anyAlreadySentPostModifiedInSession = true;
const anySetAlreadySentPostModifiedInSessionAction = {
type: 'SET_ALREADY_SENT_POST_MODIFIED_IN_SESSION',
alreadySentPostModifiedInSession: anyAlreadySentPostModifiedInSession,
};
// When
const returnedState = reducer(
DEFAULT_STATE,
anySetAlreadySentPostModifiedInSessionAction
);
// Then
expect( returnedState ).toStrictEqual( {
...DEFAULT_STATE,
alreadySentPostModifiedInSession: anyAlreadySentPostModifiedInSession,
} );
} );
test( 'set published with email enabled in session action type adds the flag to the returned state.', () => {
// Given
const anyPublishedWithEmailEnabledInSession = true;
const anySetPublishedWithEmailEnabledInSessionAction = {
type: 'SET_PUBLISHED_WITH_EMAIL_ENABLED_IN_SESSION',
publishedWithEmailEnabledInSession: anyPublishedWithEmailEnabledInSession,
};
// When
const returnedState = reducer(
DEFAULT_STATE,
anySetPublishedWithEmailEnabledInSessionAction
);
// Then
expect( returnedState ).toStrictEqual( {
...DEFAULT_STATE,
publishedWithEmailEnabledInSession: anyPublishedWithEmailEnabledInSession,
} );
} );

Copilot uses AI. Check for mistakes.
Comment on lines +545 to +549
const categoriesMatch =
! statsOnSend?.has_newsletter_categories ||
( Array.isArray( postCategories ) &&
statsCats.length === postCategories.length &&
statsCats.every( ( id, i ) => postCategories[ i ] === id ) );
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The category comparison logic may incorrectly detect a mismatch when categories are in different orders. The current implementation uses statsCats.every((id, i) => postCategories[i] === id), which compares elements at the same index. If the arrays contain the same category IDs but in different orders, this will return false even though the categories haven't actually changed. Consider sorting both arrays before comparison or using a set-based comparison approach.

Suggested change
const categoriesMatch =
! statsOnSend?.has_newsletter_categories ||
( Array.isArray( postCategories ) &&
statsCats.length === postCategories.length &&
statsCats.every( ( id, i ) => postCategories[ i ] === id ) );
const postCategorySet = Array.isArray( postCategories ) ? new Set( postCategories ) : null;
const categoriesMatch =
! statsOnSend?.has_newsletter_categories ||
( postCategorySet &&
statsCats.length === postCategories.length &&
statsCats.every( id => postCategorySet.has( id ) ) );

Copilot uses AI. Check for mistakes.
@Addison-Stavlo Addison-Stavlo requested a review from a team February 26, 2026 21:13
Copy link
Member

@allilevine allilevine left a comment

Choose a reason for hiding this comment

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

I tried it out and the panels are much more accurate with this change. 🙌 One thing that came to mind is, should we still have the email preview and test email options if the email has already been sent? Or should those be hidden because you can't re-send the email at that point?

Image Image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Block] Subscriptions [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Status] In Progress [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants