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. diff --git a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js index 9ced31e5f14d..7f6a9e07d319 100644 --- a/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js +++ b/projects/plugins/jetpack/extensions/shared/memberships/subscribers-affirmation.js @@ -1,9 +1,9 @@ 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 } from '@wordpress/element'; +import { createInterpolateElement, useRef, useLayoutEffect } from '@wordpress/element'; import { sprintf, __, _n } from '@wordpress/i18n'; import paywallBlockMetadata from '../../blocks/paywall/block.json'; import { @@ -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. @@ -164,40 +168,190 @@ 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 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 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 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. 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 prevStatusRef = useRef( null ); + 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, }; } ); 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 ]; }; const blogId = window.Jetpack_Editor_Initial_State?.wpcomBlogId; + const dispatch = useDispatch( membershipProductsStore ); const { emailSubscribersCount, hasFinishedLoading, @@ -205,35 +359,87 @@ 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, + republishedAlreadySentInSession, + publishedWithEmailEnabledInSession, + } = useSelect( + select => { + const { + getNewsletterCategories, + getNewsletterCategoriesEnabled, + getNewsletterCategoriesSubscriptionsCount, + getPostEmailSentState, + getPublishedWithEmailEnabledInSession, + getRepublishedAlreadySentPostInSession, + getSubscriberCounts, + hasFinishedResolution, + } = select( membershipProductsStore ); + + const { emailSubscribers, paidSubscribers } = getSubscriberCounts(); + + // Trigger fetch when we have a postId so we have email_sent_at / stats_on_send (including for draft) + if ( 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: postId ? getPostEmailSentState( postId ) : null, + republishedAlreadySentInSession: postId + ? getRepublishedAlreadySentPostInSession( postId ) + : false, + publishedWithEmailEnabledInSession: postId + ? getPublishedWithEmailEnabledInSession( postId ) + : false, + }; + }, + [ status, postId ] + ); + + useLayoutEffect( () => { + 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 ] ); + + 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 ( @@ -248,38 +454,162 @@ 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 || + publishedWithEmailEnabledInSession || + ( wasPublishedOnLoad.current && + publishDate && + 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; + 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 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', + 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' ); + } } 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 || republishedAlreadySentInSession ) { + 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 +622,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..b163d4b41991 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,18 @@ 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 }, +} ); + +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 e85fbf9fc528..8cb56fbd1b4c 100644 --- a/projects/plugins/jetpack/extensions/store/membership-products/reducer.js +++ b/projects/plugins/jetpack/extensions/store/membership-products/reducer.js @@ -16,6 +16,12 @@ export const DEFAULT_STATE = { enabled: false, categories: [], }, + postEmailSentState: { + email_sent_at: null, + stats_on_send: null, + }, + republishedAlreadySentPostInSession: {}, + publishedWithEmailEnabledInSession: {}, }; export default function reducer( state = DEFAULT_STATE, action ) { @@ -38,11 +44,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 +54,27 @@ export default function reducer( state = DEFAULT_STATE, action ) { ...state, newsletterCategoriesSubscriptionsCount: action.newsletterCategoriesSubscriptionsCount, }; + case 'SET_POST_EMAIL_SENT_STATE': + return { + ...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/resolvers.js b/projects/plugins/jetpack/extensions/store/membership-products/resolvers.js index d943c52a1d8f..6a111d1ed26a 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,29 @@ export const getNewsletterCategoriesSubscriptionsCount = executionLock.release( lock ); } }; + +export const getPostEmailSentState = + postId => + async ( { dispatch, registry } ) => { + if ( ! postId ) { + 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..6f401b32590f 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,23 @@ 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 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 && 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 ); - } ); } );