Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 8 additions & 7 deletions includes/reader-activation/class-integrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Comment thread
chickenn00dle marked this conversation as resolved.
];
}
return $result;
Expand Down
17 changes: 17 additions & 0 deletions includes/reader-activation/integrations/class-esp.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
chickenn00dle marked this conversation as resolved.
],
];
}

/**
* Register the settings fields declared by this integration.
*
Expand Down
17 changes: 17 additions & 0 deletions includes/reader-activation/integrations/class-integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,23 @@ 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.
*
* Each entry must include all of `slug`, `name`, `is_active`, and `is_installed` —
* the integrations UI treats a missing `is_installed` as uninstalled and renders
* a disabled "Requires …" card instead of the Activate action.
*
* @return array List of associative arrays with keys `slug`, `name`, `is_active`, `is_installed`.
*/
public function get_required_plugins() {
return [];
}

/**
* Whether this integration supports frontend reader registration.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/components/src/card-feature/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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") |

Expand Down Expand Up @@ -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 |
Expand Down
14 changes: 11 additions & 3 deletions packages/components/src/card-feature/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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". */
Expand Down Expand Up @@ -83,6 +90,7 @@ const CardFeature = ( {
icon,
enabled = false,
requirements,
requirementsActionable = false,
enableLabel,
configureLabel,
onEnable,
Expand Down Expand Up @@ -151,7 +159,7 @@ const CardFeature = ( {
<HStack expanded={ false } spacing="8px">
<Button
variant={ isConfigureState ? 'tertiary' : 'secondary' }
disabled={ isMuted }
disabled={ isMuted && ! requirementsActionable }
onClick={ handleButtonClick }
size="compact"
>
Expand Down
10 changes: 10 additions & 0 deletions src/wizards/audience/views/integrations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ const AudienceIntegrations = ( props, ref ) => {
} );
}, [] );

const handleActivatePlugin = useCallback(
pluginSlug =>
apiFetch( {
path: `/newspack/v1/plugins/${ pluginSlug }/activate`,
method: 'POST',
} ).then( () => fetchSettings() ),
Comment thread
chickenn00dle marked this conversation as resolved.
[ fetchSettings ]
);

const sharedProps = {
integrations,
pendingChanges,
Expand All @@ -97,6 +106,7 @@ const AudienceIntegrations = ( props, ref ) => {
onFieldChange: handleFieldChange,
onSave: handleSave,
onToggleEnabled: handleToggleEnabled,
onActivatePlugin: handleActivatePlugin,
};

return (
Expand Down
31 changes: 26 additions & 5 deletions src/wizards/audience/views/integrations/settings-section.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Icon, envelope } from '@wordpress/icons';

/**
Expand Down Expand Up @@ -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 (
Expand All @@ -54,22 +56,41 @@ export const SettingsSection = ( { integrations, loading, onToggleEnabled, histo
{ ! loading && integrationIds.length > 0 && (
<Grid columns={ 2 } gutter={ 16 }>
{ 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 = missingPlugins.length
? 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 (
<CardFeature
key={ id }
title={ name }
description={ description }
icon={ INTEGRATION_ICONS[ id ] || DEFAULT_ICON }
enabled={ isEnabled }
enableLabel={ isSetUp ? __( 'Enable', 'newspack-plugin' ) : __( 'Connect', 'newspack-plugin' ) }
requirements={ requirements }
requirementsActionable={ !! activatablePlugin }
enableLabel={ enableLabel }
Comment thread
chickenn00dle marked this conversation as resolved.
configureLabel={ needsSetup ? __( 'Configure', 'newspack-plugin' ) : undefined }
onEnable={ needsSetup ? goToSetup : () => onToggleEnabled( id, true ) }
onEnable={ onEnable }
onConfigure={ needsSetup ? goToSetup : () => history?.push( `/settings/${ id }` ) }
moreControls={
isEnabled
Expand Down
105 changes: 105 additions & 0 deletions tests/unit-tests/integrations/class-test-esp.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@
*/
class Test_ESP extends \WP_UnitTestCase {

/**
* Whether a test mutated the cached get_plugins() result. tear_down deletes
* the cache when set, so the next caller rescans the real plugin directory.
*
* @var bool
*/
private $plugins_cache_dirty = false;

/**
* Snapshot of the `active_plugins` option taken before a test mutates it,
* so tear_down can restore the original value.
*
* @var array|null
*/
private $original_active_plugins = null;

/**
* Cleanup state set up by individual tests so failures don't leak across cases.
*/
Expand All @@ -27,9 +43,54 @@ public function tear_down() {
remove_all_filters( 'newspack_ras_metadata_keys' );
remove_all_filters( 'newspack_ras_metadata_prefix' );
\delete_option( 'newspack_integration_incoming_fields_esp' );
if ( $this->plugins_cache_dirty ) {
\wp_cache_delete( 'plugins', 'plugins' );
$this->plugins_cache_dirty = false;
}
if ( null !== $this->original_active_plugins ) {
\update_option( 'active_plugins', $this->original_active_plugins );
$this->original_active_plugins = null;
}
parent::tear_down();
}

/**
* Stub Plugin_Manager::get_managed_plugin_status() for the newspack-newsletters
* slug by pre-populating the cached get_plugins() result and the active_plugins option.
*
* Core's get_plugins() short-circuits on its `plugins` cache key in the `plugins`
* group, so writing into that cache bypasses the real filesystem scan. The
* all_plugins filter does NOT cover this path — it only fires from the admin
* plugins list table.
*
* @param string $status One of 'active', 'inactive', 'uninstalled'.
*/
private function stub_newsletters_status( $status ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}

$plugin_file = 'newspack-newsletters/newspack-newsletters.php';
$this->original_active_plugins = \get_option( 'active_plugins', [] );

$plugins = \get_plugins();
if ( 'uninstalled' === $status ) {
unset( $plugins[ $plugin_file ] );
} else {
$plugins[ $plugin_file ] = [
'Name' => 'Newspack Newsletters',
'Version' => '1.0.0',
];
}
\wp_cache_set( 'plugins', [ '' => $plugins ], 'plugins' );
$this->plugins_cache_dirty = true;

\update_option(
'active_plugins',
'active' === $status ? [ $plugin_file ] : []
);
}

/**
* Build an ESP instance with `get_master_list_id()` stubbed to return the given list id,
* so the test can exercise field-fetching paths without staging full newsletter settings.
Expand Down Expand Up @@ -408,4 +469,48 @@ public function test_get_available_incoming_fields_skips_entries_without_usable_
$this->assertCount( 1, $result );
$this->assertSame( 'good', $result[0]->get_key() );
}

/**
* Active newspack-newsletters maps to is_active=true, is_installed=true so the
* integrations UI shows the normal Enable/Connect action, not the requirements badge.
*/
public function test_get_required_plugins_reports_active_state() {
$this->stub_newsletters_status( 'active' );

$required = ( new ESP() )->get_required_plugins();

$this->assertCount( 1, $required );
$this->assertSame( 'newspack-newsletters', $required[0]['slug'] );
$this->assertSame( 'Newspack Newsletters', $required[0]['name'] );
$this->assertTrue( $required[0]['is_active'] );
$this->assertTrue( $required[0]['is_installed'] );
}

/**
* Installed-but-inactive newspack-newsletters maps to is_active=false, is_installed=true
* so the integrations UI shows the Activate remediation button.
*/
public function test_get_required_plugins_reports_inactive_state() {
$this->stub_newsletters_status( 'inactive' );

$required = ( new ESP() )->get_required_plugins();

$this->assertCount( 1, $required );
$this->assertFalse( $required[0]['is_active'] );
$this->assertTrue( $required[0]['is_installed'] );
}

/**
* Absent newspack-newsletters maps to is_active=false, is_installed=false so the
* integrations UI falls back to a disabled "Requires …" affordance.
*/
public function test_get_required_plugins_reports_uninstalled_state() {
$this->stub_newsletters_status( 'uninstalled' );

$required = ( new ESP() )->get_required_plugins();

$this->assertCount( 1, $required );
$this->assertFalse( $required[0]['is_active'] );
$this->assertFalse( $required[0]['is_installed'] );
}
}
Loading
Loading