From 24cd0c0710dbfb297cc2a27aa2cf7a4a27785637 Mon Sep 17 00:00:00 2001 From: Bryan Elliott Date: Sat, 21 Dec 2024 10:19:44 -0500 Subject: [PATCH 1/4] Add redbubble & notice when protect threats are detected. --- .../hooks/use-notification-watcher/index.ts | 2 + .../use-protect-threats-detected-notice.tsx | 121 ++++++++++++++++++ projects/packages/my-jetpack/global.d.ts | 8 ++ .../my-jetpack/src/class-initializer.php | 23 +++- 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx diff --git a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/index.ts b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/index.ts index 0efbfe8f3da00..37c1be01d288b 100644 --- a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/index.ts +++ b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/index.ts @@ -4,11 +4,13 @@ import useBadInstallNotice from './use-bad-install-notice'; import useConnectionErrorsNotice from './use-connection-errors-notice'; import useDeprecateFeatureNotice from './use-deprecate-feature-notice'; import useExpiringPlansNotice from './use-expiring-plans-notice'; +import useProtectThreatsDetectedNotice from './use-protect-threats-detected-notice'; import useSiteConnectionNotice from './use-site-connection-notice'; const useNotificationWatcher = () => { const { redBubbleAlerts } = getMyJetpackWindowInitialState(); + useProtectThreatsDetectedNotice( redBubbleAlerts ); useExpiringPlansNotice( redBubbleAlerts ); useBackupNeedsAttentionNotice( redBubbleAlerts ); useDeprecateFeatureNotice( redBubbleAlerts ); diff --git a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx new file mode 100644 index 0000000000000..2e1ff4727aa49 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx @@ -0,0 +1,121 @@ +import { Col, getRedirectUrl, Text } from '@automattic/jetpack-components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useContext, useEffect, useCallback } from 'react'; +import { NOTICE_PRIORITY_HIGH } from '../../context/constants'; +import { NoticeContext } from '../../context/notices/noticeContext'; +import useProduct from '../../data/products/use-product'; +import preventWidows from '../../utils/prevent-widows'; +import useAnalytics from '../use-analytics'; +import type { NoticeOptions } from '../../context/notices/types'; + +type RedBubbleAlerts = Window[ 'myJetpackInitialState' ][ 'redBubbleAlerts' ]; + +const useProtectThreatsDetectedNotice = ( redBubbleAlerts: RedBubbleAlerts ) => { + const { recordEvent } = useAnalytics(); + const { setNotice } = useContext( NoticeContext ); + const { detail } = useProduct( 'protect' ); + const { hasPaidPlanForProduct, isPluginActive, manageUrl: protectDashboardUrl } = detail || {}; + + const { + type, + data: { + threat_count: threatCount, + critical_threat_count: criticalThreatCount, + fixable_threat_ids: fixableThreatIds, + }, + } = redBubbleAlerts?.protect_has_threats || { type: 'warning', data: {} }; + + const fixThreatsLearnMoreUrl = getRedirectUrl( 'protect-footer-learn-more-scan', { + anchor: 'how-do-i-fix-threats', + } ); + + const noticeTitle = sprintf( + // translators: %s is the product name. Can be either "Scan" or "Protect". + __( '%s found threats on your site', 'jetpack-my-jetpack' ), + hasPaidPlanForProduct && isPluginActive ? 'Protect' : 'Scan' + ); + + const onPrimaryCtaClick = useCallback( () => { + window.open( protectDashboardUrl ); + recordEvent( 'jetpack_my_jetpack_protect_threats_detected_notice_primary_cta_click', { + threat_count: threatCount, + critical_threat_count: criticalThreatCount, + fixable_threat_ids: fixableThreatIds, + } ); + }, [ criticalThreatCount, fixableThreatIds, protectDashboardUrl, recordEvent, threatCount ] ); + + const onSecondaryCtaClick = useCallback( () => { + window.open( fixThreatsLearnMoreUrl ); + recordEvent( 'jetpack_my_jetpack_protect_threats_detected_notice_secondary_cta_click', { + threat_count: threatCount, + critical_threat_count: criticalThreatCount, + fixable_threat_ids: fixableThreatIds, + } ); + }, [ criticalThreatCount, fixThreatsLearnMoreUrl, fixableThreatIds, recordEvent, threatCount ] ); + + useEffect( () => { + if ( ! redBubbleAlerts?.protect_has_threats ) { + return; + } + + const noticeMessage = ( + + + { preventWidows( + __( + 'We’ve detected some security threats that need your attention.', + 'jetpack-my-jetpack' + ) + ) } + + + { preventWidows( + sprintf( + // translators: %s is the product name. Can be either "Scan" or "Protect". + __( + 'Visit the %s dashboard to view threat details, auto-fix threats, and keep your site safe.', + 'jetpack-my-jetpack' + ), + hasPaidPlanForProduct && isPluginActive ? 'Protect' : 'Scan' + ) + ) } + + + ); + + const noticeOptions: NoticeOptions = { + id: 'protect-threats-detected-notice', + level: type, + actions: [ + { + label: __( 'Fix threats', 'jetpack-my-jetpack' ), + onClick: onPrimaryCtaClick, + noDefaultClasses: true, + }, + { + label: __( 'Learn more', 'jetpack-my-jetpack' ), + onClick: onSecondaryCtaClick, + isExternalLink: true, + }, + ], + priority: NOTICE_PRIORITY_HIGH, + }; + + setNotice( { + title: noticeTitle, + message: noticeMessage, + options: noticeOptions, + } ); + }, [ + hasPaidPlanForProduct, + isPluginActive, + noticeTitle, + onPrimaryCtaClick, + onSecondaryCtaClick, + redBubbleAlerts?.protect_has_threats, + setNotice, + type, + ] ); +}; + +export default useProtectThreatsDetectedNotice; diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index 1d1a0adc25983..fe632fef4cfe4 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -412,6 +412,14 @@ interface Window { manage_url?: string; products_effected?: string[]; }; + protect_has_threats?: { + type: 'warning' | 'error'; + data: { + threat_count: number; + critical_threat_count: number; + fixable_threat_ids: number[]; + }; + }; }; recommendedModules: { modules: JetpackModule[] | null; diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index fd05d0ff87b0e..aecdc0afabc79 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -929,7 +929,8 @@ public static function add_red_bubble_alerts( array $red_bubble_slugs ) { return array_merge( self::alert_if_missing_connection( $red_bubble_slugs ), self::alert_if_last_backup_failed( $red_bubble_slugs ), - self::alert_if_paid_plan_expiring( $red_bubble_slugs ) + self::alert_if_paid_plan_expiring( $red_bubble_slugs ), + self::alert_if_protect_has_threats( $red_bubble_slugs ) ); } } @@ -1056,4 +1057,24 @@ public static function alert_if_last_backup_failed( array $red_bubble_slugs ) { return $red_bubble_slugs; } + + /** + * Add an alert slug if Protect has scan threats/vulnerabilities. + * + * @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing. + * @return array + */ + public static function alert_if_protect_has_threats( array $red_bubble_slugs ) { + // Make sure we're dealing with the Protect product only + if ( ! Products\Protect::has_paid_plan_for_product() ) { + return $red_bubble_slugs; + } + + $protect_threats_status = Products\Protect::does_module_need_attention(); + if ( $protect_threats_status ) { + $red_bubble_slugs['protect_has_threats'] = $protect_threats_status; + } + + return $red_bubble_slugs; + } } From 11435dabc3bde1cbfd71ead93753573e73250955 Mon Sep 17 00:00:00 2001 From: Bryan Elliott Date: Sat, 21 Dec 2024 10:49:04 -0500 Subject: [PATCH 2/4] Changelog. --- .../my-jetpack/changelog/add-protect-redbubble-and-notice | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/my-jetpack/changelog/add-protect-redbubble-and-notice diff --git a/projects/packages/my-jetpack/changelog/add-protect-redbubble-and-notice b/projects/packages/my-jetpack/changelog/add-protect-redbubble-and-notice new file mode 100644 index 0000000000000..8149be99c3828 --- /dev/null +++ b/projects/packages/my-jetpack/changelog/add-protect-redbubble-and-notice @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +My Jetpack: Adds a red bubble and notice when Protect threats are detected. From 514b27ad25e7f033096b32506fba892d6caab2b2 Mon Sep 17 00:00:00 2001 From: Bryan Elliott Date: Sat, 21 Dec 2024 11:47:19 -0500 Subject: [PATCH 3/4] Fix method of checking for standalone plugin. --- .../protect-card/use-protect-tooltip-copy.ts | 19 ++++++++++--------- .../use-protect-threats-detected-notice.tsx | 13 +++++++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts index c6bdab86dcfc9..d79132a0945d9 100644 --- a/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts +++ b/projects/packages/my-jetpack/_inc/components/product-cards-section/protect-card/use-protect-tooltip-copy.ts @@ -28,10 +28,11 @@ export function useProtectTooltipCopy(): TooltipContent { const slug = PRODUCT_SLUGS.PROTECT; const { detail } = useProduct( slug ); const { - isPluginActive: isProtectPluginActive, + standalonePluginInfo, hasPaidPlanForProduct: hasProtectPaidPlan, manageUrl: protectDashboardUrl, } = detail || {}; + const { isStandaloneActive } = standalonePluginInfo || {}; const { recordEvent } = useAnalytics(); const { plugins, @@ -60,11 +61,11 @@ export function useProtectTooltipCopy(): TooltipContent { }, [ threats ] ); const settingsLink = useMemo( () => { - if ( isProtectPluginActive ) { + if ( isStandaloneActive ) { return 'admin.php?page=jetpack-protect#/firewall'; } return isJetpackPluginActive() ? 'admin.php?page=jetpack#/settings' : null; - }, [ isProtectPluginActive ] ); + }, [ isStandaloneActive ] ); const trackFirewallSettingsLinkClick = useCallback( () => { recordEvent( 'jetpack_protect_card_tooltip_content_link_click', { @@ -84,7 +85,7 @@ export function useProtectTooltipCopy(): TooltipContent { } ); }, [ recordEvent, protectDashboardUrl ] ); - const isBruteForcePluginsActive = isProtectPluginActive || isJetpackPluginActive(); + const isBruteForcePluginsActive = isStandaloneActive || isJetpackPluginActive(); const blockedLoginsTooltip = useMemo( () => { if ( blockedLoginsCount === 0 ) { @@ -107,7 +108,7 @@ export function useProtectTooltipCopy(): TooltipContent { 'Brute Force Protection is disabled and not actively blocking malicious login attempts. Go to %s to activate it.', 'jetpack-my-jetpack' ), - isProtectPluginActive ? 'firewall settings' : 'Jetpack settings' + isStandaloneActive ? 'firewall settings' : 'Jetpack settings' ), { a: createElement( 'a', { @@ -143,7 +144,7 @@ export function useProtectTooltipCopy(): TooltipContent { 'Brute Force Protection is disabled and not actively blocking malicious login attempts. Go to %s to activate it.', 'jetpack-my-jetpack' ), - isProtectPluginActive ? 'firewall settings' : 'Jetpack settings' + isStandaloneActive ? 'firewall settings' : 'Jetpack settings' ), { a: createElement( 'a', { @@ -162,7 +163,7 @@ export function useProtectTooltipCopy(): TooltipContent { blockedLoginsCount, hasBruteForceProtection, isBruteForcePluginsActive, - isProtectPluginActive, + isStandaloneActive, settingsLink, trackFirewallSettingsLinkClick, ] ); @@ -206,7 +207,7 @@ export function useProtectTooltipCopy(): TooltipContent { numThreats ), criticalThreatCount, - isProtectPluginActive ? 'Protect' : 'Scan' + isStandaloneActive ? 'Protect' : 'Scan' ), { a: createElement( 'a', { @@ -227,7 +228,7 @@ export function useProtectTooltipCopy(): TooltipContent { _n( '%d threat', '%d threats', numThreats, 'jetpack-my-jetpack' ), numThreats ), - isProtectPluginActive ? 'Protect' : 'Scan' + isStandaloneActive ? 'Protect' : 'Scan' ), { a: createElement( 'a', { diff --git a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx index 2e1ff4727aa49..1386cb78c19d5 100644 --- a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx +++ b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx @@ -14,7 +14,12 @@ const useProtectThreatsDetectedNotice = ( redBubbleAlerts: RedBubbleAlerts ) => const { recordEvent } = useAnalytics(); const { setNotice } = useContext( NoticeContext ); const { detail } = useProduct( 'protect' ); - const { hasPaidPlanForProduct, isPluginActive, manageUrl: protectDashboardUrl } = detail || {}; + const { + hasPaidPlanForProduct, + standalonePluginInfo, + manageUrl: protectDashboardUrl, + } = detail || {}; + const { isStandaloneActive } = standalonePluginInfo || {}; const { type, @@ -32,7 +37,7 @@ const useProtectThreatsDetectedNotice = ( redBubbleAlerts: RedBubbleAlerts ) => const noticeTitle = sprintf( // translators: %s is the product name. Can be either "Scan" or "Protect". __( '%s found threats on your site', 'jetpack-my-jetpack' ), - hasPaidPlanForProduct && isPluginActive ? 'Protect' : 'Scan' + hasPaidPlanForProduct && isStandaloneActive ? 'Protect' : 'Scan' ); const onPrimaryCtaClick = useCallback( () => { @@ -76,7 +81,7 @@ const useProtectThreatsDetectedNotice = ( redBubbleAlerts: RedBubbleAlerts ) => 'Visit the %s dashboard to view threat details, auto-fix threats, and keep your site safe.', 'jetpack-my-jetpack' ), - hasPaidPlanForProduct && isPluginActive ? 'Protect' : 'Scan' + hasPaidPlanForProduct && isStandaloneActive ? 'Protect' : 'Scan' ) ) } @@ -108,7 +113,7 @@ const useProtectThreatsDetectedNotice = ( redBubbleAlerts: RedBubbleAlerts ) => } ); }, [ hasPaidPlanForProduct, - isPluginActive, + isStandaloneActive, noticeTitle, onPrimaryCtaClick, onSecondaryCtaClick, From 5ec9cb803f9c984feb78b857491022fe4acc295d Mon Sep 17 00:00:00 2001 From: Bryan Elliott Date: Sun, 22 Dec 2024 15:13:54 -0500 Subject: [PATCH 4/4] Change notice priority to medium. --- .../use-protect-threats-detected-notice.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx index 1386cb78c19d5..1674368333318 100644 --- a/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx +++ b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx @@ -1,7 +1,7 @@ import { Col, getRedirectUrl, Text } from '@automattic/jetpack-components'; import { __, sprintf } from '@wordpress/i18n'; import { useContext, useEffect, useCallback } from 'react'; -import { NOTICE_PRIORITY_HIGH } from '../../context/constants'; +import { NOTICE_PRIORITY_MEDIUM } from '../../context/constants'; import { NoticeContext } from '../../context/notices/noticeContext'; import useProduct from '../../data/products/use-product'; import preventWidows from '../../utils/prevent-widows'; @@ -103,7 +103,7 @@ const useProtectThreatsDetectedNotice = ( redBubbleAlerts: RedBubbleAlerts ) => isExternalLink: true, }, ], - priority: NOTICE_PRIORITY_HIGH, + priority: NOTICE_PRIORITY_MEDIUM, }; setNotice( {