From 1ee5f528f4a76fd252c9a1f61f49f27367bec6a0 Mon Sep 17 00:00:00 2001 From: Rasmy Nguyen Date: Wed, 13 May 2026 13:02:52 -0400 Subject: [PATCH 1/4] feat(integrations): add inactive plugin state Co-Authored-By: Claude Opus 4.7 (1M context) --- .../reader-activation/class-integrations.php | 15 +++++----- .../integrations/class-esp.php | 17 +++++++++++ .../integrations/class-integration.php | 13 ++++++++ .../audience/views/integrations/index.js | 10 +++++++ .../views/integrations/settings-section.js | 30 +++++++++++++++---- 5 files changed, 73 insertions(+), 12 deletions(-) diff --git a/includes/reader-activation/class-integrations.php b/includes/reader-activation/class-integrations.php index 17ffe4a9d4..bbcb006f77 100644 --- a/includes/reader-activation/class-integrations.php +++ b/includes/reader-activation/class-integrations.php @@ -424,13 +424,14 @@ public static function get_all_integration_settings() { continue; } $result[ $id ] = [ - 'id' => $id, - 'name' => $integration->get_name(), - 'description' => $integration->get_description(), - 'enabled' => self::is_enabled( $id ), - 'is_set_up' => $integration->is_set_up(), - 'setup_url' => $integration->get_setup_url(), - 'settings' => $integration->get_settings_config(), + 'id' => $id, + 'name' => $integration->get_name(), + 'description' => $integration->get_description(), + 'enabled' => self::is_enabled( $id ), + 'is_set_up' => $integration->is_set_up(), + 'setup_url' => $integration->get_setup_url(), + 'settings' => $integration->get_settings_config(), + 'required_plugins' => $integration->get_required_plugins(), ]; } return $result; diff --git a/includes/reader-activation/integrations/class-esp.php b/includes/reader-activation/integrations/class-esp.php index 13bcbd6685..a8fb417c1b 100644 --- a/includes/reader-activation/integrations/class-esp.php +++ b/includes/reader-activation/integrations/class-esp.php @@ -64,6 +64,23 @@ public function get_setup_url() { return $newsletters_configuration_manager->get_settings_url(); } + /** + * Get the plugins this integration depends on, with their install/active status. + * + * @return array List of associative arrays with keys `slug`, `name`, `is_active`, `is_installed`. + */ + public function get_required_plugins() { + $status = \Newspack\Plugin_Manager::get_managed_plugin_status( 'newspack-newsletters' ); + return [ + [ + 'slug' => 'newspack-newsletters', + 'name' => __( 'Newspack Newsletters', 'newspack-plugin' ), + 'is_active' => 'active' === $status, + 'is_installed' => 'uninstalled' !== $status, + ], + ]; + } + /** * Register the settings fields declared by this integration. * diff --git a/includes/reader-activation/integrations/class-integration.php b/includes/reader-activation/integrations/class-integration.php index c667727afa..c8cc10a880 100644 --- a/includes/reader-activation/integrations/class-integration.php +++ b/includes/reader-activation/integrations/class-integration.php @@ -151,6 +151,19 @@ public function get_setup_url() { return ''; } + /** + * Get the plugins this integration depends on, with their active status. + * + * Child classes should override this to declare any plugins that must be + * active for the integration to function. The integrations UI uses this + * to surface a "requirements" affordance on the integration card. + * + * @return array List of associative arrays with keys `slug`, `name`, `is_active`. + */ + public function get_required_plugins() { + return []; + } + /** * Whether this integration supports frontend reader registration. * diff --git a/src/wizards/audience/views/integrations/index.js b/src/wizards/audience/views/integrations/index.js index 2ca1bd540d..707d690251 100644 --- a/src/wizards/audience/views/integrations/index.js +++ b/src/wizards/audience/views/integrations/index.js @@ -88,6 +88,15 @@ const AudienceIntegrations = ( props, ref ) => { } ); }, [] ); + const handleActivatePlugin = useCallback( + pluginSlug => + apiFetch( { + path: `/newspack/v1/plugins/${ pluginSlug }/activate`, + method: 'POST', + } ).then( () => fetchSettings() ), + [ fetchSettings ] + ); + const sharedProps = { integrations, pendingChanges, @@ -97,6 +106,7 @@ const AudienceIntegrations = ( props, ref ) => { onFieldChange: handleFieldChange, onSave: handleSave, onToggleEnabled: handleToggleEnabled, + onActivatePlugin: handleActivatePlugin, }; return ( diff --git a/src/wizards/audience/views/integrations/settings-section.js b/src/wizards/audience/views/integrations/settings-section.js index c062afe389..cee3b4f0ec 100644 --- a/src/wizards/audience/views/integrations/settings-section.js +++ b/src/wizards/audience/views/integrations/settings-section.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Icon, envelope } from '@wordpress/icons'; /** @@ -32,7 +32,9 @@ const DEFAULT_ICON = { backgroundColor: colors[ 'neutral-100' ], }; -export const SettingsSection = ( { integrations, loading, onToggleEnabled, history } ) => { +const getMissingPlugins = integration => ( integration.required_plugins || [] ).filter( plugin => ! plugin.is_active ); + +export const SettingsSection = ( { integrations, loading, onToggleEnabled, onActivatePlugin, history } ) => { const integrationIds = Object.keys( integrations ); return ( @@ -54,12 +56,29 @@ export const SettingsSection = ( { integrations, loading, onToggleEnabled, histo { ! loading && integrationIds.length > 0 && ( { integrationIds.map( id => { - const { enabled, is_set_up: isSetUp, setup_url, name, description } = integrations[ id ]; + const integration = integrations[ id ]; + const { enabled, is_set_up: isSetUp, setup_url, name, description } = integration; + const missingPlugins = getMissingPlugins( integration ); + const uninstalledPlugin = missingPlugins.find( plugin => ! plugin.is_installed ); + const activatablePlugin = missingPlugins.length && ! uninstalledPlugin ? missingPlugins[ 0 ] : null; + const requirements = uninstalledPlugin + ? sprintf( + /* translators: %s: comma-separated list of required plugin names. */ + __( 'Requires %s', 'newspack-plugin' ), + missingPlugins.map( plugin => plugin.name ).join( ', ' ) + ) + : undefined; const isEnabled = enabled; const needsSetup = ! isSetUp && !! setup_url; const goToSetup = () => { window.location.href = setup_url; }; + let enableLabel = isSetUp ? __( 'Enable', 'newspack-plugin' ) : __( 'Connect', 'newspack-plugin' ); + let onEnable = needsSetup ? goToSetup : () => onToggleEnabled( id, true ); + if ( activatablePlugin ) { + enableLabel = __( 'Activate', 'newspack-plugin' ); + onEnable = () => onActivatePlugin( activatablePlugin.slug ); + } return ( onToggleEnabled( id, true ) } + onEnable={ onEnable } onConfigure={ needsSetup ? goToSetup : () => history?.push( `/settings/${ id }` ) } moreControls={ isEnabled From d957f2ef80d5c93a7192e53e2c863a2b0ea09e8e Mon Sep 17 00:00:00 2001 From: Rasmy Nguyen Date: Thu, 14 May 2026 12:12:26 -0400 Subject: [PATCH 2/4] feat(integrations): keep activate button clickable on inactive plugin Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/components/src/card-feature/README.md | 3 ++- packages/components/src/card-feature/index.tsx | 14 +++++++++++--- .../views/integrations/settings-section.js | 3 ++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/components/src/card-feature/README.md b/packages/components/src/card-feature/README.md index 6c7777ec35..d691cdcbb5 100644 --- a/packages/components/src/card-feature/README.md +++ b/packages/components/src/card-feature/README.md @@ -11,7 +11,7 @@ A card component for presenting a named feature or setting with a predictable, s | State | Condition | Button | Dropdown | Badge | |---|---|---|---|---| -| **Unmet requirements** | `requirements` is set | "Enable" — disabled | Hidden | Error badge with `requirements` text | +| **Unmet requirements** | `requirements` is set | "Enable" — disabled (clickable if `requirementsActionable`) | Hidden | Error badge with `requirements` text | | **Disabled** | `!enabled`, no requirements | "Enable" | Hidden | None | | **Enabled** | `enabled`, no requirements | "Configure" | Shown if `moreControls` provided | Success badge ("Enabled") | @@ -155,6 +155,7 @@ import { __ } from '@wordpress/i18n'; | `icon` | `CardFeatureIcon` | — | Icon displayed on the right. See `CardFeatureIcon` below. | | `enabled` | `boolean` | `false` | Whether the feature is currently enabled | | `requirements` | `string` | — | When set, enters the unmet-requirements state; value is used as the error badge text | +| `requirementsActionable` | `boolean` | `false` | When `requirements` is set, keep the primary button clickable so it can remediate the unmet requirement | | `enableLabel` | `string` | `"Enable"` | Primary button label when not enabled | | `configureLabel` | `string` | `"Configure"` | Primary button label when enabled | | `onEnable` | `() => void` | — | Called when the primary button is clicked and the feature is not enabled | diff --git a/packages/components/src/card-feature/index.tsx b/packages/components/src/card-feature/index.tsx index 079ddf040f..cef651d093 100644 --- a/packages/components/src/card-feature/index.tsx +++ b/packages/components/src/card-feature/index.tsx @@ -49,10 +49,17 @@ type CardFeatureProps = { /** Whether the feature is currently enabled. */ enabled?: boolean; /** - * When set, the card enters the "unmet requirements" state: the primary - * button is disabled and an error badge displays this string. + * When set, the card enters the "unmet requirements" state: an error + * badge displays this string and the title/description are muted. By + * default the primary button is disabled — set `requirementsActionable` + * if the primary button is the remediation for the unmet requirement. */ requirements?: string; + /** + * When `requirements` is set, keep the primary button clickable so the + * user can remediate the unmet requirement from this card. + */ + requirementsActionable?: boolean; /** Primary button label when not enabled. Default: "Enable". */ enableLabel?: string; /** Primary button label when enabled. Default: "Configure". */ @@ -83,6 +90,7 @@ const CardFeature = ( { icon, enabled = false, requirements, + requirementsActionable = false, enableLabel, configureLabel, onEnable, @@ -151,7 +159,7 @@ const CardFeature = ( {