diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4066146279aaf..3fe1b3a9b68b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4207,15 +4207,18 @@ importers: '@automattic/jetpack-connection': specifier: workspace:* version: link:../../js-packages/connection + '@tanstack/react-query': + specifier: 5.20.5 + version: 5.20.5(react@18.3.1) + '@tanstack/react-query-devtools': + specifier: 5.20.5 + version: 5.20.5(@tanstack/react-query@5.20.5(react@18.3.1))(react@18.3.1) '@wordpress/api-fetch': specifier: 7.5.0 version: 7.5.0 '@wordpress/components': specifier: 28.5.0 version: 28.5.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@wordpress/data': - specifier: 10.5.0 - version: 10.5.0(react@18.3.1) '@wordpress/date': specifier: 5.5.0 version: 5.5.0 @@ -7190,6 +7193,15 @@ packages: '@tanstack/query-core@5.20.5': resolution: {integrity: sha512-T1W28gGgWn0A++tH3lxj3ZuUVZZorsiKcv+R50RwmPYz62YoDEkG4/aXHZELGkRp4DfrW07dyq2K5dvJ4Wl1aA==} + '@tanstack/query-devtools@5.20.2': + resolution: {integrity: sha512-BZfSjhk/NGPbqte5E3Vc1Zbj28uWt///4I0DgzAdWrOtMVvdl0WlUXK23K2daLsbcyfoDR4jRI4f2Z5z/mMzuw==} + + '@tanstack/react-query-devtools@5.20.5': + resolution: {integrity: sha512-Wl7IzNuKCb4h41a5iH/YXNwalHItqJPCAr4r8+0iUYOLHNOf3E9P0G4kzZ9sqDoWKxY04qst6Vrij9bwPzLQRQ==} + peerDependencies: + '@tanstack/react-query': ^5.20.5 + react: ^18.0.0 + '@tanstack/react-query@4.35.3': resolution: {integrity: sha512-UgTPioip/rGG3EQilXfA2j4BJkhEQsR+KAbF+KIuvQ7j4MkgnTCJF01SfRpIRNtQTlEfz/+IL7+jP8WA8bFbsw==} peerDependencies: @@ -17387,6 +17399,14 @@ snapshots: '@tanstack/query-core@5.20.5': {} + '@tanstack/query-devtools@5.20.2': {} + + '@tanstack/react-query-devtools@5.20.5(@tanstack/react-query@5.20.5(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-devtools': 5.20.2 + '@tanstack/react-query': 5.20.5(react@18.3.1) + react: 18.3.1 + '@tanstack/react-query@4.35.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/query-core': 4.35.3 diff --git a/projects/plugins/protect/changelog/add-protect-tanstack-query b/projects/plugins/protect/changelog/add-protect-tanstack-query new file mode 100644 index 0000000000000..8499ce03ae272 --- /dev/null +++ b/projects/plugins/protect/changelog/add-protect-tanstack-query @@ -0,0 +1,5 @@ +Significance: patch +Type: added +Comment: Added react query, no user-facing impact. + + diff --git a/projects/plugins/protect/package.json b/projects/plugins/protect/package.json index 639e586f71945..c023fa69335b4 100644 --- a/projects/plugins/protect/package.json +++ b/projects/plugins/protect/package.json @@ -29,9 +29,10 @@ "@automattic/jetpack-base-styles": "workspace:*", "@automattic/jetpack-components": "workspace:*", "@automattic/jetpack-connection": "workspace:*", + "@tanstack/react-query": "5.20.5", + "@tanstack/react-query-devtools": "5.20.5", "@wordpress/api-fetch": "7.5.0", "@wordpress/components": "28.5.0", - "@wordpress/data": "10.5.0", "@wordpress/date": "5.5.0", "@wordpress/element": "6.5.0", "@wordpress/i18n": "5.5.0", diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index b2db8472267ea..88b320e9a61af 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -19,6 +19,7 @@ use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer; use Automattic\Jetpack\My_Jetpack\Products as My_Jetpack_Products; use Automattic\Jetpack\Plugins_Installer; +use Automattic\Jetpack\Protect\Credentials; use Automattic\Jetpack\Protect\Onboarding; use Automattic\Jetpack\Protect\REST_Controller; use Automattic\Jetpack\Protect\Scan_History; @@ -214,6 +215,7 @@ public function initial_state() { 'apiRoot' => esc_url_raw( rest_url() ), 'apiNonce' => wp_create_nonce( 'wp_rest' ), 'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ), + 'credentials' => Credentials::get_credential_array(), 'status' => Status::get_status( $refresh_status_from_wpcom ), 'scanHistory' => Scan_History::get_scan_history( $refresh_status_from_wpcom ), 'installedPlugins' => Plugins_Installer::get_plugins(), @@ -223,7 +225,7 @@ public function initial_state() { 'siteSuffix' => ( new Jetpack_Status() )->get_site_suffix(), 'blogID' => Connection_Manager::get_site_id( true ), 'jetpackScan' => My_Jetpack_Products::get_product( 'scan' ), - 'hasRequiredPlan' => Plan::has_required_plan(), + 'hasPlan' => Plan::has_required_plan(), 'onboardingProgress' => Onboarding::get_current_user_progress(), 'waf' => array( 'wafSupported' => Waf_Runner::is_supported_environment(), @@ -232,8 +234,6 @@ public function initial_state() { 'upgradeIsSeen' => self::get_waf_upgrade_seen_status(), 'displayUpgradeBadge' => self::get_waf_upgrade_badge_display_status(), 'isEnabled' => Waf_Runner::is_enabled(), - 'isToggling' => false, - 'isUpdating' => false, 'config' => Waf_Runner::get_config(), 'stats' => self::get_waf_stats(), 'globalStats' => Waf_Stats::get_global_stats(), diff --git a/projects/plugins/protect/src/class-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php index 2f89144f5a86b..f6834c1d743c9 100644 --- a/projects/plugins/protect/src/class-rest-controller.php +++ b/projects/plugins/protect/src/class-rest-controller.php @@ -10,8 +10,10 @@ namespace Automattic\Jetpack\Protect; use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication; +use Automattic\Jetpack\IP\Utils as IP_Utils; use Automattic\Jetpack\Protect_Status\REST_Controller as Protect_Status_REST_Controller; use Automattic\Jetpack\Waf\Waf_Runner; +use Automattic\Jetpack\Waf\Waf_Stats; use Jetpack_Protect; use WP_Error; use WP_REST_Request; @@ -380,10 +382,15 @@ public static function api_get_waf() { return new WP_REST_Response( array( - 'is_seen' => Jetpack_Protect::get_waf_seen_status(), - 'is_enabled' => Waf_Runner::is_enabled(), - 'config' => Waf_Runner::get_config(), - 'stats' => Jetpack_Protect::get_waf_stats(), + 'wafSupported' => Waf_Runner::is_supported_environment(), + 'currentIp' => IP_Utils::get_ip(), + 'isSeen' => Jetpack_Protect::get_waf_seen_status(), + 'upgradeIsSeen' => Jetpack_Protect::get_waf_upgrade_seen_status(), + 'displayUpgradeBadge' => Jetpack_Protect::get_waf_upgrade_badge_display_status(), + 'isEnabled' => Waf_Runner::is_enabled(), + 'config' => Waf_Runner::get_config(), + 'stats' => Jetpack_Protect::get_waf_stats(), + 'globalStats' => Waf_Stats::get_global_stats(), ) ); } diff --git a/projects/plugins/protect/src/js/api.js b/projects/plugins/protect/src/js/api.js index 30ab7a200896b..7952af7b5a5bb 100644 --- a/projects/plugins/protect/src/js/api.js +++ b/projects/plugins/protect/src/js/api.js @@ -2,7 +2,7 @@ import apiFetch from '@wordpress/api-fetch'; import camelize from 'camelize'; const API = { - fetchWaf: () => + getWaf: () => apiFetch( { path: 'jetpack-protect/v1/waf', method: 'GET', @@ -19,7 +19,7 @@ const API = { method: 'POST', path: 'jetpack/v4/waf', data, - } ), + } ).then( camelize ), wafSeen: () => apiFetch( { @@ -33,7 +33,7 @@ const API = { method: 'POST', } ), - fetchOnboardingProgress: () => + getOnboardingProgress: () => apiFetch( { path: 'jetpack-protect/v1/onboarding-progress', method: 'GET', @@ -46,11 +46,71 @@ const API = { data: { step_ids: stepIds }, } ), - fetchScanHistory: () => + getScanHistory: () => apiFetch( { path: 'jetpack-protect/v1/scan-history', method: 'GET', + } ).then( camelize ), + + scan: () => + apiFetch( { + path: `jetpack-protect/v1/scan`, + method: 'POST', } ), + + getScanStatus: () => + apiFetch( { + path: 'jetpack-protect/v1/status?hard_refresh=true', + method: 'GET', + } ).then( camelize ), + + fixThreats: threatIds => + apiFetch( { + path: `jetpack-protect/v1/fix-threats`, + method: 'POST', + data: { threat_ids: threatIds }, + } ), + + getFixersStatus: threatIds => { + const path = threatIds.reduce( ( carryPath, threatId ) => { + return `${ carryPath }threat_ids[]=${ threatId }&`; + }, 'jetpack-protect/v1/fix-threats-status?' ); + + return apiFetch( { + path, + method: 'GET', + } ); + }, + + ignoreThreat: threatId => + apiFetch( { + path: `jetpack-protect/v1/ignore-threat?threat_id=${ threatId }`, + method: 'POST', + } ), + + unIgnoreThreat: threatId => + apiFetch( { + path: `jetpack-protect/v1/unignore-threat?threat_id=${ threatId }`, + method: 'POST', + } ), + + checkCredentials: () => + apiFetch( { + path: 'jetpack-protect/v1/check-credentials', + method: 'POST', + } ), + + checkPlan: () => + apiFetch( { + path: 'jetpack-protect/v1/check-plan', + method: 'GET', + } ), + + getProductData: () => + apiFetch( { + path: '/my-jetpack/v1/site/products/scan', + method: 'GET', + } ).then( camelize ), }; export default API; diff --git a/projects/plugins/protect/src/js/components/admin-page/index.jsx b/projects/plugins/protect/src/js/components/admin-page/index.jsx index 6a0e509d7c743..bdeee21f24b77 100644 --- a/projects/plugins/protect/src/js/components/admin-page/index.jsx +++ b/projects/plugins/protect/src/js/components/admin-page/index.jsx @@ -1,62 +1,35 @@ import { AdminPage as JetpackAdminPage, Container } from '@automattic/jetpack-components'; -import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; -import apiFetch from '@wordpress/api-fetch'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useConnection } from '@automattic/jetpack-connection'; import { __ } from '@wordpress/i18n'; -import { addQueryArgs, getQueryArg } from '@wordpress/url'; -import React, { useEffect } from 'react'; -import { JETPACK_SCAN_SLUG } from '../../constants'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useNotices from '../../hooks/use-notices'; import useWafData from '../../hooks/use-waf-data'; -import { STORE_ID } from '../../state/store'; -import InterstitialPage from '../interstitial-page'; import Logo from '../logo'; import Notice from '../notice'; import Tabs, { Tab } from '../tabs'; import styles from './styles.module.scss'; -import useRegistrationWatcher from './use-registration-watcher'; const AdminPage = ( { children } ) => { - useRegistrationWatcher(); - + const { notice } = useNotices(); + const { isRegistered } = useConnection(); const { isSeen: wafSeen } = useWafData(); - const notice = useSelect( select => select( STORE_ID ).getNotice() ); - const { refreshPlan, startScanOptimistically, refreshStatus, refreshScanHistory } = - useDispatch( STORE_ID ); - const { adminUrl } = window.jetpackProtectInitialState || {}; - const { run, isRegistered, hasCheckoutStarted } = useProductCheckoutWorkflow( { - productSlug: JETPACK_SCAN_SLUG, - redirectUrl: addQueryArgs( adminUrl, { checkPlan: true } ), - siteProductAvailabilityHandler: async () => - apiFetch( { - path: 'jetpack-protect/v1/check-plan', - method: 'GET', - } ).then( hasRequiredPlan => hasRequiredPlan ), - useBlogIdSuffix: true, - } ); + const navigate = useNavigate(); + // Redirect to the setup page if the site is not registered. useEffect( () => { - if ( getQueryArg( window.location.search, 'checkPlan' ) ) { - startScanOptimistically(); - setTimeout( () => { - refreshPlan(); - refreshStatus( true ); - refreshScanHistory(); - }, 5000 ); + if ( ! isRegistered ) { + navigate( '/setup' ); } - }, [ refreshPlan, refreshStatus, refreshScanHistory, startScanOptimistically ] ); + }, [ isRegistered, navigate ] ); - /* - * Show interstital page when - * - Site is not registered - * - Checkout workflow has started - */ - if ( ! isRegistered || hasCheckoutStarted ) { - return ; + if ( ! isRegistered ) { + return null; } return ( }> - { notice.message && } + { notice && } diff --git a/projects/plugins/protect/src/js/components/admin-page/use-registration-watcher.js b/projects/plugins/protect/src/js/components/admin-page/use-registration-watcher.js deleted file mode 100644 index 01a100ba62bf7..0000000000000 --- a/projects/plugins/protect/src/js/components/admin-page/use-registration-watcher.js +++ /dev/null @@ -1,21 +0,0 @@ -import { useConnection } from '@automattic/jetpack-connection'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useEffect } from 'react'; -import { STORE_ID } from '../../state/store'; - -const useRegistrationWatcher = () => { - const { isRegistered } = useConnection(); - const { refreshStatus, refreshScanHistory } = useDispatch( STORE_ID ); - const status = useSelect( select => select( STORE_ID ).getStatus() ); - - useEffect( () => { - if ( isRegistered && ! status.status ) { - refreshStatus(); - refreshScanHistory(); - } - // We don't want to run the effect if status changes. Only on changes on isRegistered. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ isRegistered ] ); -}; - -export default useRegistrationWatcher; diff --git a/projects/plugins/protect/src/js/components/credentials-gate/index.jsx b/projects/plugins/protect/src/js/components/credentials-gate/index.jsx index daf2f9381b700..d93e99bdc17d3 100644 --- a/projects/plugins/protect/src/js/components/credentials-gate/index.jsx +++ b/projects/plugins/protect/src/js/components/credentials-gate/index.jsx @@ -1,23 +1,13 @@ import { Spinner } from '@automattic/jetpack-components'; -import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { STORE_ID } from '../../state/store'; +import useCredentialsQuery from '../../data/use-credentials-query'; import CredentialsNeededModal from '../credentials-needed-modal'; import styles from './styles.module.scss'; const CredentialsGate = ( { children } ) => { - const { checkCredentials } = useDispatch( STORE_ID ); + const { data: credentials, isLoading: credentialsIsFetching } = useCredentialsQuery(); - const { credentials, credentialsIsFetching } = useSelect( select => ( { - credentials: select( STORE_ID ).getCredentials(), - credentialsIsFetching: select( STORE_ID ).getCredentialsIsFetching(), - } ) ); - - if ( ! credentials && ! credentialsIsFetching ) { - checkCredentials(); - } - - if ( ! credentials ) { + if ( credentialsIsFetching ) { return (
{ ); } - if ( credentials.length === 0 ) { + if ( ! credentials || credentials.length === 0 ) { return ; } diff --git a/projects/plugins/protect/src/js/components/credentials-needed-modal/index.jsx b/projects/plugins/protect/src/js/components/credentials-needed-modal/index.jsx index b1c624869031c..fae5e28633d6e 100644 --- a/projects/plugins/protect/src/js/components/credentials-needed-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/credentials-needed-modal/index.jsx @@ -1,18 +1,19 @@ import { Button, Text, getRedirectUrl } from '@automattic/jetpack-components'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useQueryClient } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import { useEffect } from 'react'; -import { STORE_ID } from '../../state/store'; +import { QUERY_CREDENTIALS_KEY } from '../../constants'; +import useCredentialsQuery from '../../data/use-credentials-query'; +import useModal from '../../hooks/use-modal'; import Notice from '../notice'; import styles from './styles.module.scss'; const CredentialsNeededModal = () => { - const { setModal } = useDispatch( STORE_ID ); + const queryClient = useQueryClient(); + const { setModal } = useModal(); + const { data: credentials } = useCredentialsQuery(); const { siteSuffix, blogID } = window.jetpackProtectInitialState; - const { checkCredentials } = useDispatch( STORE_ID ); - const credentials = useSelect( select => select( STORE_ID ).getCredentials() ); - const handleCancelClick = () => { return event => { event.preventDefault(); @@ -26,12 +27,12 @@ const CredentialsNeededModal = () => { useEffect( () => { const interval = setInterval( () => { if ( ! credentials || credentials.length === 0 ) { - checkCredentials(); + queryClient.invalidateQueries( { queryKey: [ QUERY_CREDENTIALS_KEY ] } ); } - }, 3000 ); + }, 5_000 ); return () => clearInterval( interval ); - }, [ checkCredentials, credentials ] ); + }, [ queryClient, credentials ] ); return ( <> diff --git a/projects/plugins/protect/src/js/components/error-section/index.tsx b/projects/plugins/protect/src/js/components/error-section/index.tsx index 55868ffe1581b..b94c0e80a17db 100644 --- a/projects/plugins/protect/src/js/components/error-section/index.tsx +++ b/projects/plugins/protect/src/js/components/error-section/index.tsx @@ -44,6 +44,6 @@ export default function ErrorScreen( {
} preserveSecondaryOnMobile={ false } - /> + > ); } diff --git a/projects/plugins/protect/src/js/components/firewall-footer/index.jsx b/projects/plugins/protect/src/js/components/firewall-footer/index.jsx index 67283d0f21e2a..15429e13f13f8 100644 --- a/projects/plugins/protect/src/js/components/firewall-footer/index.jsx +++ b/projects/plugins/protect/src/js/components/firewall-footer/index.jsx @@ -1,18 +1,15 @@ import { AdminSectionHero, Title, Text, Button } from '@automattic/jetpack-components'; -import { CheckboxControl, ExternalLink } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; -import { createInterpolateElement } from '@wordpress/element'; +import { CheckboxControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useState, useEffect, useCallback } from 'react'; -import { FREE_PLUGIN_SUPPORT_URL, PAID_PLUGIN_SUPPORT_URL } from '../../constants'; -import useProtectData from '../../hooks/use-protect-data'; +import useModal from '../../hooks/use-modal'; +import useNotices from '../../hooks/use-notices'; import useWafData from '../../hooks/use-waf-data'; -import { STORE_ID } from '../../state/store'; import SeventyFiveLayout from '../seventy-five-layout'; import styles from './styles.module.scss'; const StandaloneMode = () => { - const { setModal } = useDispatch( STORE_ID ); + const { setModal } = useModal(); const handleClick = () => { return event => { @@ -46,9 +43,8 @@ const StandaloneMode = () => { const ShareDebugData = () => { const { config, isUpdating, toggleShareDebugData } = useWafData(); - const { hasRequiredPlan } = useProtectData(); const { jetpackWafShareDebugData } = config || {}; - const { setNotice } = useDispatch( STORE_ID ); + const { showSuccessNotice, showErrorNotice } = useNotices(); const [ settings, setSettings ] = useState( { jetpack_waf_share_debug_data: jetpackWafShareDebugData, @@ -60,34 +56,11 @@ const ShareDebugData = () => { jetpack_waf_share_debug_data: ! settings.jetpack_waf_share_debug_data, } ); toggleShareDebugData() - .then( () => - setNotice( { - type: 'success', - duration: 5000, - dismissable: true, - message: __( 'Changes saved.', 'jetpack-protect' ), - } ) - ) + .then( () => showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ) ) .catch( () => { - setNotice( { - type: 'error', - dismissable: true, - message: createInterpolateElement( - __( - 'An error ocurred. Please try again or contact support.', - 'jetpack-protect' - ), - { - supportLink: ( - - ), - } - ), - } ); + showErrorNotice(); } ); - }, [ settings, toggleShareDebugData, setNotice, hasRequiredPlan ] ); + }, [ settings, toggleShareDebugData, showSuccessNotice, showErrorNotice ] ); useEffect( () => { setSettings( { @@ -117,9 +90,8 @@ const ShareDebugData = () => { const ShareData = () => { const { config, isUpdating, toggleShareData } = useWafData(); - const { hasRequiredPlan } = useProtectData(); const { jetpackWafShareData } = config || {}; - const { setNotice } = useDispatch( STORE_ID ); + const { showSuccessNotice, showErrorNotice } = useNotices(); const [ settings, setSettings ] = useState( { jetpack_waf_share_data: jetpackWafShareData, @@ -128,34 +100,11 @@ const ShareData = () => { const handleShareDataChange = useCallback( () => { setSettings( { ...settings, jetpack_waf_share_data: ! settings.jetpack_waf_share_data } ); toggleShareData() - .then( () => - setNotice( { - type: 'success', - duration: 5000, - dismissable: true, - message: __( 'Changes saved.', 'jetpack-protect' ), - } ) - ) + .then( () => showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ) ) .catch( () => { - setNotice( { - type: 'error', - dismissable: true, - message: createInterpolateElement( - __( - 'An error ocurred. Please try again or contact support.', - 'jetpack-protect' - ), - { - supportLink: ( - - ), - } - ), - } ); + showErrorNotice(); } ); - }, [ settings, toggleShareData, setNotice, hasRequiredPlan ] ); + }, [ settings, toggleShareData, showSuccessNotice, showErrorNotice ] ); useEffect( () => { setSettings( { diff --git a/projects/plugins/protect/src/js/components/firewall-header/index.jsx b/projects/plugins/protect/src/js/components/firewall-header/index.jsx index b0552bdafd82c..c4a65bab7c63e 100644 --- a/projects/plugins/protect/src/js/components/firewall-header/index.jsx +++ b/projects/plugins/protect/src/js/components/firewall-header/index.jsx @@ -7,33 +7,29 @@ import { Button, Status, } from '@automattic/jetpack-components'; -import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; import { Spinner, Popover } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { Icon, help } from '@wordpress/icons'; import React, { useState, useCallback } from 'react'; -import { JETPACK_SCAN_SLUG } from '../../constants'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import useProtectData from '../../hooks/use-protect-data'; +import usePlan from '../../hooks/use-plan'; import useWafData from '../../hooks/use-waf-data'; import styles from './styles.module.scss'; const UpgradePrompt = () => { + const { recordEvent } = useAnalyticsTracks(); const { adminUrl } = window.jetpackProtectInitialState || {}; const firewallUrl = adminUrl + '#/firewall'; + const { upgradePlan } = usePlan( { redirectUrl: firewallUrl } ); const { config: { automaticRulesAvailable }, } = useWafData(); - const { run } = useProductCheckoutWorkflow( { - productSlug: JETPACK_SCAN_SLUG, - redirectUrl: firewallUrl, - useBlogIdSuffix: true, - } ); - - const { recordEventHandler } = useAnalyticsTracks(); - const getScan = recordEventHandler( 'jetpack_protect_waf_header_get_scan_link_click', run ); + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_waf_header_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); return ( - diff --git a/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx b/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx index a2e222867d2e9..8fb56f5517f78 100644 --- a/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/ignore-threat-modal/index.jsx @@ -1,16 +1,16 @@ import { Button, getRedirectUrl, Text } from '@automattic/jetpack-components'; -import { useDispatch, useSelect } from '@wordpress/data'; import { createInterpolateElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Icon } from '@wordpress/icons'; -import { STORE_ID } from '../../state/store'; +import useIgnoreThreatMutation from '../../data/scan/use-ignore-threat-mutation'; +import useModal from '../../hooks/use-modal'; import ThreatSeverityBadge from '../severity'; import UserConnectionGate from '../user-connection-gate'; import styles from './styles.module.scss'; const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { - const { setModal, ignoreThreat } = useDispatch( STORE_ID ); - const threatsUpdating = useSelect( select => select( STORE_ID ).getThreatsUpdating() ); + const { setModal } = useModal(); + const ignoreThreatMutation = useIgnoreThreatMutation(); const codeableURL = getRedirectUrl( 'jetpack-protect-codeable-referral' ); const handleCancelClick = () => { @@ -23,9 +23,8 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { const handleIgnoreClick = () => { return async event => { event.preventDefault(); - ignoreThreat( id, () => { - setModal( { type: null } ); - } ); + await ignoreThreatMutation.mutateAsync( id ); + setModal( { type: null } ); }; }; @@ -66,7 +65,7 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { - + ); diff --git a/projects/plugins/protect/src/js/components/notice/index.jsx b/projects/plugins/protect/src/js/components/notice/index.jsx index 911b116da9d4d..cf779386bb35c 100644 --- a/projects/plugins/protect/src/js/components/notice/index.jsx +++ b/projects/plugins/protect/src/js/components/notice/index.jsx @@ -1,8 +1,7 @@ -import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { check, close, info, warning, Icon } from '@wordpress/icons'; import { useCallback, useEffect } from 'react'; -import { STORE_ID } from '../../state/store'; +import useNotices from '../../hooks/use-notices'; import styles from './styles.module.scss'; const Notice = ( { @@ -12,7 +11,7 @@ const Notice = ( { message, type = 'success', } ) => { - const { clearNotice } = useDispatch( STORE_ID ); + const { clearNotice } = useNotices(); let icon; switch ( type ) { diff --git a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx index 5264208ed34e8..690bd3dd0d113 100644 --- a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx +++ b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx @@ -1,11 +1,10 @@ import { Spinner, Text, useBreakpointMatch } from '@automattic/jetpack-components'; -import { useSelect } from '@wordpress/data'; import { dateI18n } from '@wordpress/date'; import { sprintf, __ } from '@wordpress/i18n'; import { Icon, check, chevronDown, chevronUp } from '@wordpress/icons'; import clsx from 'clsx'; import React, { useState, useCallback, useContext } from 'react'; -import { STORE_ID } from '../../state/store'; +import useFixers from '../../hooks/use-fixers'; import ThreatSeverityBadge from '../severity'; import styles from './styles.module.scss'; @@ -75,13 +74,14 @@ export const PaidAccordionItem = ( { const accordionData = useContext( PaidAccordionContext ); const open = accordionData?.open === id; const setOpen = accordionData?.setOpen; - const threatsAreFixing = useSelect( select => select( STORE_ID ).getThreatsAreFixing() ); const bodyClassNames = clsx( styles[ 'accordion-body' ], { [ styles[ 'accordion-body-open' ] ]: open, [ styles[ 'accordion-body-close' ] ]: ! open, } ); + const { fixersStatus } = useFixers(); + const handleClick = useCallback( () => { if ( ! open ) { onOpen?.(); @@ -122,7 +122,7 @@ export const PaidAccordionItem = ( {
{ fixable && ( <> - { threatsAreFixing.indexOf( id ) >= 0 ? ( + { fixersStatus?.threats?.[ id ]?.status === 'in_progress' ? ( ) : ( diff --git a/projects/plugins/protect/src/js/components/paid-plan-gate/index.tsx b/projects/plugins/protect/src/js/components/paid-plan-gate/index.tsx index 9f00fc3446df8..93fceaa94f188 100644 --- a/projects/plugins/protect/src/js/components/paid-plan-gate/index.tsx +++ b/projects/plugins/protect/src/js/components/paid-plan-gate/index.tsx @@ -1,5 +1,5 @@ import { Navigate } from 'react-router-dom'; -import useProtectData from '../../hooks/use-protect-data'; +import usePlan from '../../hooks/use-plan'; /** * Paid Plan Gate @@ -19,9 +19,9 @@ export default function PaidPlanGate( { children?: JSX.Element; redirect?: string; } ): JSX.Element { - const { hasRequiredPlan } = useProtectData(); + const { hasPlan } = usePlan(); - if ( ! hasRequiredPlan ) { + if ( ! hasPlan ) { return ; } diff --git a/projects/plugins/protect/src/js/components/pricing-table/index.jsx b/projects/plugins/protect/src/js/components/pricing-table/index.jsx index 3b2b4bc4c8ac1..3edd7911a0b6a 100644 --- a/projects/plugins/protect/src/js/components/pricing-table/index.jsx +++ b/projects/plugins/protect/src/js/components/pricing-table/index.jsx @@ -1,6 +1,3 @@ -/** - * External dependencies - */ import { Button, ProductPrice, @@ -10,34 +7,30 @@ import { PricingTableItem, } from '@automattic/jetpack-components'; import { useConnection } from '@automattic/jetpack-connection'; -import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import useConnectSiteMutation from '../../data/use-connection-mutation'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; +import usePlan from '../../hooks/use-plan'; import useProtectData from '../../hooks/use-protect-data'; -import useWafData from '../../hooks/use-waf-data'; -import { STORE_ID } from '../../state/store'; /** * Product Detail component. * - * @param {object} props - Component props - * @param {Function} props.onScanAdd - Callback when adding paid protect product successfully * @return {object} ConnectedPricingTable react component. */ -const ConnectedPricingTable = ( { onScanAdd } ) => { - const { handleRegisterSite, registrationError } = useConnection( { +const ConnectedPricingTable = () => { + const navigate = useNavigate(); + const { recordEvent } = useAnalyticsTracks(); + const connectSiteMutation = useConnectSiteMutation(); + const { upgradePlan, isLoading: isPlanLoading } = usePlan(); + const { registrationError } = useConnection( { skipUserConnection: true, } ); - const { refreshPlan, refreshStatus, startScanOptimistically } = useDispatch( STORE_ID ); - - const [ getProtectFreeButtonIsLoading, setGetProtectFreeButtonIsLoading ] = useState( false ); - const [ getScanButtonIsLoading, setGetScanButtonIsLoading ] = useState( false ); - // Access paid protect product data const { jetpackScan } = useProtectData(); - const { refreshWaf } = useWafData(); const { pricingForUi } = jetpackScan; const { introductoryOffer, currencyCode: currency = 'USD' } = pricingForUi; @@ -47,33 +40,16 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { ? Math.ceil( ( introductoryOffer.costPerInterval / 12 ) * 100 ) / 100 : null; - // Track free and paid click events - const { recordEvent, recordEventHandler } = useAnalyticsTracks(); - const getScan = recordEventHandler( 'jetpack_protect_pricing_table_get_scan_link_click', () => { - setGetScanButtonIsLoading( true ); - onScanAdd(); - } ); + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_pricing_table_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); const getProtectFree = useCallback( async () => { recordEvent( 'jetpack_protect_connected_product_activated' ); - setGetProtectFreeButtonIsLoading( true ); - try { - await handleRegisterSite(); - startScanOptimistically(); - await refreshPlan(); - await refreshWaf(); - await refreshStatus( true ); - } finally { - setGetProtectFreeButtonIsLoading( false ); - } - }, [ - handleRegisterSite, - recordEvent, - refreshWaf, - refreshPlan, - refreshStatus, - startScanOptimistically, - ] ); + await connectSiteMutation.mutateAsync(); + navigate( '/scan' ); + }, [ connectSiteMutation, recordEvent, navigate ] ); const args = { title: __( 'Stay one step ahead of threats', 'jetpack-protect' ), @@ -120,8 +96,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { @@ -158,8 +134,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { fullWidth variant="secondary" onClick={ getProtectFree } - isLoading={ getProtectFreeButtonIsLoading } - disabled={ getProtectFreeButtonIsLoading || getScanButtonIsLoading } + isLoading={ connectSiteMutation.isPending } + disabled={ connectSiteMutation.isPending || isPlanLoading } error={ registrationError ? __( 'An error occurred. Please try again.', 'jetpack-protect' ) diff --git a/projects/plugins/protect/src/js/components/scan-button/index.jsx b/projects/plugins/protect/src/js/components/scan-button/index.jsx index 91554cdaf4b93..4844d9e4c060c 100644 --- a/projects/plugins/protect/src/js/components/scan-button/index.jsx +++ b/projects/plugins/protect/src/js/components/scan-button/index.jsx @@ -1,28 +1,20 @@ import { Button } from '@automattic/jetpack-components'; -import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import React, { forwardRef } from 'react'; -import { STORE_ID } from '../../state/store'; +import useStartScanMutator from '../../data/scan/use-start-scan-mutation'; const ScanButton = forwardRef( ( { variant = 'secondary', children, ...props }, ref ) => { - const { scan } = useDispatch( STORE_ID ); - const scanIsEnqueuing = useSelect( select => select( STORE_ID ).getScanIsEnqueuing(), [] ); + const startScanMutation = useStartScanMutator(); const handleScanClick = () => { return event => { event.preventDefault(); - scan(); + startScanMutation.mutate(); }; }; return ( - ); diff --git a/projects/plugins/protect/src/js/components/scan-footer/index.jsx b/projects/plugins/protect/src/js/components/scan-footer/index.jsx index 341212a746366..200752c48e676 100644 --- a/projects/plugins/protect/src/js/components/scan-footer/index.jsx +++ b/projects/plugins/protect/src/js/components/scan-footer/index.jsx @@ -7,31 +7,25 @@ import { Col, Container, } from '@automattic/jetpack-components'; -import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; import { __, sprintf } from '@wordpress/i18n'; -import React from 'react'; -import { JETPACK_SCAN_SLUG } from '../../constants'; +import React, { useCallback } from 'react'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; -import useProtectData from '../../hooks/use-protect-data'; +import usePlan from '../../hooks/use-plan'; import useWafData from '../../hooks/use-waf-data'; import SeventyFiveLayout from '../seventy-five-layout'; import styles from './styles.module.scss'; const ProductPromotion = () => { - const { adminUrl, siteSuffix, blogID } = window.jetpackProtectInitialState || {}; + const { recordEvent } = useAnalyticsTracks(); + const { hasPlan, upgradePlan } = usePlan(); + const { siteSuffix, blogID } = window.jetpackProtectInitialState || {}; - const { run } = useProductCheckoutWorkflow( { - productSlug: JETPACK_SCAN_SLUG, - redirectUrl: adminUrl, - useBlogIdSuffix: true, - } ); + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_footer_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); - const { recordEventHandler } = useAnalyticsTracks(); - const getScan = recordEventHandler( 'jetpack_protect_footer_get_scan_link_click', run ); - - const { hasRequiredPlan } = useProtectData(); - - if ( hasRequiredPlan ) { + if ( hasPlan ) { const goToCloudUrl = getRedirectUrl( 'jetpack-scan-dash', { site: blogID ?? siteSuffix } ); return ( @@ -74,14 +68,14 @@ const ProductPromotion = () => { }; const FooterInfo = () => { - const { hasRequiredPlan } = useProtectData(); + const { hasPlan } = usePlan(); const { globalStats } = useWafData(); const totalVulnerabilities = parseInt( globalStats?.totalVulnerabilities ); const totalVulnerabilitiesFormatted = isNaN( totalVulnerabilities ) ? '50,000' : totalVulnerabilities.toLocaleString(); - if ( hasRequiredPlan ) { + if ( hasPlan ) { const learnMoreScanUrl = getRedirectUrl( 'protect-footer-learn-more-scan' ); return ( diff --git a/projects/plugins/protect/src/js/components/summary/index.jsx b/projects/plugins/protect/src/js/components/summary/index.jsx index 60abb79dba7a3..618f4b78213ad 100644 --- a/projects/plugins/protect/src/js/components/summary/index.jsx +++ b/projects/plugins/protect/src/js/components/summary/index.jsx @@ -2,6 +2,7 @@ import { useBreakpointMatch } from '@automattic/jetpack-components'; import { dateI18n } from '@wordpress/date'; import { __, sprintf } from '@wordpress/i18n'; import React, { useState } from 'react'; +import usePlan from '../../hooks/use-plan'; import useProtectData from '../../hooks/use-protect-data'; import ScanSectionHeader from '../../routes/scan/scan-section-header'; import OnboardingPopover from '../onboarding-popover'; @@ -13,8 +14,8 @@ const Summary = () => { current: { threats: numThreats }, }, lastChecked, - hasRequiredPlan, } = useProtectData(); + const { hasPlan } = usePlan(); // Popover anchors const [ dailyScansPopoverAnchor, setDailyScansPopoverAnchor ] = useState( null ); @@ -40,7 +41,7 @@ const Summary = () => { dateI18n( 'F jS', lastChecked ) ) }
- { ! hasRequiredPlan && ( + { ! hasPlan && ( { }; const EmptyList = () => { - const { lastChecked, hasRequiredPlan } = useProtectData(); + const { lastChecked } = useProtectData(); + const { hasPlan } = usePlan(); const [ dailyAndManualScansPopoverAnchor, setDailyAndManualScansPopoverAnchor ] = useState( null ); @@ -115,7 +117,7 @@ const EmptyList = () => { } ) } - { hasRequiredPlan && ( + { hasPlan && ( <> { - const { adminUrl } = window.jetpackProtectInitialState || {}; - const { run } = useProductCheckoutWorkflow( { - productSlug: JETPACK_SCAN_SLUG, - redirectUrl: adminUrl, - useBlogIdSuffix: true, - } ); + const { recordEvent } = useAnalyticsTracks(); + const { upgradePlan } = usePlan(); - const { recordEventHandler } = useAnalyticsTracks(); - const getScan = recordEventHandler( 'jetpack_protect_threat_list_get_scan_link_click', run ); + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_threat_list_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); const learnMoreButton = source ? ( diff --git a/projects/plugins/protect/src/js/routes/scan/history/index.jsx b/projects/plugins/protect/src/js/routes/scan/history/index.jsx index e30dc0c4c65bc..1c0014983d296 100644 --- a/projects/plugins/protect/src/js/routes/scan/history/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/history/index.jsx @@ -10,6 +10,7 @@ import ThreatsNavigation from '../../../components/threats-list/navigation'; import PaidList from '../../../components/threats-list/paid-list'; import useThreatsList from '../../../components/threats-list/use-threats-list'; import useAnalyticsTracks from '../../../hooks/use-analytics-tracks'; +import usePlan from '../../../hooks/use-plan'; import useProtectData from '../../../hooks/use-protect-data'; import ScanSectionHeader from '../scan-section-header'; import StatusFilters from './status-filters'; @@ -19,6 +20,7 @@ const ScanHistoryRoute = () => { // Track page view. useAnalyticsTracks( { pageViewEventName: 'protect_scan_history' } ); + const { hasPlan } = usePlan(); const { filter = 'all' } = useParams(); const { item, list, selected, setSelected } = useThreatsList( { @@ -26,7 +28,7 @@ const ScanHistoryRoute = () => { status: filter, } ); - const { counts, error, hasRequiredPlan } = useProtectData( { + const { counts, error } = useProtectData( { sourceType: 'history', filter: { status: filter }, } ); @@ -228,7 +230,7 @@ const ScanHistoryRoute = () => { }, [ selected, list.length, filter, item?.name, item?.version ] ); // Threat history is only available for paid plans. - if ( ! hasRequiredPlan ) { + if ( ! hasPlan ) { return ; } diff --git a/projects/plugins/protect/src/js/routes/scan/index.jsx b/projects/plugins/protect/src/js/routes/scan/index.jsx index b848b6cb2dc6c..84b1ee75a21a5 100644 --- a/projects/plugins/protect/src/js/routes/scan/index.jsx +++ b/projects/plugins/protect/src/js/routes/scan/index.jsx @@ -1,9 +1,8 @@ import { AdminSectionHero, Container, Col, H3, Text } from '@automattic/jetpack-components'; import { useConnectionErrorNotice, ConnectionError } from '@automattic/jetpack-connection'; import { Spinner } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; import { __, sprintf } from '@wordpress/i18n'; -import React, { useEffect, useMemo } from 'react'; +import { useMemo } from 'react'; import inProgressImage from '../../../../assets/images/in-progress.png'; import AdminPage from '../../components/admin-page'; import ErrorScreen from '../../components/error-section'; @@ -12,17 +11,15 @@ import ScanFooter from '../../components/scan-footer'; import SeventyFiveLayout from '../../components/seventy-five-layout'; import Summary from '../../components/summary'; import ThreatsList from '../../components/threats-list'; -import { SCAN_STATUS_UNAVAILABLE } from '../../constants'; +import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; import { OnboardingContext } from '../../hooks/use-onboarding'; +import usePlan from '../../hooks/use-plan'; import useProtectData from '../../hooks/use-protect-data'; import useWafData from '../../hooks/use-waf-data'; -import { STORE_ID } from '../../state/store'; import onboardingSteps from './onboarding-steps'; import ScanSectionHeader from './scan-section-header'; import styles from './styles.module.scss'; -import useCredentials from './use-credentials'; -import useStatusPolling from './use-status-polling'; const ConnectionErrorCol = () => { const { hasConnectionError } = useConnectionErrorNotice(); @@ -73,7 +70,7 @@ const ErrorSection = ( { errorMessage, errorCode } ) => { }; const ScanningSection = ( { currentProgress } ) => { - const { hasRequiredPlan } = useProtectData(); + const { hasPlan } = usePlan(); const { globalStats } = useWafData(); const totalVulnerabilities = parseInt( globalStats?.totalVulnerabilities ); const totalVulnerabilitiesFormatted = isNaN( totalVulnerabilities ) @@ -106,9 +103,7 @@ const ScanningSection = ( { currentProgress } ) => {

{ __( 'Your results will be ready soon', 'jetpack-protect' ) }

- { hasRequiredPlan && currentProgress !== null && currentProgress >= 0 && ( - - ) } + { hasPlan && } { sprintf( // translators: placeholder is the number of total vulnerabilities i.e. "22,000". @@ -153,20 +148,12 @@ const DefaultSection = () => { }; const ScanPage = () => { - const { lastChecked, hasRequiredPlan } = useProtectData(); - const { refreshStatus } = useDispatch( STORE_ID ); - const { scanInProgress, statusIsFetching, scanIsUnavailable, status, scanError } = useSelect( - select => ( { - scanError: select( STORE_ID ).scanError(), - scanInProgress: select( STORE_ID ).scanInProgress(), - scanIsUnavailable: select( STORE_ID ).getScanIsUnavailable(), - status: select( STORE_ID ).getStatus(), - statusIsFetching: select( STORE_ID ).getStatusIsFetching(), - } ) - ); + const { hasPlan } = usePlan(); + const { lastChecked } = useProtectData(); + const { data: status } = useScanStatusQuery( { usePolling: true } ); let currentScanStatus; - if ( scanError ) { + if ( status.error ) { currentScanStatus = 'error'; } else if ( ! lastChecked ) { currentScanStatus = 'in_progress'; @@ -179,31 +166,21 @@ const ScanPage = () => { pageViewEventName: 'protect_admin', pageViewEventProperties: { check_status: currentScanStatus, - has_plan: hasRequiredPlan, + has_plan: hasPlan, }, } ); - useStatusPolling(); - useCredentials(); - - // retry fetching status if it is not available - useEffect( () => { - if ( ! statusIsFetching && SCAN_STATUS_UNAVAILABLE === status.status && ! scanIsUnavailable ) { - refreshStatus( true ); - } - }, [ statusIsFetching, status.status, refreshStatus, scanIsUnavailable ] ); - const renderSection = useMemo( () => { - if ( scanInProgress ) { + if ( isScanInProgress( status ) ) { return ; } - if ( scanError ) { - return ; + if ( status.error ) { + return ; } return ; - }, [ scanInProgress, status.currentProgress, scanError ] ); + }, [ status ] ); return ( diff --git a/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx b/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx index 9e113d104679c..0e85aa56d9289 100644 --- a/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx +++ b/projects/plugins/protect/src/js/routes/scan/onboarding-steps.jsx @@ -1,11 +1,10 @@ import { Button, Text, getRedirectUrl } from '@automattic/jetpack-components'; -import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; -import { createInterpolateElement } from '@wordpress/element'; +import { createInterpolateElement, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { JETPACK_SCAN_SLUG } from '../../constants'; import useAnalyticsTracks from '../../hooks/use-analytics-tracks'; +import usePlan from '../../hooks/use-plan'; -const { adminUrl, siteSuffix } = window.jetpackProtectInitialState; +const { siteSuffix } = window.jetpackProtectInitialState; const scanResultsTitle = __( 'Your scan results', 'jetpack-protect' ); const scanResultsDescription = ( @@ -17,12 +16,12 @@ const scanResultsDescription = ( ); const UpgradeButton = props => { - const { run } = useProductCheckoutWorkflow( { - productSlug: JETPACK_SCAN_SLUG, - redirectUrl: adminUrl, - } ); - const { recordEventHandler } = useAnalyticsTracks(); - const getScan = recordEventHandler( 'jetpack_protect_onboarding_get_scan_link_click', run ); + const { upgradePlan } = usePlan(); + const { recordEvent } = useAnalyticsTracks(); + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_onboarding_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); return