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..1674368333318 --- /dev/null +++ b/projects/packages/my-jetpack/_inc/hooks/use-notification-watcher/use-protect-threats-detected-notice.tsx @@ -0,0 +1,126 @@ +import { Col, getRedirectUrl, Text } from '@automattic/jetpack-components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useContext, useEffect, useCallback } from 'react'; +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'; +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, + standalonePluginInfo, + manageUrl: protectDashboardUrl, + } = detail || {}; + const { isStandaloneActive } = standalonePluginInfo || {}; + + 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 && isStandaloneActive ? '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 && isStandaloneActive ? '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_MEDIUM, + }; + + setNotice( { + title: noticeTitle, + message: noticeMessage, + options: noticeOptions, + } ); + }, [ + hasPaidPlanForProduct, + isStandaloneActive, + noticeTitle, + onPrimaryCtaClick, + onSecondaryCtaClick, + redBubbleAlerts?.protect_has_threats, + setNotice, + type, + ] ); +}; + +export default useProtectThreatsDetectedNotice; 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. diff --git a/projects/packages/my-jetpack/global.d.ts b/projects/packages/my-jetpack/global.d.ts index 7c72d7535904b..cc0373cc75204 100644 --- a/projects/packages/my-jetpack/global.d.ts +++ b/projects/packages/my-jetpack/global.d.ts @@ -416,6 +416,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 78a64cc37106f..d80e8549de55a 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; + } }