Skip to content

Commit bd755df

Browse files
authored
My Jetpack: Link security modules to the Protect page (#53744)
* My Jetpack: Link security modules to the Protect page While the Protect package's dashboard is the Protect page, the Features tab's Firewall, Brute force, Account protection, Downtime monitoring and WordPress.com login rows offer "Manage in Protect" instead of a switch, and stay out of bulk selection. The Protect card's tooltips and its firewall manage and post-checkout URLs point at the matching settings card. * My Jetpack: Keep forced modules' Configure link, fix login link tracking A host-forced security module gets no switch and so no Protect page link either; it keeps the Configure link it had. The brute force tooltip's click now records the login link it opens rather than the firewall's. * My Jetpack: Drop the Protect page links once that page is switched off The URL map comes from page load, but the Protect row can be switched off in place on the Features tab. Check the modules store, once loaded, so the security rows get their switches back rather than links to a removed page.
1 parent 4826024 commit bd755df

12 files changed

Lines changed: 231 additions & 22 deletions

File tree

‎projects/packages/my-jetpack/_inc/components/my-jetpack-tab-panel/features/feature-action.tsx‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { LoadingPlaceholder } from '@automattic/jetpack-components';
22
import { FormToggle } from '@wordpress/components';
3-
import { __ } from '@wordpress/i18n';
3+
import { __, sprintf } from '@wordpress/i18n';
44
import { Button, ButtonLink } from '@wordpress/ui';
55
import { useCallback } from 'react';
6+
import { getProtectPageUrl } from '../../../utils/protect-page';
67
import { ModuleToggle } from '../../module-toggle';
78
import { getSwitchLabel } from '../utils';
89
import { getFeatureManageUrl, getForcedReason } from './feature-state';
@@ -156,6 +157,28 @@ export function FeatureAction( { state, describedby, origin = 'card' }: FeatureA
156157
return null;
157158
}
158159

160+
const protectPageUrl = getProtectPageUrl( control.module.module );
161+
if ( protectPageUrl ) {
162+
return (
163+
<ButtonLink
164+
href={ protectPageUrl }
165+
variant="outline"
166+
size="compact"
167+
aria-label={ sprintf(
168+
/* translators: %s is the feature name. "Protect" is a product name. */
169+
__( 'Manage %s in Protect', 'jetpack-my-jetpack' ),
170+
feature.name
171+
) }
172+
aria-describedby={ describedby }
173+
>
174+
{
175+
/* translators: "Protect" is a product name. */
176+
__( 'Manage in Protect', 'jetpack-my-jetpack' )
177+
}
178+
</ButtonLink>
179+
);
180+
}
181+
159182
return (
160183
<ModuleToggle
161184
module={ control.module }

‎projects/packages/my-jetpack/_inc/components/my-jetpack-tab-panel/features/feature-item.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import { Badge, Icon, Link, Text } from '@wordpress/ui';
55
import clsx from 'clsx';
66
import { useCallback } from 'react';
77
import { getBlockThemeMigration } from '../../../utils/block-theme-migration';
8+
import { getProtectPageUrl } from '../../../utils/protect-page';
89
import { getActivationStatusLabel } from '../utils';
910
import { FeatureAction } from './feature-action';
1011
import { FeatureIcon } from './feature-icon';
1112
import { FeatureInstallNotice } from './feature-install-notice';
1213
import { getModuleUnavailableNote } from './feature-state';
13-
import { getFeatureUnavailableReason } from './module-availability';
14+
import { getFeatureUnavailableReason, getModuleStatus } from './module-availability';
1415
import styles from './styles.module.scss';
1516
import { getDeprecatedModules } from './use-more-features';
1617
import type { FeatureState } from './feature-state';
@@ -30,6 +31,11 @@ export function getModuleSettingsUrl( $module: MyJetpackModule ): string | undef
3031
return undefined;
3132
}
3233

34+
// The Protect page link already stands in for the switch; a forced module gets neither, so it keeps this one.
35+
if ( getProtectPageUrl( $module.module ) && getModuleStatus( $module ).isAvailable ) {
36+
return undefined;
37+
}
38+
3339
const url = $module.configure_url;
3440
// Jetpack's fallback: a settings search, which for a module without options finds only its toggle.
3541
const isSettingsSearch = url?.includes( 'page=jetpack-settings#/settings?term=' );

‎projects/packages/my-jetpack/_inc/components/my-jetpack-tab-panel/features/test/feature-controls.test.tsx‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,43 @@ describe( 'FeatureItem', () => {
451451
}
452452
);
453453

454+
it.each( [
455+
[ 'a switch', false, 'Manage Firewall in Protect' ],
456+
[ 'nothing, for a module a host forced on,', 'active' as const, 'Configure Firewall' ],
457+
] )( 'links to the Protect page in place of %s', ( _, override, linkName ) => {
458+
const previousInitial = window.myJetpackInitialState;
459+
window.myJetpackInitialState = {
460+
protectPageModuleUrls: { waf: 'admin.php?page=jetpack-protect&p=firewall' },
461+
} as unknown as typeof previousInitial;
462+
const waf = {
463+
module: 'waf',
464+
available: true,
465+
activated: true,
466+
override,
467+
configure_url: 'admin.php?page=jetpack-protect&p=firewall',
468+
} as MyJetpackModule;
469+
470+
try {
471+
render(
472+
<FeatureItem
473+
state={ buildState(
474+
{ kind: 'module', module: waf },
475+
{ status: 'active', feature: buildFeature( { slug: 'waf', name: 'Firewall' } ) }
476+
) }
477+
/>
478+
);
479+
} finally {
480+
window.myJetpackInitialState = previousInitial;
481+
}
482+
483+
expect( screen.getByRole( 'link', { name: linkName } ) ).toHaveAttribute(
484+
'href',
485+
'admin.php?page=jetpack-protect&p=firewall'
486+
);
487+
expect( screen.getAllByRole( 'link' ) ).toHaveLength( 1 );
488+
expect( screen.queryByRole( 'checkbox' ) ).not.toBeInTheDocument();
489+
} );
490+
454491
it( 'shows only the status for a module a host forced on: no switch, no note', () => {
455492
const state = buildState(
456493
{ kind: 'module', module: forcedModule },

‎projects/packages/my-jetpack/_inc/components/my-jetpack-tab-panel/features/test/use-bulk-feature-switch.test.tsx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jest.mock( '@automattic/jetpack-shared-stores', () => ( { store: 'modules-store'
2121
jest.mock( '@wordpress/notices', () => ( { store: 'core/notices' } ) );
2222

2323
jest.mock( '@wordpress/data', () => ( {
24+
select: () => ( { hasFinishedResolution: () => false } ),
2425
useDispatch: () => ( { fetchModules: mockFetchModules } ),
2526
} ) );
2627

@@ -66,8 +67,19 @@ beforeEach( () => {
6667
} );
6768

6869
describe( 'isBulkSwitchable', () => {
70+
beforeAll( () => {
71+
window.myJetpackInitialState = {
72+
protectPageModuleUrls: { waf: 'admin.php?page=jetpack-protect' },
73+
} as unknown as Window[ 'myJetpackInitialState' ];
74+
} );
75+
76+
afterAll( () => {
77+
delete window.myJetpackInitialState;
78+
} );
79+
6980
it.each( [
7081
[ 'a module with a plain switch', moduleState( 'stats', 'inactive' ), true ],
82+
[ 'a module whose settings are on the Protect page', moduleState( 'waf', 'inactive' ), false ],
7183
[ 'a plugin switch', pluginState( 'akismet', 'active' ), true ],
7284
[ 'an unavailable module', moduleState( 'stats', 'inactive', { available: false } ), false ],
7385
[ 'a forced module', moduleState( 'stats', 'active', { override: 'active' } ), false ],

‎projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts‎

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import useProduct from '../../../data/products/use-product';
66
import { getMyJetpackWindowInitialState } from '../../../data/utils/get-my-jetpack-window-state';
77
import useAnalytics from '../../../hooks/use-analytics';
88
import { isJetpackPluginActive } from '../../../utils/is-jetpack-plugin-active';
9+
import { getProtectPageUrl } from '../../../utils/protect-page';
910

1011
type TooltipType =
1112
'pluginsThemesTooltip' | 'scanThreatsTooltip' | 'autoFirewallTooltip' | 'blockedLoginsTooltip';
@@ -56,15 +57,32 @@ export const useProtectTooltipCopy = ( data: ProtectData ): TooltipContent => {
5657
}
5758
return isJetpackPluginActive() ? 'admin.php?page=jetpack#/settings' : null;
5859
}, [ isStandaloneActive ] );
60+
const loginSettingsLink = getProtectPageUrl( 'protect' ) ?? settingsLink;
61+
const firewallSettingsLink = getProtectPageUrl( 'waf' ) ?? settingsLink;
62+
let loginSettingsName = isStandaloneActive ? 'firewall settings' : 'Jetpack settings';
63+
if ( getProtectPageUrl( 'protect' ) ) {
64+
/* translators: "Protect" is a product name. */
65+
loginSettingsName = __( 'Protect settings', 'jetpack-my-jetpack' );
66+
}
5967

6068
const trackFirewallSettingsLinkClick = useCallback( () => {
6169
recordEvent( 'jetpack_protect_card_tooltip_content_link_click', {
6270
page: 'my-jetpack',
6371
feature: 'jetpack-protect',
6472
location: 'auto-firewall-tooltip',
65-
path: settingsLink,
73+
path: firewallSettingsLink,
6674
} );
67-
}, [ recordEvent, settingsLink ] );
75+
}, [ recordEvent, firewallSettingsLink ] );
76+
77+
// Same location as the firewall link, as before, so only the path tells them apart.
78+
const trackLoginSettingsLinkClick = useCallback( () => {
79+
recordEvent( 'jetpack_protect_card_tooltip_content_link_click', {
80+
page: 'my-jetpack',
81+
feature: 'jetpack-protect',
82+
location: 'auto-firewall-tooltip',
83+
path: loginSettingsLink,
84+
} );
85+
}, [ recordEvent, loginSettingsLink ] );
6886

6987
const trackProtectDashboardLinkClick = useCallback( () => {
7088
recordEvent( 'jetpack_protect_card_tooltip_content_link_click', {
@@ -90,20 +108,20 @@ export const useProtectTooltipCopy = ( data: ProtectData ): TooltipContent => {
90108
}
91109
return {
92110
title: __( 'Brute Force Protection: Inactive', 'jetpack-my-jetpack' ),
93-
text: settingsLink
111+
text: loginSettingsLink
94112
? createInterpolateElement(
95113
sprintf(
96-
/* translators: %s is the location/page where the settings are located. Either "firewall settings" or "Jetpack settings". */
114+
/* translators: %s is the location/page where the settings are located: "Protect settings", "firewall settings" or "Jetpack settings". */
97115
__(
98116
'Brute Force Protection is disabled and not actively blocking malicious login attempts. Go to <a>%s</a> to activate it.',
99117
'jetpack-my-jetpack'
100118
),
101-
isStandaloneActive ? 'firewall settings' : 'Jetpack settings'
119+
loginSettingsName
102120
),
103121
{
104122
a: createElement( 'a', {
105-
href: settingsLink,
106-
onClick: trackFirewallSettingsLinkClick,
123+
href: loginSettingsLink,
124+
onClick: trackLoginSettingsLinkClick,
107125
} ),
108126
}
109127
)
@@ -126,20 +144,20 @@ export const useProtectTooltipCopy = ( data: ProtectData ): TooltipContent => {
126144
}
127145
return {
128146
title: __( 'Brute Force Protection: Inactive', 'jetpack-my-jetpack' ),
129-
text: settingsLink
147+
text: loginSettingsLink
130148
? createInterpolateElement(
131149
sprintf(
132-
/* translators: %s is the location/page where the settings are located. Either "firewall settings" or "Jetpack settings". */
150+
/* translators: %s is the location/page where the settings are located: "Protect settings", "firewall settings" or "Jetpack settings". */
133151
__(
134152
'Brute Force Protection is disabled and not actively blocking malicious login attempts. Go to <a>%s</a> to activate it.',
135153
'jetpack-my-jetpack'
136154
),
137-
isStandaloneActive ? 'firewall settings' : 'Jetpack settings'
155+
loginSettingsName
138156
),
139157
{
140158
a: createElement( 'a', {
141-
href: settingsLink,
142-
onClick: trackFirewallSettingsLinkClick,
159+
href: loginSettingsLink,
160+
onClick: trackLoginSettingsLinkClick,
143161
} ),
144162
}
145163
)
@@ -153,9 +171,9 @@ export const useProtectTooltipCopy = ( data: ProtectData ): TooltipContent => {
153171
blockedLoginsCount,
154172
hasBruteForceProtection,
155173
isBruteForcePluginsActive,
156-
isStandaloneActive,
157-
settingsLink,
158-
trackFirewallSettingsLinkClick,
174+
loginSettingsLink,
175+
loginSettingsName,
176+
trackLoginSettingsLinkClick,
159177
] );
160178

161179
return {
@@ -247,7 +265,7 @@ export const useProtectTooltipCopy = ( data: ProtectData ): TooltipContent => {
247265
),
248266
{
249267
a: createElement( 'a', {
250-
href: settingsLink,
268+
href: firewallSettingsLink,
251269
onClick: trackFirewallSettingsLinkClick,
252270
} ),
253271
}

‎projects/packages/my-jetpack/_inc/data/module-switch.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getBlockThemeMigration } from '../utils/block-theme-migration';
2+
import { getProtectPageUrl } from '../utils/protect-page';
23
import { queueActivationRequest } from './queue-activation-request';
34
import {
45
clearRequestedSwitch,
@@ -33,11 +34,15 @@ export function requestModuleSwitch(
3334
}
3435

3536
/**
36-
* Whether the module's control is a plain on/off switch, rather than locked or a migration link.
37+
* Whether the module's control is a plain on/off switch, rather than locked or a link elsewhere.
3738
*
3839
* @param $module - The module.
3940
* @return Whether the module has a plain switch.
4041
*/
4142
export function hasPlainSwitch( $module: MyJetpackModule ): boolean {
42-
return ! $module.override && ! getBlockThemeMigration( $module );
43+
return (
44+
! $module.override &&
45+
! getBlockThemeMigration( $module ) &&
46+
! getProtectPageUrl( $module.module )
47+
);
4348
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { store as modulesStore } from '@automattic/jetpack-shared-stores';
2+
import { select } from '@wordpress/data';
3+
import { getMyJetpackWindowInitialState } from '../data/utils/get-my-jetpack-window-state';
4+
5+
/**
6+
* Where the Protect page offers a Jetpack security module's settings, so other screens link there instead.
7+
*
8+
* @param slug - The module slug, such as `waf`.
9+
* @return The Protect page URL, or undefined when that page doesn't own the module.
10+
*/
11+
export function getProtectPageUrl( slug: string ): string | undefined {
12+
const url = getMyJetpackWindowInitialState( 'protectPageModuleUrls' )?.[ slug ];
13+
if ( ! url ) {
14+
return undefined;
15+
}
16+
17+
// The map is from page load; switching the Protect row off since then removes the page.
18+
const modules = select( modulesStore );
19+
const dashboard = modules.hasFinishedResolution( 'getJetpackModules' )
20+
? modules.getJetpackModules()?.[ 'protect-dashboard' ]
21+
: undefined;
22+
23+
return dashboard?.activated === false ? undefined : url;
24+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getProtectPageUrl } from '../protect-page';
2+
3+
const mockStore: { resolved: boolean; modules?: Record< string, { activated: boolean } > } = {
4+
resolved: false,
5+
};
6+
7+
jest.mock( '@automattic/jetpack-shared-stores', () => ( { store: 'modules-store' } ) );
8+
jest.mock( '@wordpress/data', () => ( {
9+
select: () => ( {
10+
hasFinishedResolution: () => mockStore.resolved,
11+
getJetpackModules: () => mockStore.modules,
12+
} ),
13+
} ) );
14+
15+
const url = 'admin.php?page=jetpack-protect&p=firewall';
16+
17+
beforeAll( () => {
18+
window.myJetpackInitialState = {
19+
protectPageModuleUrls: { waf: url },
20+
} as unknown as Window[ 'myJetpackInitialState' ];
21+
} );
22+
23+
afterAll( () => {
24+
delete window.myJetpackInitialState;
25+
} );
26+
27+
describe( 'getProtectPageUrl', () => {
28+
it.each( [
29+
[ 'keeps the page-load URL before the modules load', false, undefined, url ],
30+
[
31+
'keeps it while the Protect page is on',
32+
true,
33+
{ 'protect-dashboard': { activated: true } },
34+
url,
35+
],
36+
[
37+
'drops it once the Protect page is switched off',
38+
true,
39+
{ 'protect-dashboard': { activated: false } },
40+
undefined,
41+
],
42+
] )( '%s', ( _, resolved, modules, expected ) => {
43+
mockStore.resolved = resolved;
44+
mockStore.modules = modules;
45+
46+
expect( getProtectPageUrl( 'waf' ) ).toBe( expected );
47+
} );
48+
} );
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: minor
2+
Type: changed
3+
4+
Features: Link the five security modules to their card on the Protect page, in place of their switches, while that page owns their settings. Point the Protect card's settings links there too.

‎projects/packages/my-jetpack/global.d.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,8 @@ interface Window {
756756
showCard: boolean;
757757
redirect: string;
758758
};
759+
// Security modules whose settings live on the Protect page, keyed by module slug; empty without that page.
760+
protectPageModuleUrls?: Record< string, string >;
759761
// Null unless the partner coupon screen replaces the dashboard.
760762
partnerCoupon?: {
761763
coupon: {

0 commit comments

Comments
 (0)