Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
99d6de7
feat(newsletters): wire wizard SubscriptionLists to wizard-bridge events
thomasguillot May 7, 2026
a6bff06
test(newsletters): cover wizard SubscriptionLists bridge wiring
thomasguillot May 7, 2026
a62ce92
fix(newsletters-wizard): read bridge readiness synchronously
thomasguillot May 7, 2026
2ea09de
feat(newsletters): bundled-mode parity for ESP-list edit modal (NEWS-…
thomasguillot May 7, 2026
5f613d6
chore: ignore /build output directory
thomasguillot May 7, 2026
5550417
feat(components): shared scaffolding for newsletters settings (NEWS-2…
thomasguillot May 18, 2026
d9677b9
feat(newsletters): rebuild Settings page (NEWS-2263)
thomasguillot May 18, 2026
4b1d784
fix(newsletters): address PR review feedback (NEWS-2263)
thomasguillot May 18, 2026
a7e1950
fix(newsletters): expose labels response with explicit key (NEWS-2263)
thomasguillot May 18, 2026
c84e066
fix(newsletters): a11y labels on per-row Edit/Delete (NEWS-2263)
thomasguillot May 18, 2026
952e3c4
fix(newsletters): address second-pass review feedback (NEWS-2263)
thomasguillot May 19, 2026
9ccdf4f
fix(newsletters): disable settings controls during save (NEWS-2263)
thomasguillot May 19, 2026
ffeb834
fix(newsletters): hide Subscription Lists for manual provider (NEWS-2…
thomasguillot May 19, 2026
f0bdaf1
fix(newsletters): a11y + bridge-event robustness (NEWS-2263)
thomasguillot May 19, 2026
5201805
fix(newsletters): queue bridge dispatch when not yet ready (NEWS-2263)
thomasguillot May 19, 2026
573337e
fix(newsletters): resolve bridge event name at replay time (NEWS-2263)
thomasguillot May 19, 2026
baa3ddd
fix(newsletters): flush queue if bridge is ready at fallback (NEWS-2263)
thomasguillot May 19, 2026
2a3fcbb
fix(newsletters): clear stale queue on immediate dispatch (NEWS-2263)
thomasguillot May 19, 2026
5e1f3cc
fix(newsletters): friendlier error for missing PATCH endpoint (NEWS-2…
thomasguillot May 19, 2026
30fc56e
fix(components,newsletters): hash-router guard + reload listener reat…
thomasguillot May 19, 2026
14625e8
fix(newsletters): defer hash-router guard to ConfirmDialog; wrap test…
thomasguillot May 19, 2026
6bd7cbc
fix(newsletters): re-render lists on ESP switch-back and notice on de…
thomasguillot Jun 1, 2026
a366e00
fix(components): structural single-consumer guard for unsaved-changes…
thomasguillot Jun 1, 2026
e946a4c
fix(newsletters): drop stale tracking-admin remove_action
thomasguillot Jun 1, 2026
a076b3c
fix(newsletters): sync subscription lists with ESP connection status
thomasguillot Jun 3, 2026
137215f
refactor(newsletters): rename signature helper to match its scope
thomasguillot Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Thumbs.db
/vendor/

# Built files
/build
/dist
/packages/components/colors
/packages/components/dist
Expand Down
32 changes: 24 additions & 8 deletions includes/wizards/newsletters/class-newsletters-wizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ public function __construct() {

// Menu removals.
remove_action( 'admin_menu', [ Newspack_Newsletters_Settings::class, 'add_plugin_page' ] );
remove_action( 'admin_menu', [ Newspack_Newsletters_Tracking_Admin::class, 'add_settings_page' ] );

// Customize the Newsletter ads menu titles.
add_filter(
Expand Down Expand Up @@ -227,9 +226,25 @@ function ( $acc, $value ) {
},
[]
);

$labels = [];
if ( class_exists( 'Newspack_Newsletters' ) ) {
$provider = Newspack_Newsletters::get_service_provider();
if ( $provider && method_exists( $provider, 'label' ) ) {
// `label()` accepts a second `$context` arg (usually a list public id).
// Intentionally omitted here: this is the generic explanation copy
// shown above the "Add new local list" button, not a per-list label.
$labels = [
'local_list_explanation' => $provider::label( 'local_list_explanation' ),
];
}
}

return [
'configured' => $newsletters_configuration_manager->is_configured(),
'settings' => $settings,
'configured' => $newsletters_configuration_manager->is_configured(),
'esp_connected' => (bool) $newsletters_configuration_manager->is_esp_set_up(),
'settings' => $settings,
'labels' => $labels,
];
}

Expand All @@ -253,6 +268,12 @@ public function api_update_newsletters_settings( $request ) {
$args = $request->get_params();
$newsletters_configuration_manager = Configuration_Managers::configuration_manager_class_for_plugin_slug( 'newspack-newsletters' );
$newsletters_configuration_manager->update_settings( $args );
// The provider instance is memoized on `init`, before this save runs, so
// refresh it before reading credential status — otherwise a provider switch
// reports the previously-active provider's connection state.
if ( method_exists( 'Newspack_Newsletters', 'memoize_service_provider' ) ) {
\Newspack_Newsletters::memoize_service_provider();
}
return $this->api_get_newsletters_settings();
}

Expand Down Expand Up @@ -424,11 +445,6 @@ private function get_tabs() {
];
}

$tabs[] = [
'textContent' => esc_html__( 'Tracking', 'newspack-plugin' ),
'href' => admin_url( 'edit.php?post_type=' . Newspack_Newsletters::NEWSPACK_NEWSLETTERS_CPT . '&page=' . $this->slug . '#/tracking' ),
];

return $tabs;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/components/src/card-settings-group/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import './style.scss';
const CardSettingsGroup = ( {
actionType = 'none',
children,
className,
disabled = false,
icon = null,
headerAction,
title = '',
Expand All @@ -21,6 +23,8 @@ const CardSettingsGroup = ( {
}: {
actionType?: 'chevron' | 'toggle' | 'button' | 'link' | 'none';
children?: React.ReactNode;
className?: string;
disabled?: boolean;
icon?: React.ReactNode;
title: string;
headerAction?: {
Expand All @@ -40,7 +44,7 @@ const CardSettingsGroup = ( {
} ) => {
return (
<Card
className="newspack-card--core--settings-group"
className={ [ 'newspack-card--core--settings-group', className ].filter( Boolean ).join( ' ' ) }
actionType={ actionType }
isSmall
__experimentalCoreCard
Expand All @@ -54,6 +58,7 @@ const CardSettingsGroup = ( {
headerAction,
onHeaderClick,
onToggle: onEnable,
disabled,
icon,
iconBackgroundColor: true,
isActive,
Expand Down
6 changes: 5 additions & 1 deletion packages/components/src/card/core-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const CoreCard = ( {
headerStyle,
childrenStyle,
footerStyle,
disabled,
icon,
iconBackgroundColor,
isActive,
Expand Down Expand Up @@ -62,6 +63,7 @@ const CoreCard = ( {
icon && 'newspack-card--core__has-icon',
iconBackgroundColor && 'newspack-card--core__has-icon-background-color',
isActive && 'newspack-card--core__is-active',
disabled && 'newspack-card--core__is-disabled',
children && 'newspack-card--core__has-children',
noMargin && 'newspack-card--core__no-margin',
hasGreyHeader && 'newspack-card--core__has-grey-header'
Expand Down Expand Up @@ -91,7 +93,9 @@ const CoreCard = ( {
style={ headerStyle }
size={ sizeProps }
gap={ 4 }
onClick={ onHeaderClick }
onClick={ disabled ? undefined : onHeaderClick }
disabled={ onHeaderClick && disabled ? true : undefined }
aria-disabled={ onHeaderClick && disabled ? true : undefined }
>
{ isDraggable && (
<div className="newspack-card--core__header__draggable-controls">
Expand Down
7 changes: 7 additions & 0 deletions packages/components/src/card/style-core.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@
&__no-margin .components-card__body {
padding: 0 !important;
}
&__is-disabled {
cursor: not-allowed;
opacity: 0.5;
.newspack-card--core__header {
pointer-events: none;
}
}
&__header {
color: wp-colors.$gray-700;
flex-wrap: wrap;
Expand Down
140 changes: 140 additions & 0 deletions packages/components/src/hooks/use-unsaved-changes-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* WordPress dependencies.
*/
import { useEffect, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
* Internal dependencies.
*/
import useConfirmDialog from './use-confirm-dialog';

type UseUnsavedChangesDialogOptions = {
when: boolean;
};

// Stack of mounted guards; the last entry owns the document-level handler so
// only the most-recently-mounted guard prompts when several are active. Using a
// stack keeps ownership correct under out-of-order unmounts.
const activeGuards: symbol[] = [];

/**
* Returns true when `href` resolves to the same origin as the current page —
* i.e. it is an internal navigation that would unload the wizard. External
* `https://...` links, `mailto:`, `tel:`, and other schemes navigate to a new
* context (new tab, mail client, dialer) without unloading the wizard, and
* must not trigger the discard-changes prompt.
*/
function isSameOriginNavigation( link: HTMLAnchorElement ): boolean {
try {
const url = new URL( link.href, window.location.href );
return url.origin === window.location.origin && ( url.protocol === 'http:' || url.protocol === 'https:' );
} catch ( e ) {
return false;
}
}

/**
* Shared unsaved-changes guard. Wraps `useConfirmDialog` with standardized
* messaging, intercepts same-origin link clicks so the dialog fires instead
* of a silent navigation, and adds a `beforeunload` listener as the last-resort
* guard for refresh / tab-close (browser-native, cannot be styled). The
* returned `confirmDialog` element must be rendered in JSX.
*
* Single-consumer constraint: the click handler is attached at the document
* level in capture phase. Two simultaneously-active instances will both fire
* a dialog. A development-only warning surfaces this.
*/
function useUnsavedChangesDialog( { when }: UseUnsavedChangesDialogOptions ) {
const { confirmDialog, requestConfirm } = useConfirmDialog( {
when,
message: __( 'You have unsaved changes that will be lost. Discard changes?', 'newspack-plugin' ),
confirmButtonText: __( 'Discard changes', 'newspack-plugin' ),
hideTitle: true,
} );

// Tracks navigation the user has already approved via our custom dialog so
// the beforeunload guard doesn't fire a second native prompt on top of it.
const isNavigatingRef = useRef( false );

useEffect( () => {
if ( ! when ) {
return;
}
const ownerId = Symbol( 'unsaved-changes-guard' );
activeGuards.push( ownerId );
if ( process.env.NODE_ENV !== 'production' && activeGuards.length > 1 ) {
// eslint-disable-next-line no-console
console.warn(
'useUnsavedChangesDialog: more than one active instance detected. ' +
Comment thread
thomasguillot marked this conversation as resolved.
'Document-level click capture will fire a dialog per instance — ensure only one guard is active at a time.'
);
}
const handler = ( e: MouseEvent ) => {
// Only the top-of-stack guard prompts, so concurrent guards can't stack dialogs.
if ( activeGuards[ activeGuards.length - 1 ] !== ownerId ) {
return;
}
if ( e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0 ) {
return;
}
const target = e.target as HTMLElement | null;
const link = target?.closest( 'a[href]' ) as HTMLAnchorElement | null;
if ( ! link ) {
return;
}
const href = link.getAttribute( 'href' );
if ( ! href || href.startsWith( '#' ) || href.startsWith( 'javascript:' ) ) {
// All `#`-prefixed links are skipped here: plain anchors
// (`#section`) are scroll targets, and HashRouter paths
// (`#/route`) are intercepted by `ConfirmDialog`'s built-in
// `history.block` listener instead. Routing them through
// `window.location.href` here would trigger a hashchange
// that the still-active block re-catches, double-prompting.
return;
Comment thread
thomasguillot marked this conversation as resolved.
}
if ( link.target && link.target !== '_self' ) {
return;
}
// Skip mailto:, tel:, external origins, and any non-http(s) scheme —
// they don't unload the wizard, so a discard prompt would be wrong.
if ( ! isSameOriginNavigation( link ) ) {
return;
}
e.preventDefault();
e.stopPropagation();
const destination = link.href;
requestConfirm( () => {
isNavigatingRef.current = true;
window.location.href = destination;
Comment thread
thomasguillot marked this conversation as resolved.
} );
};
document.addEventListener( 'click', handler, true );
return () => {
document.removeEventListener( 'click', handler, true );
const idx = activeGuards.indexOf( ownerId );
if ( idx !== -1 ) {
activeGuards.splice( idx, 1 );
}
};
}, [ when, requestConfirm ] );

useEffect( () => {
if ( ! when ) {
return;
}
const handler = ( e: BeforeUnloadEvent ) => {
if ( isNavigatingRef.current ) {
return;
}
e.preventDefault();
e.returnValue = '';
};
window.addEventListener( 'beforeunload', handler );
return () => window.removeEventListener( 'beforeunload', handler );
}, [ when ] );

return { confirmDialog, requestConfirm };
}

export default useUnsavedChangesDialog;
3 changes: 3 additions & 0 deletions packages/components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ export { default as withWizardScreen } from './with-wizard-screen';
export { default as Router } from './proxied-imports/router';
export { default as hooks } from './hooks';
export { default as useConfirmDialog } from './hooks/use-confirm-dialog';
export { default as useUnsavedChangesDialog } from './hooks/use-unsaved-changes-dialog';
import * as integrationIcons from './integration-icons';
export { integrationIcons };
export { default as utils } from './utils';

import './style.scss';
14 changes: 14 additions & 0 deletions packages/components/src/integration-icons/active-campaign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const activeCampaign = (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M16.6934 10.5811C17.1772 10.9036 17.4355 11.4194 17.4355 11.9678C17.4355 12.516 17.1449 13.0319 16.6934 13.3545L6.56445 20V18.6133C6.56445 18.1939 6.75844 17.7746 7.14551 17.5166L15.5 11.9678L7.14551 6.38672C6.79063 6.16086 6.56445 5.74175 6.56445 5.29004V4L16.6934 10.5811Z"
fill="white"
/>
<path
d="M6.56445 8.77441C6.56445 8.35519 7.04853 8.12873 7.37109 8.35449L12.6611 11.9355L11.9512 12.4189C11.4996 12.7091 10.9193 12.7092 10.4678 12.4189L9.30664 11.6777L6.56445 9.83887V8.77441Z"
fill="white"
/>
</svg>
);

export default activeCampaign;
22 changes: 22 additions & 0 deletions packages/components/src/integration-icons/constant-contact.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const constantContact = (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M11.3261 20.9997C6.36799 20.9997 3 16.9581 3 12.6626C3 8.30073 6.43425 4.5352 11.0059 4.35852C11.1826 4.34748 11.3372 4.50207 11.3372 4.67875V6.01491C11.3372 6.18055 11.2047 6.3241 11.028 6.33514C7.67102 6.48974 4.96558 9.23935 4.96558 12.6736C4.96558 16.0305 7.61581 19.0341 11.3261 19.0341C14.9371 19.0341 17.4989 16.152 17.6535 12.9717C17.6646 12.8061 17.7971 12.6626 17.9738 12.6626H19.3099C19.4866 12.6626 19.6302 12.8061 19.6302 12.9938C19.4756 17.2342 16.0634 20.9997 11.3261 20.9997Z"
fill="#1856ED"
/>
<path
d="M11.3372 16.5827C9.12864 16.5827 7.41704 14.8269 7.41704 12.6625C7.41704 10.6417 8.97405 8.95223 10.9948 8.78659C11.1826 8.77555 11.3372 8.9191 11.3372 9.10683V10.443C11.3372 10.5976 11.2267 10.7411 11.0611 10.7632C10.0894 10.8957 9.38262 11.7239 9.38262 12.6736C9.38262 13.7337 10.2108 14.6281 11.3372 14.6281C12.2758 14.6281 13.115 13.9214 13.2475 12.9497C13.2696 12.7951 13.4132 12.6736 13.5678 12.6736H14.9039C15.0916 12.6736 15.2352 12.8282 15.2242 13.0159C15.0475 14.9925 13.3911 16.5827 11.3372 16.5827Z"
fill="#1856ED"
/>
<path
d="M19.0339 11.0283C18.8793 7.79277 16.2953 5.12046 12.9715 4.96586C12.8058 4.95482 12.6623 4.82231 12.6623 4.64563V3.32051C12.6623 3.14383 12.8058 3.00028 12.9936 3.00028C17.3112 3.16592 20.8338 6.62225 20.9994 11.0062C21.0105 11.1828 20.8559 11.3374 20.6792 11.3374H19.3431C19.1885 11.3374 19.0449 11.2049 19.0339 11.0283Z"
fill="#FF9E1A"
/>
<path
d="M13.0267 9.41602C12.8279 9.38289 12.6844 9.21725 12.6844 9.05161V7.74859C12.6844 7.56086 12.839 7.41731 13.0267 7.42835C14.9481 7.59399 16.4278 9.12891 16.5934 10.9841C16.6045 11.1718 16.4609 11.3264 16.2732 11.3264H14.8708C14.7383 11.3264 14.6389 11.238 14.6168 11.1055C14.5174 10.3215 13.9211 9.57062 13.0267 9.41602Z"
fill="#FF9E1A"
/>
</svg>
);

export default constantContact;
18 changes: 18 additions & 0 deletions packages/components/src/integration-icons/fundraise-up.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const fundraiseUp = (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M4.90143 6.11914C4.90143 4.94655 5.85727 4 7.03802 4H19.0985V5.75182C19.0985 6.92441 18.1427 7.87098 16.9619 7.87098H4.90143V6.11914Z"
fill="white"
/>
<path
d="M4.90143 11.4721C4.90143 10.6979 5.53463 10.0645 6.33667 10.0645H14.8778V11.824C14.8778 12.9924 13.921 13.9355 12.739 13.9355H4.90143V11.4721Z"
fill="white"
/>
<path
d="M4.90143 16.8329C4.90143 16.4387 5.22775 16.1291 5.62502 16.1291L10.657 16.129V17.8886C10.657 19.0569 9.69221 20 8.50045 20H4.90143V16.8329Z"
fill="white"
/>
</svg>
);

export default fundraiseUp;
6 changes: 6 additions & 0 deletions packages/components/src/integration-icons/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as activeCampaign } from './active-campaign';
export { default as constantContact } from './constant-contact';
export { default as fundraiseUp } from './fundraise-up';
export { default as mailchimp } from './mailchimp';
export { default as salesforce } from './salesforce';
export { default as wisepops } from './wisepops';
10 changes: 10 additions & 0 deletions packages/components/src/integration-icons/mailchimp.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions packages/components/src/integration-icons/salesforce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const salesforce = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<path
d="M13.3173 7.24314C14.3498 6.16866 15.7848 5.50017 17.3737 5.50017C19.4877 5.50017 21.3287 6.67965 22.3122 8.42612C23.1661 8.04463 24.1111 7.83113 25.1016 7.83113C28.913 7.83113 32 10.9461 32 14.7925C32 18.639 28.9095 21.7539 25.1016 21.7539C24.6361 21.7539 24.1811 21.7084 23.7436 21.6174C22.8791 23.1574 21.2307 24.2004 19.3407 24.2004C18.5497 24.2004 17.8007 24.0184 17.1357 23.6929C16.2607 25.7543 14.2168 27.1998 11.8403 27.1998C9.46385 27.1998 7.25189 25.6319 6.4399 23.4339C6.0864 23.5074 5.71891 23.5494 5.34092 23.5494C2.39046 23.5494 0 21.1344 0 18.1525C0 16.154 1.07448 14.411 2.67046 13.4765C2.34146 12.7206 2.15947 11.8841 2.15947 11.0091C2.15947 7.57913 4.94192 4.80018 8.37537 4.80018C10.3913 4.80018 12.1833 5.75916 13.3173 7.24314Z"
fill="var(--integration-icon-color, #0D9DDA)"
/>
</svg>
);

export default salesforce;
14 changes: 14 additions & 0 deletions packages/components/src/integration-icons/wisepops.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const wisepops = (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<path
d="M19.0543 10.8641C19.5935 10.8642 19.9806 11.384 19.8262 11.9008L16.1245 24.2947C16.0225 24.6356 15.7087 24.8698 15.3526 24.8698H10.5242C10.5199 24.8699 10.5155 24.8702 10.5112 24.8702H3.28999C2.96651 24.8702 2.67422 24.6766 2.548 24.3789L0.064734 18.5208C-0.160398 17.9896 0.229352 17.4005 0.80633 17.4004H8.02794C8.35135 17.4004 8.64327 17.5941 8.76954 17.8918L8.99143 18.4154L11.0754 11.4393C11.1773 11.0982 11.4911 10.8641 11.8473 10.8641H19.0543Z"
fill="var(--integration-icon-color, #010101)"
/>
<path
d="M31.1937 7.12976C31.7311 7.12979 32.1179 7.646 31.9671 8.1617L27.2512 24.2907C27.1508 24.6344 26.8355 24.8702 26.4778 24.8702H19.3384C18.8011 24.8702 18.4142 24.3544 18.5649 23.8387L23.2812 7.70967C23.3816 7.36609 23.6965 7.12981 24.0543 7.12976H31.1937Z"
fill="var(--integration-icon-color, #010101)"
/>
</svg>
);

export default wisepops;
Loading
Loading