From cfe4e3beefe7b724748d2a3eda308573b325f318 Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Tue, 20 Aug 2024 13:24:34 -0600 Subject: [PATCH 01/50] Protect: React Query changelog changelog --- pnpm-lock.yaml | 26 +- .../changelog/add-protect-tanstack-query | 5 + projects/plugins/protect/package.json | 3 +- .../protect/src/class-jetpack-protect.php | 6 +- .../protect/src/class-rest-controller.php | 15 +- projects/plugins/protect/src/js/api.js | 68 ++- .../src/js/components/admin-page/index.jsx | 50 +- .../admin-page/use-registration-watcher.js | 21 - .../js/components/credentials-gate/index.jsx | 18 +- .../credentials-needed-modal/index.jsx | 19 +- .../src/js/components/error-section/index.tsx | 2 +- .../js/components/firewall-footer/index.jsx | 75 +-- .../js/components/firewall-header/index.jsx | 38 +- .../stories/broken/index.stories.jsx | 10 +- .../fix-all-threats-modal/index.jsx | 18 +- .../js/components/fix-threat-modal/index.jsx | 18 +- .../components/ignore-threat-modal/index.jsx | 15 +- .../js/components/interstitial-page/index.jsx | 8 +- .../protect/src/js/components/modal/README.md | 9 +- .../protect/src/js/components/modal/index.jsx | 13 +- .../src/js/components/notice/index.jsx | 5 +- .../js/components/paid-accordion/index.jsx | 8 +- .../js/components/paid-plan-gate/index.tsx | 6 +- .../src/js/components/pricing-table/index.jsx | 63 +- .../src/js/components/scan-button/index.jsx | 16 +- .../src/js/components/scan-footer/index.jsx | 30 +- .../src/js/components/summary/index.jsx | 5 +- .../src/js/components/threats-list/empty.jsx | 6 +- .../js/components/threats-list/free-list.jsx | 21 +- .../src/js/components/threats-list/index.jsx | 15 +- .../js/components/threats-list/navigation.jsx | 5 +- .../js/components/threats-list/paid-list.jsx | 10 +- .../unignore-threat-modal/index.jsx | 18 +- .../user-connection-needed-modal/index.jsx | 5 +- projects/plugins/protect/src/js/constants.js | 20 +- .../use-onboarding-progress-mutator.ts | 21 + .../use-onboarding-progress-query.ts | 16 + .../src/js/data/scan/use-fixers-mutation.ts | 48 ++ .../src/js/data/scan/use-fixers-query.ts | 43 ++ .../src/js/data/scan/use-history-query.ts | 26 + .../data/scan/use-ignore-threat-mutation.ts | 28 + .../src/js/data/scan/use-scan-status-query.ts | 60 ++ .../js/data/scan/use-start-scan-mutation.ts | 29 + .../data/scan/use-unignore-threat-mutation.ts | 31 + .../src/js/data/use-connection-mutation.ts | 42 ++ .../src/js/data/use-credentials-query.ts | 40 ++ .../protect/src/js/data/use-has-plan-query.ts | 25 + .../src/js/data/use-product-data-query.ts | 17 + .../src/js/data/use-upgrade-plan-mutation.ts | 41 ++ .../waf/use-toggle-waf-module-mutation.ts | 32 ++ .../src/js/data/waf/use-waf-mutation.ts | 64 +++ .../protect/src/js/data/waf/use-waf-query.ts | 17 + .../src/js/data/waf/use-waf-seen-mutation.ts | 21 + .../data/waf/use-waf-upgrade-seen-mutation.ts | 21 + projects/plugins/protect/src/js/global.d.ts | 14 + .../protect/src/js/hooks/use-fixers.ts | 60 ++ .../protect/src/js/hooks/use-modal.tsx | 34 ++ .../protect/src/js/hooks/use-notices.tsx | 96 ++++ .../src/js/hooks/use-onboarding/index.jsx | 42 +- .../plugins/protect/src/js/hooks/use-plan.tsx | 77 +++ .../src/js/hooks/use-protect-data/index.js | 18 +- .../src/js/hooks/use-waf-data/index.jsx | 211 ++++--- projects/plugins/protect/src/js/index.tsx | 82 +-- .../protect/src/js/routes/firewall/index.jsx | 392 +++---------- .../src/js/routes/scan/history/index.jsx | 6 +- .../protect/src/js/routes/scan/index.jsx | 63 +- .../src/js/routes/scan/onboarding-steps.jsx | 19 +- .../js/routes/scan/scan-section-header.tsx | 6 +- .../src/js/routes/scan/use-credentials.js | 16 - .../src/js/routes/scan/use-status-polling.js | 105 ---- .../plugins/protect/src/js/state/actions.js | 544 ------------------ .../plugins/protect/src/js/state/reducers.js | 228 -------- .../plugins/protect/src/js/state/resolvers.js | 23 - .../plugins/protect/src/js/state/selectors.js | 101 ---- .../protect/src/js/state/store-holder.js | 14 - .../plugins/protect/src/js/state/store.js | 24 - 76 files changed, 1503 insertions(+), 1964 deletions(-) create mode 100644 projects/plugins/protect/changelog/add-protect-tanstack-query delete mode 100644 projects/plugins/protect/src/js/components/admin-page/use-registration-watcher.js create mode 100644 projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-mutator.ts create mode 100644 projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-fixers-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-history-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-start-scan-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/use-connection-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/use-credentials-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-has-plan-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-product-data-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-upgrade-plan-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-toggle-waf-module-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-query.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-seen-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-upgrade-seen-mutation.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-fixers.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-modal.tsx create mode 100644 projects/plugins/protect/src/js/hooks/use-notices.tsx create mode 100644 projects/plugins/protect/src/js/hooks/use-plan.tsx delete mode 100644 projects/plugins/protect/src/js/routes/scan/use-credentials.js delete mode 100644 projects/plugins/protect/src/js/routes/scan/use-status-polling.js delete mode 100644 projects/plugins/protect/src/js/state/actions.js delete mode 100644 projects/plugins/protect/src/js/state/reducers.js delete mode 100644 projects/plugins/protect/src/js/state/resolvers.js delete mode 100644 projects/plugins/protect/src/js/state/selectors.js delete mode 100644 projects/plugins/protect/src/js/state/store-holder.js delete mode 100644 projects/plugins/protect/src/js/state/store.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b295772f7266a..7f7d9812c0679 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: @@ -17380,6 +17392,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 132e63bbbc6ce..9448f3fa0d891 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 300886a9bed03..b84ff6b00fe7f 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; @@ -213,6 +214,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(), @@ -222,7 +224,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(), @@ -231,8 +233,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..8c030ae22ff21 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: { 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..4a66f52529893 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,28 @@ 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 useNotices from '../../hooks/use-notices'; +import { useCheckoutContext } from '../../hooks/use-plan'; 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 { hasCheckoutStarted } = useCheckoutContext(); + 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, - } ); - - useEffect( () => { - if ( getQueryArg( window.location.search, 'checkPlan' ) ) { - startScanOptimistically(); - setTimeout( () => { - refreshPlan(); - refreshStatus( true ); - refreshScanHistory(); - }, 5000 ); - } - }, [ refreshPlan, refreshStatus, refreshScanHistory, startScanOptimistically ] ); - /* - * Show interstital page when - * - Site is not registered - * - Checkout workflow has started - */ if ( ! isRegistered || hasCheckoutStarted ) { - return ; + return ; } 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..9b6507137db5e 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?.[ 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..46583fb60f23b 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,28 @@ 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 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 { 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 +38,15 @@ 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, - ] ); + connectSiteMutation.mutate(); + }, [ connectSiteMutation, recordEvent ] ); const args = { title: __( 'Stay one step ahead of threats', 'jetpack-protect' ), @@ -120,8 +93,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { @@ -158,8 +131,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..6fc47bb1318ca 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,20 @@ 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 { + SCAN_IN_PROGRESS_STATUSES, + SCAN_STATUS_IDLE, + SCAN_STATUS_UNAVAILABLE, +} from '../../constants'; +import useScanStatusQuery 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 +75,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 +108,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 +153,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 +171,32 @@ const ScanPage = () => { pageViewEventName: 'protect_admin', pageViewEventProperties: { check_status: currentScanStatus, - has_plan: hasRequiredPlan, + has_plan: hasPlan, }, } ); - useStatusPolling(); - useCredentials(); + const isScanning = useMemo( () => { + // If there has never been a scan, and the scan status is idle or not yet available, then we must still be getting set up. + const scanIsInitializing = + ! status?.lastChecked && + [ SCAN_STATUS_IDLE, SCAN_STATUS_UNAVAILABLE ].includes( status?.status ); - // 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 scanIsInProgress = SCAN_IN_PROGRESS_STATUSES.indexOf( status?.status ) >= 0; + + return scanIsInitializing || scanIsInProgress; + }, [ status?.status, status?.lastChecked ] ); const renderSection = useMemo( () => { - if ( scanInProgress ) { + if ( isScanning ) { return ; } - if ( scanError ) { - return ; + if ( status.error ) { + return ; } return ; - }, [ scanInProgress, status.currentProgress, scanError ] ); + }, [ isScanning, status.error, status.currentProgress, status.errorMessage, status.errorCode ] ); 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 From 9758e3bbc82f60d17ebd0f69fb19e250bd90247a Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 28 Aug 2024 11:05:31 -0700 Subject: [PATCH 05/50] Fix tests --- projects/packages/protect-status/tests/php/test-scan-status.php | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/packages/protect-status/tests/php/test-scan-status.php b/projects/packages/protect-status/tests/php/test-scan-status.php index 653d50b460e33..e9753f716f58d 100644 --- a/projects/packages/protect-status/tests/php/test-scan-status.php +++ b/projects/packages/protect-status/tests/php/test-scan-status.php @@ -134,6 +134,7 @@ public function get_sample_status() { 'num_plugins_threats' => 1, 'num_themes_threats' => 0, 'status' => 'idle', + 'fixable_threats' => array( '69353714' ), 'plugins' => array( new Extension_Model( array( From ba489fe9dab716b4fb290a5da06d6439e82e9d04 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 28 Aug 2024 11:08:39 -0700 Subject: [PATCH 06/50] Fix fixThreats apiFetch call --- projects/plugins/protect/src/js/api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/protect/src/js/api.js b/projects/plugins/protect/src/js/api.js index 8c030ae22ff21..7952af7b5a5bb 100644 --- a/projects/plugins/protect/src/js/api.js +++ b/projects/plugins/protect/src/js/api.js @@ -68,7 +68,7 @@ const API = { apiFetch( { path: `jetpack-protect/v1/fix-threats`, method: 'POST', - data: { threatIds }, + data: { threat_ids: threatIds }, } ), getFixersStatus: threatIds => { From a392ec130654cc1e98288386871a0ed9b7510c5c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Wed, 28 Aug 2024 11:53:32 -0700 Subject: [PATCH 07/50] Do not camelize fixerStatus in useFixersQuery initialData --- projects/plugins/protect/src/js/data/scan/use-fixers-query.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts index 20abf85dcb31c..903755b1f4440 100644 --- a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts +++ b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts @@ -1,6 +1,5 @@ import { useConnection } from '@automattic/jetpack-connection'; import { useQuery } from '@tanstack/react-query'; -import camelize from 'camelize'; import API from '../../api'; import { QUERY_FIXERS_KEY } from '../../constants'; @@ -29,7 +28,7 @@ export default function useFixersQuery( { return useQuery( { queryKey: [ QUERY_FIXERS_KEY, ...threatIds ], queryFn: () => API.getFixersStatus( threatIds ), - initialData: camelize( window.jetpackProtectInitialState?.fixerStatus ), + initialData: window.jetpackProtectInitialState?.fixerStatus, refetchInterval( query ) { if ( ! usePolling || ! query.state.data ) { return false; From b510e1e73f3075d17d301659e7d13b00dea98ebe Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Tue, 20 Aug 2024 13:24:34 -0600 Subject: [PATCH 08/50] Protect: React Query changelog changelog --- pnpm-lock.yaml | 26 +- .../changelog/add-protect-tanstack-query | 5 + projects/plugins/protect/package.json | 3 +- .../protect/src/class-jetpack-protect.php | 6 +- .../protect/src/class-rest-controller.php | 15 +- projects/plugins/protect/src/js/api.js | 68 ++- .../src/js/components/admin-page/index.jsx | 50 +- .../admin-page/use-registration-watcher.js | 21 - .../js/components/credentials-gate/index.jsx | 18 +- .../credentials-needed-modal/index.jsx | 19 +- .../src/js/components/error-section/index.tsx | 2 +- .../js/components/firewall-footer/index.jsx | 75 +-- .../js/components/firewall-header/index.jsx | 38 +- .../stories/broken/index.stories.jsx | 10 +- .../fix-all-threats-modal/index.jsx | 18 +- .../js/components/fix-threat-modal/index.jsx | 18 +- .../components/ignore-threat-modal/index.jsx | 15 +- .../js/components/interstitial-page/index.jsx | 8 +- .../protect/src/js/components/modal/README.md | 9 +- .../protect/src/js/components/modal/index.jsx | 13 +- .../src/js/components/notice/index.jsx | 5 +- .../js/components/paid-accordion/index.jsx | 8 +- .../js/components/paid-plan-gate/index.tsx | 6 +- .../src/js/components/pricing-table/index.jsx | 63 +- .../src/js/components/scan-button/index.jsx | 16 +- .../src/js/components/scan-footer/index.jsx | 30 +- .../src/js/components/summary/index.jsx | 5 +- .../src/js/components/threats-list/empty.jsx | 6 +- .../js/components/threats-list/free-list.jsx | 21 +- .../src/js/components/threats-list/index.jsx | 15 +- .../js/components/threats-list/navigation.jsx | 5 +- .../js/components/threats-list/paid-list.jsx | 10 +- .../unignore-threat-modal/index.jsx | 18 +- .../user-connection-needed-modal/index.jsx | 5 +- projects/plugins/protect/src/js/constants.js | 20 +- .../use-onboarding-progress-mutator.ts | 21 + .../use-onboarding-progress-query.ts | 16 + .../src/js/data/scan/use-fixers-mutation.ts | 48 ++ .../src/js/data/scan/use-fixers-query.ts | 43 ++ .../src/js/data/scan/use-history-query.ts | 26 + .../data/scan/use-ignore-threat-mutation.ts | 28 + .../src/js/data/scan/use-scan-status-query.ts | 60 ++ .../js/data/scan/use-start-scan-mutation.ts | 29 + .../data/scan/use-unignore-threat-mutation.ts | 31 + .../src/js/data/use-connection-mutation.ts | 42 ++ .../src/js/data/use-credentials-query.ts | 40 ++ .../protect/src/js/data/use-has-plan-query.ts | 25 + .../src/js/data/use-product-data-query.ts | 17 + .../src/js/data/use-upgrade-plan-mutation.ts | 41 ++ .../waf/use-toggle-waf-module-mutation.ts | 32 ++ .../src/js/data/waf/use-waf-mutation.ts | 64 +++ .../protect/src/js/data/waf/use-waf-query.ts | 17 + .../src/js/data/waf/use-waf-seen-mutation.ts | 21 + .../data/waf/use-waf-upgrade-seen-mutation.ts | 21 + projects/plugins/protect/src/js/global.d.ts | 14 + .../protect/src/js/hooks/use-fixers.ts | 60 ++ .../protect/src/js/hooks/use-modal.tsx | 34 ++ .../protect/src/js/hooks/use-notices.tsx | 96 ++++ .../src/js/hooks/use-onboarding/index.jsx | 42 +- .../plugins/protect/src/js/hooks/use-plan.tsx | 77 +++ .../src/js/hooks/use-protect-data/index.js | 18 +- .../src/js/hooks/use-waf-data/index.jsx | 211 ++++--- projects/plugins/protect/src/js/index.tsx | 82 +-- .../protect/src/js/routes/firewall/index.jsx | 392 +++---------- .../src/js/routes/scan/history/index.jsx | 6 +- .../protect/src/js/routes/scan/index.jsx | 63 +- .../src/js/routes/scan/onboarding-steps.jsx | 19 +- .../js/routes/scan/scan-section-header.tsx | 6 +- .../src/js/routes/scan/use-credentials.js | 16 - .../src/js/routes/scan/use-status-polling.js | 105 ---- .../plugins/protect/src/js/state/actions.js | 544 ------------------ .../plugins/protect/src/js/state/reducers.js | 228 -------- .../plugins/protect/src/js/state/resolvers.js | 23 - .../plugins/protect/src/js/state/selectors.js | 101 ---- .../protect/src/js/state/store-holder.js | 14 - .../plugins/protect/src/js/state/store.js | 24 - 76 files changed, 1503 insertions(+), 1964 deletions(-) create mode 100644 projects/plugins/protect/changelog/add-protect-tanstack-query delete mode 100644 projects/plugins/protect/src/js/components/admin-page/use-registration-watcher.js create mode 100644 projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-mutator.ts create mode 100644 projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-fixers-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-history-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-start-scan-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/use-connection-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/use-credentials-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-has-plan-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-product-data-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-upgrade-plan-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-toggle-waf-module-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-query.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-seen-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-upgrade-seen-mutation.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-fixers.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-modal.tsx create mode 100644 projects/plugins/protect/src/js/hooks/use-notices.tsx create mode 100644 projects/plugins/protect/src/js/hooks/use-plan.tsx delete mode 100644 projects/plugins/protect/src/js/routes/scan/use-credentials.js delete mode 100644 projects/plugins/protect/src/js/routes/scan/use-status-polling.js delete mode 100644 projects/plugins/protect/src/js/state/actions.js delete mode 100644 projects/plugins/protect/src/js/state/reducers.js delete mode 100644 projects/plugins/protect/src/js/state/resolvers.js delete mode 100644 projects/plugins/protect/src/js/state/selectors.js delete mode 100644 projects/plugins/protect/src/js/state/store-holder.js delete mode 100644 projects/plugins/protect/src/js/state/store.js 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..4a66f52529893 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,28 @@ 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 useNotices from '../../hooks/use-notices'; +import { useCheckoutContext } from '../../hooks/use-plan'; 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 { hasCheckoutStarted } = useCheckoutContext(); + 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, - } ); - - useEffect( () => { - if ( getQueryArg( window.location.search, 'checkPlan' ) ) { - startScanOptimistically(); - setTimeout( () => { - refreshPlan(); - refreshStatus( true ); - refreshScanHistory(); - }, 5000 ); - } - }, [ refreshPlan, refreshStatus, refreshScanHistory, startScanOptimistically ] ); - /* - * Show interstital page when - * - Site is not registered - * - Checkout workflow has started - */ if ( ! isRegistered || hasCheckoutStarted ) { - return ; + return ; } 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..9b6507137db5e 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?.[ 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..46583fb60f23b 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,28 @@ 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 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 { 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 +38,15 @@ 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, - ] ); + connectSiteMutation.mutate(); + }, [ connectSiteMutation, recordEvent ] ); const args = { title: __( 'Stay one step ahead of threats', 'jetpack-protect' ), @@ -120,8 +93,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { @@ -158,8 +131,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..6fc47bb1318ca 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,20 @@ 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 { + SCAN_IN_PROGRESS_STATUSES, + SCAN_STATUS_IDLE, + SCAN_STATUS_UNAVAILABLE, +} from '../../constants'; +import useScanStatusQuery 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 +75,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 +108,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 +153,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 +171,32 @@ const ScanPage = () => { pageViewEventName: 'protect_admin', pageViewEventProperties: { check_status: currentScanStatus, - has_plan: hasRequiredPlan, + has_plan: hasPlan, }, } ); - useStatusPolling(); - useCredentials(); + const isScanning = useMemo( () => { + // If there has never been a scan, and the scan status is idle or not yet available, then we must still be getting set up. + const scanIsInitializing = + ! status?.lastChecked && + [ SCAN_STATUS_IDLE, SCAN_STATUS_UNAVAILABLE ].includes( status?.status ); - // 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 scanIsInProgress = SCAN_IN_PROGRESS_STATUSES.indexOf( status?.status ) >= 0; + + return scanIsInitializing || scanIsInProgress; + }, [ status?.status, status?.lastChecked ] ); const renderSection = useMemo( () => { - if ( scanInProgress ) { + if ( isScanning ) { return ; } - if ( scanError ) { - return ; + if ( status.error ) { + return ; } return ; - }, [ scanInProgress, status.currentProgress, scanError ] ); + }, [ isScanning, status.error, status.currentProgress, status.errorMessage, status.errorCode ] ); 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 - 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..9b6507137db5e 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?.[ 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..46583fb60f23b 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,28 @@ 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 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 { 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 +38,15 @@ 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, - ] ); + connectSiteMutation.mutate(); + }, [ connectSiteMutation, recordEvent ] ); const args = { title: __( 'Stay one step ahead of threats', 'jetpack-protect' ), @@ -120,8 +93,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { @@ -158,8 +131,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..6fc47bb1318ca 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,20 @@ 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 { + SCAN_IN_PROGRESS_STATUSES, + SCAN_STATUS_IDLE, + SCAN_STATUS_UNAVAILABLE, +} from '../../constants'; +import useScanStatusQuery 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 +75,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 +108,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 +153,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 +171,32 @@ const ScanPage = () => { pageViewEventName: 'protect_admin', pageViewEventProperties: { check_status: currentScanStatus, - has_plan: hasRequiredPlan, + has_plan: hasPlan, }, } ); - useStatusPolling(); - useCredentials(); + const isScanning = useMemo( () => { + // If there has never been a scan, and the scan status is idle or not yet available, then we must still be getting set up. + const scanIsInitializing = + ! status?.lastChecked && + [ SCAN_STATUS_IDLE, SCAN_STATUS_UNAVAILABLE ].includes( status?.status ); - // 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 scanIsInProgress = SCAN_IN_PROGRESS_STATUSES.indexOf( status?.status ) >= 0; + + return scanIsInitializing || scanIsInProgress; + }, [ status?.status, status?.lastChecked ] ); const renderSection = useMemo( () => { - if ( scanInProgress ) { + if ( isScanning ) { return ; } - if ( scanError ) { - return ; + if ( status.error ) { + return ; } return ; - }, [ scanInProgress, status.currentProgress, scanError ] ); + }, [ isScanning, status.error, status.currentProgress, status.errorMessage, status.errorCode ] ); 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 - 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..9b6507137db5e 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?.[ 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..46583fb60f23b 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,28 @@ 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 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 { 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 +38,15 @@ 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, - ] ); + connectSiteMutation.mutate(); + }, [ connectSiteMutation, recordEvent ] ); const args = { title: __( 'Stay one step ahead of threats', 'jetpack-protect' ), @@ -120,8 +93,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { @@ -158,8 +131,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 - 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..9b6507137db5e 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?.[ 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..46583fb60f23b 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,28 @@ 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 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 { 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 +38,15 @@ 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, - ] ); + connectSiteMutation.mutate(); + }, [ connectSiteMutation, recordEvent ] ); const args = { title: __( 'Stay one step ahead of threats', 'jetpack-protect' ), @@ -120,8 +93,8 @@ const ConnectedPricingTable = ( { onScanAdd } ) => { @@ -158,8 +131,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 - 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..9b6507137db5e 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?.[ 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 - 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 - 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 - 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 - 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..269105063fc62 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 { fixInProgressThreatIds } = useFixers(); + const handleClick = useCallback( () => { if ( ! open ) { onOpen?.(); @@ -122,7 +122,7 @@ export const PaidAccordionItem = ( {
{ fixable && ( <> - { threatsAreFixing.indexOf( id ) >= 0 ? ( + { fixInProgressThreatIds.includes( id ) ? ( ) : ( 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 diff --git a/projects/plugins/protect/src/js/hooks/use-fixers.ts b/projects/plugins/protect/src/js/hooks/use-fixers.ts index 09de6c852d4e2..62b4f856f939b 100644 --- a/projects/plugins/protect/src/js/hooks/use-fixers.ts +++ b/projects/plugins/protect/src/js/hooks/use-fixers.ts @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query'; -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { QUERY_HISTORY_KEY, QUERY_SCAN_STATUS_KEY } from '../constants'; import useFixersMutation from '../data/scan/use-fixers-mutation'; import useFixersQuery from '../data/scan/use-fixers-query'; @@ -19,7 +19,10 @@ export default function useFixers() { usePolling: true, } ); - const fixThreats = async ( threatIds: number[] ) => fixersMutation.mutateAsync( threatIds ); + const fixThreats = useCallback( + async ( threatIds: number[] ) => fixersMutation.mutateAsync( threatIds ), + [ fixersMutation ] + ); // List of threat IDs that are currently being fixed. const fixInProgressThreatIds = useMemo( @@ -32,7 +35,7 @@ export default function useFixers() { useEffect( () => { if ( - Object.values( fixersStatus?.threats ).some( + Object.values( fixersStatus?.threats || {} ).some( ( threat: { status: string } ) => threat.status !== 'in_progress' ) ) { From 17a49275114e6e5c4482596b887b51bf12fdcf0c Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 30 Aug 2024 10:50:06 -0700 Subject: [PATCH 29/50] Fix fixInProgressThreatIds logic --- projects/plugins/protect/src/class-jetpack-protect.php | 2 +- projects/plugins/protect/src/js/hooks/use-fixers.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 4b431c0a86645..d5168c03abfbf 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -220,7 +220,7 @@ public function initial_state() { 'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ), 'credentials' => Credentials::get_credential_array(), 'status' => $status, - 'fixerStatus' => Threats::fix_threats_status( $status->fixable_threats ), + 'fixerStatus' => Threats::fix_threats_status( $status->fixable_threat_ids ), 'scanHistory' => Scan_History::get_scan_history( $refresh_status_from_wpcom ), 'installedPlugins' => Plugins_Installer::get_plugins(), 'installedThemes' => Sync_Functions::get_themes(), diff --git a/projects/plugins/protect/src/js/hooks/use-fixers.ts b/projects/plugins/protect/src/js/hooks/use-fixers.ts index 62b4f856f939b..065cfdc915d2a 100644 --- a/projects/plugins/protect/src/js/hooks/use-fixers.ts +++ b/projects/plugins/protect/src/js/hooks/use-fixers.ts @@ -27,9 +27,11 @@ export default function useFixers() { // List of threat IDs that are currently being fixed. const fixInProgressThreatIds = useMemo( () => - Object.values( fixersStatus?.threats || {} ) - .filter( ( threat: { status?: string } ) => threat.status === 'in_progress' ) - .map( ( threat: { id?: string } ) => parseInt( threat.id ) ), + Object.entries( fixersStatus?.threats || {} ) + .filter( + ( [ , threat ]: [ string, { status?: string } ] ) => threat.status === 'in_progress' + ) + .map( ( [ id ] ) => parseInt( id ) ), [ fixersStatus ] ); From 0aa61cd24763ab814460f1b5730d752a71ae4665 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 30 Aug 2024 11:41:44 -0700 Subject: [PATCH 30/50] Fix fixable_threat_ids type --- projects/packages/protect-models/src/class-status-model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/protect-models/src/class-status-model.php b/projects/packages/protect-models/src/class-status-model.php index e4a47ee5045ae..73bec9dd0f4de 100644 --- a/projects/packages/protect-models/src/class-status-model.php +++ b/projects/packages/protect-models/src/class-status-model.php @@ -56,7 +56,7 @@ class Status_Model { /** * List of fixable threat IDs. * - * @var number[] + * @var string[] */ public $fixable_threat_ids = array(); From 774d5a552699435915c7408d1764ebc378cf3731 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 30 Aug 2024 11:55:01 -0700 Subject: [PATCH 31/50] Update property name --- projects/plugins/protect/src/js/hooks/use-fixers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/protect/src/js/hooks/use-fixers.ts b/projects/plugins/protect/src/js/hooks/use-fixers.ts index 065cfdc915d2a..7686fd4337928 100644 --- a/projects/plugins/protect/src/js/hooks/use-fixers.ts +++ b/projects/plugins/protect/src/js/hooks/use-fixers.ts @@ -15,7 +15,7 @@ export default function useFixers() { const { data: status } = useScanStatusQuery(); const fixersMutation = useFixersMutation(); const { data: fixersStatus } = useFixersQuery( { - threatIds: status.fixableThreats, + threatIds: status.fixableThreatIds, usePolling: true, } ); @@ -47,7 +47,7 @@ export default function useFixers() { }, [ fixersStatus, queryClient ] ); return { - fixableThreats: status.fixableThreats, + fixableThreatIds: status.fixableThreatIds, fixersStatus, fixThreats, fixInProgressThreatIds, From 09cb682dedc31b66d0f6855ea75d1ba698652483 Mon Sep 17 00:00:00 2001 From: dkmyta Date: Fri, 30 Aug 2024 12:08:12 -0700 Subject: [PATCH 32/50] Fix useFixersQuery cachedData check logic --- .../src/js/data/scan/use-fixers-query.ts | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts index afcb2fd8e89fb..9e1e4626ae410 100644 --- a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts +++ b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts @@ -38,29 +38,34 @@ export default function useFixersQuery( { | undefined; // Check if any fixers have completed, by comparing the latest data against the cache. - data?.threats.forEach( ( threat: { id: number; status: string } ) => { - // Find the specific threat in the cached data. - const cachedThreat = Object.values( cachedData?.threats ).find( - ( t: { id: number } ) => t.id === threat.id - ); + data?.threats && + Object.entries( data.threats ).forEach( ( [ , threat ] ) => { + const typedThreat = threat as { id: number; status: string }; // Ensure threat has the correct type - if ( - cachedThreat && - cachedThreat.status === 'in_progress' && - threat.status !== 'in_progress' - ) { - // Invalidate related queries. - queryClient.invalidateQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); - queryClient.invalidateQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + // Find the specific threat in the cached data. + const cachedThreat = + cachedData?.threats && + Object.values( cachedData.threats ).find( + ( t: { id: number } ) => t.id === typedThreat.id + ); - // Show a relevant notice. - if ( threat.status === 'fixed' ) { - showSuccessNotice( __( 'Threat fixed successfully.', 'jetpack-protect' ) ); - } else if ( threat.status === 'not_fixed' ) { - showErrorNotice( __( 'Threat could not be fixed.', 'jetpack-protect' ) ); + if ( + cachedThreat && + cachedThreat.status === 'in_progress' && + typedThreat.status !== 'in_progress' + ) { + // Invalidate related queries. + queryClient.invalidateQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + + // Show a relevant notice. + if ( typedThreat.status === 'fixed' ) { + showSuccessNotice( __( 'Threat fixed successfully.', 'jetpack-protect' ) ); + } else if ( typedThreat.status === 'not_fixed' ) { + showErrorNotice( __( 'Threat could not be fixed.', 'jetpack-protect' ) ); + } } - } - } ); + } ); return data; }, From 0f9c56ba49db461e1f8a509a5c89d35bab7032d8 Mon Sep 17 00:00:00 2001 From: Nate Weller Date: Tue, 20 Aug 2024 13:24:34 -0600 Subject: [PATCH 33/50] Protect: React Query --- pnpm-lock.yaml | 26 +- .../changelog/add-protect-tanstack-query | 5 + projects/plugins/protect/package.json | 3 +- .../protect/src/class-jetpack-protect.php | 6 +- .../protect/src/class-rest-controller.php | 33 +- projects/plugins/protect/src/js/api.js | 68 ++- .../src/js/components/admin-page/index.jsx | 55 +- .../admin-page/use-registration-watcher.js | 21 - .../js/components/credentials-gate/index.jsx | 18 +- .../credentials-needed-modal/index.jsx | 19 +- .../src/js/components/error-section/index.tsx | 2 +- .../js/components/firewall-footer/index.jsx | 75 +-- .../js/components/firewall-header/index.jsx | 38 +- .../stories/broken/index.stories.jsx | 10 +- .../fix-all-threats-modal/index.jsx | 26 +- .../js/components/fix-threat-modal/index.jsx | 18 +- .../components/ignore-threat-modal/index.jsx | 17 +- .../protect/src/js/components/modal/README.md | 9 +- .../protect/src/js/components/modal/index.jsx | 13 +- .../src/js/components/notice/index.jsx | 5 +- .../js/components/paid-accordion/index.jsx | 8 +- .../js/components/paid-plan-gate/index.tsx | 6 +- .../src/js/components/pricing-table/index.jsx | 66 +-- .../src/js/components/scan-button/index.jsx | 16 +- .../src/js/components/scan-footer/index.jsx | 30 +- .../components/seventy-five-layout/index.jsx | 5 +- .../src/js/components/summary/index.jsx | 5 +- .../src/js/components/threats-list/empty.jsx | 6 +- .../js/components/threats-list/free-list.jsx | 21 +- .../src/js/components/threats-list/index.jsx | 15 +- .../js/components/threats-list/navigation.jsx | 5 +- .../js/components/threats-list/paid-list.jsx | 10 +- .../unignore-threat-modal/index.jsx | 16 +- .../user-connection-needed-modal/index.jsx | 5 +- projects/plugins/protect/src/js/constants.js | 20 +- .../use-onboarding-progress-mutator.ts | 21 + .../use-onboarding-progress-query.ts | 16 + .../src/js/data/scan/use-fixers-mutation.ts | 35 ++ .../src/js/data/scan/use-fixers-query.ts | 88 +++ .../src/js/data/scan/use-history-query.ts | 26 + .../data/scan/use-ignore-threat-mutation.ts | 28 + .../src/js/data/scan/use-scan-status-query.ts | 64 +++ .../js/data/scan/use-start-scan-mutation.ts | 29 + .../data/scan/use-unignore-threat-mutation.ts | 31 + .../src/js/data/use-connection-mutation.ts | 44 ++ .../src/js/data/use-credentials-query.ts | 40 ++ .../protect/src/js/data/use-has-plan-query.ts | 25 + .../src/js/data/use-product-data-query.ts | 17 + .../src/js/data/use-upgrade-plan-mutation.ts | 34 ++ .../waf/use-toggle-waf-module-mutation.ts | 32 ++ .../src/js/data/waf/use-waf-mutation.ts | 64 +++ .../protect/src/js/data/waf/use-waf-query.ts | 17 + .../src/js/data/waf/use-waf-seen-mutation.ts | 21 + .../data/waf/use-waf-upgrade-seen-mutation.ts | 21 + projects/plugins/protect/src/js/global.d.ts | 14 + .../protect/src/js/hooks/use-fixers.ts | 67 +++ .../protect/src/js/hooks/use-modal.tsx | 34 ++ .../protect/src/js/hooks/use-notices.tsx | 101 ++++ .../src/js/hooks/use-onboarding/index.jsx | 42 +- .../plugins/protect/src/js/hooks/use-plan.tsx | 77 +++ .../src/js/hooks/use-protect-data/index.js | 18 +- .../src/js/hooks/use-waf-data/index.jsx | 211 ++++--- projects/plugins/protect/src/js/index.tsx | 84 +-- .../protect/src/js/routes/firewall/index.jsx | 392 +++---------- .../src/js/routes/scan/history/index.jsx | 6 +- .../protect/src/js/routes/scan/index.jsx | 56 +- .../src/js/routes/scan/onboarding-steps.jsx | 19 +- .../js/routes/scan/scan-section-header.tsx | 6 +- .../src/js/routes/scan/use-credentials.js | 16 - .../src/js/routes/scan/use-status-polling.js | 105 ---- .../setup}/index.jsx | 18 +- .../setup}/stories/broken/index.stories.jsx | 0 .../setup}/stories/broken/mock.js | 0 .../setup}/styles.module.scss | 0 .../plugins/protect/src/js/state/actions.js | 544 ------------------ .../plugins/protect/src/js/state/reducers.js | 228 -------- .../plugins/protect/src/js/state/resolvers.js | 23 - .../plugins/protect/src/js/state/selectors.js | 101 ---- .../protect/src/js/state/store-holder.js | 14 - .../plugins/protect/src/js/state/store.js | 24 - 80 files changed, 1563 insertions(+), 1991 deletions(-) create mode 100644 projects/plugins/protect/changelog/add-protect-tanstack-query delete mode 100644 projects/plugins/protect/src/js/components/admin-page/use-registration-watcher.js create mode 100644 projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-mutator.ts create mode 100644 projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-fixers-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-history-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-start-scan-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/use-connection-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/use-credentials-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-has-plan-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-product-data-query.ts create mode 100644 projects/plugins/protect/src/js/data/use-upgrade-plan-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-toggle-waf-module-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-query.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-seen-mutation.ts create mode 100644 projects/plugins/protect/src/js/data/waf/use-waf-upgrade-seen-mutation.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-fixers.ts create mode 100644 projects/plugins/protect/src/js/hooks/use-modal.tsx create mode 100644 projects/plugins/protect/src/js/hooks/use-notices.tsx create mode 100644 projects/plugins/protect/src/js/hooks/use-plan.tsx delete mode 100644 projects/plugins/protect/src/js/routes/scan/use-credentials.js delete mode 100644 projects/plugins/protect/src/js/routes/scan/use-status-polling.js rename projects/plugins/protect/src/js/{components/interstitial-page => routes/setup}/index.jsx (70%) rename projects/plugins/protect/src/js/{components/interstitial-page => routes/setup}/stories/broken/index.stories.jsx (100%) rename projects/plugins/protect/src/js/{components/interstitial-page => routes/setup}/stories/broken/mock.js (100%) rename projects/plugins/protect/src/js/{components/interstitial-page => routes/setup}/styles.module.scss (100%) delete mode 100644 projects/plugins/protect/src/js/state/actions.js delete mode 100644 projects/plugins/protect/src/js/state/reducers.js delete mode 100644 projects/plugins/protect/src/js/state/resolvers.js delete mode 100644 projects/plugins/protect/src/js/state/selectors.js delete mode 100644 projects/plugins/protect/src/js/state/store-holder.js delete mode 100644 projects/plugins/protect/src/js/state/store.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d070d92c2bda..8183830da7677 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4210,15 +4210,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 @@ -7193,6 +7196,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: @@ -17390,6 +17402,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..0aa752ddfd6d1 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; @@ -239,7 +241,7 @@ public static function api_ignore_threat( $request ) { $threat_ignored = Threats::ignore_threat( $request['threat_id'] ); if ( ! $threat_ignored ) { - return new WP_REST_Response( 'An error occured while attempting to ignore the threat.', 500 ); + return new WP_REST_Response( 'An error occurred while attempting to ignore the threat.', 500 ); } return new WP_REST_Response( 'Threat ignored.' ); @@ -260,7 +262,7 @@ public static function api_unignore_threat( $request ) { $threat_ignored = Threats::unignore_threat( $request['threat_id'] ); if ( ! $threat_ignored ) { - return new WP_REST_Response( 'An error occured while attempting to unignore the threat.', 500 ); + return new WP_REST_Response( 'An error occurred while attempting to unignore the threat.', 500 ); } return new WP_REST_Response( 'Threat unignored.' ); @@ -281,7 +283,7 @@ public static function api_fix_threats( $request ) { $threats_fixed = Threats::fix_threats( $request['threat_ids'] ); if ( ! $threats_fixed ) { - return new WP_REST_Response( 'An error occured while attempting to fix the threat.', 500 ); + return new WP_REST_Response( 'An error occurred while attempting to fix the threat.', 500 ); } return new WP_REST_Response( $threats_fixed ); @@ -302,7 +304,7 @@ public static function api_fix_threats_status( $request ) { $threats_fixed = Threats::fix_threats_status( $request['threat_ids'] ); if ( ! $threats_fixed ) { - return new WP_REST_Response( 'An error occured while attempting to get the fixer status of the threats.', 500 ); + return new WP_REST_Response( 'An error occurred while attempting to get the fixer status of the threats.', 500 ); } return new WP_REST_Response( $threats_fixed ); @@ -317,7 +319,7 @@ public static function api_check_credentials() { $credential_array = Credentials::get_credential_array(); if ( ! isset( $credential_array ) ) { - return new WP_REST_Response( 'An error occured while attempting to fetch the credentials array', 500 ); + return new WP_REST_Response( 'An error occurred while attempting to fetch the credentials array', 500 ); } return new WP_REST_Response( $credential_array ); @@ -332,7 +334,7 @@ public static function api_scan() { $scan_enqueued = Threats::scan(); if ( ! $scan_enqueued ) { - return new WP_REST_Response( 'An error occured while attempting to enqueue the scan.', 500 ); + return new WP_REST_Response( 'An error occurred while attempting to enqueue the scan.', 500 ); } return new WP_REST_Response( 'Scan enqueued.' ); @@ -349,7 +351,7 @@ public static function api_toggle_waf() { if ( ! $disabled ) { return new WP_Error( 'waf_disable_failed', - __( 'An error occured disabling the firewall.', 'jetpack-protect' ), + __( 'An error occurred disabling the firewall.', 'jetpack-protect' ), array( 'status' => 500 ) ); } @@ -361,7 +363,7 @@ public static function api_toggle_waf() { if ( ! $enabled ) { return new WP_Error( 'waf_enable_failed', - __( 'An error occured enabling the firewall.', 'jetpack-protect' ), + __( 'An error occurred enabling the firewall.', 'jetpack-protect' ), array( 'status' => 500 ) ); } @@ -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(), ) ); } @@ -449,7 +456,7 @@ public static function api_complete_onboarding_steps( $request ) { $completed = Onboarding::complete_steps( $request['step_ids'] ); if ( ! $completed ) { - return new WP_REST_Response( 'An error occured completing the onboarding step(s).', 500 ); + return new WP_REST_Response( 'An error occurred completing the onboarding step(s).', 500 ); } return new WP_REST_Response( 'Onboarding step(s) completed.' ); 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..071b443888902 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 = () => { @@ -21,11 +21,10 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { }; const handleIgnoreClick = () => { - return async event => { + return event => { event.preventDefault(); - ignoreThreat( id, () => { - setModal( { type: null } ); - } ); + ignoreThreatMutation.mutate( 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..269105063fc62 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 { fixInProgressThreatIds } = useFixers(); + const handleClick = useCallback( () => { if ( ! open ) { onOpen?.(); @@ -122,7 +122,7 @@ export const PaidAccordionItem = ( {
{ fixable && ( <> - { threatsAreFixing.indexOf( id ) >= 0 ? ( + { fixInProgressThreatIds.includes( id ) ? ( ) : ( 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/seventy-five-layout/index.jsx b/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx index c5a6402f04b6a..3848d09864ec3 100644 --- a/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx +++ b/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx @@ -13,9 +13,10 @@ import React from 'react'; * @param {React.ReactNode} props.main - Main section component * @param {React.ReactNode} props.secondary - Secondary section component * @param {boolean} props.preserveSecondaryOnMobile - Whether to show secondary section on mobile + * @param {boolean} props.fluid - Whether to use fluid layout * @return {React.ReactNode} - React meta-component */ -const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false } ) => { +const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false, fluid } ) => { const [ isSmall, isLarge ] = useBreakpointMatch( [ 'sm', 'lg' ] ); /* @@ -26,7 +27,7 @@ const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false const hideSecondarySection = ! preserveSecondaryOnMobile && isSmall; return ( - + { ! hideSecondarySection && ( <> 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..ff93ac3b42353 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 ) @@ -91,7 +88,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". @@ -129,6 +124,7 @@ const ScanningSection = ( { currentProgress } ) => { } preserveSecondaryOnMobile={ false } + fluid={ true } />
@@ -153,20 +149,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 +167,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 ) { - return ; + 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 - 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..071b443888902 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 = () => { @@ -21,11 +21,10 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { }; const handleIgnoreClick = () => { - return async event => { + return event => { event.preventDefault(); - ignoreThreat( id, () => { - setModal( { type: null } ); - } ); + ignoreThreatMutation.mutate( 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 a8a1dae62e20c..fffabbaa138aa 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/seventy-five-layout/index.jsx b/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx index c5a6402f04b6a..3848d09864ec3 100644 --- a/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx +++ b/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx @@ -13,9 +13,10 @@ import React from 'react'; * @param {React.ReactNode} props.main - Main section component * @param {React.ReactNode} props.secondary - Secondary section component * @param {boolean} props.preserveSecondaryOnMobile - Whether to show secondary section on mobile + * @param {boolean} props.fluid - Whether to use fluid layout * @return {React.ReactNode} - React meta-component */ -const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false } ) => { +const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false, fluid } ) => { const [ isSmall, isLarge ] = useBreakpointMatch( [ 'sm', 'lg' ] ); /* @@ -26,7 +27,7 @@ const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false const hideSecondarySection = ! preserveSecondaryOnMobile && isSmall; return ( - + { ! hideSecondarySection && ( <> 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..ff93ac3b42353 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 ) @@ -91,7 +88,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". @@ -129,6 +124,7 @@ const ScanningSection = ( { currentProgress } ) => { } preserveSecondaryOnMobile={ false } + fluid={ true } />
@@ -153,20 +149,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 +167,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 ) { - return ; + 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 - 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..67d5f9894d9a2 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,18 +1,20 @@ import { Button, getRedirectUrl, Text } from '@automattic/jetpack-components'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { createInterpolateElement } from '@wordpress/element'; +import { createInterpolateElement, useState } 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 [ isIgnoring, setIsIgnoring ] = useState( false ); + const handleCancelClick = () => { return event => { event.preventDefault(); @@ -23,9 +25,10 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { const handleIgnoreClick = () => { return async event => { event.preventDefault(); - ignoreThreat( id, () => { - setModal( { type: null } ); - } ); + setIsIgnoring( true ); + await ignoreThreatMutation.mutateAsync( id ); + setModal( { type: null } ); + setIsIgnoring( false ); }; }; @@ -64,11 +67,7 @@ const IgnoreThreatModal = ( { id, title, label, icon, severity } ) => { - diff --git a/projects/plugins/protect/src/js/components/modal/README.md b/projects/plugins/protect/src/js/components/modal/README.md index 0d808d8c7879d..554ef9def9c28 100644 --- a/projects/plugins/protect/src/js/components/modal/README.md +++ b/projects/plugins/protect/src/js/components/modal/README.md @@ -1,6 +1,6 @@ # Modal -The `` is a connected component that renders a pop-up modal based on `modal` redux state. +The `` is a connected component that renders a pop-up modal based on `modal` context state. ## Usage @@ -18,14 +18,11 @@ const MyComponent = () => ( ## Opening a modal -Trigger modals by dispatching a `setModal()` action with the modal type to open: +Trigger modals by using the `setModal()` function: ```jsx -import { useDispatch } from '@wordpress/data'; -import { STORE_ID } from './state/store'; - const MyComponent = () => { - const { setModal } = useDispatch( STORE_ID ); + const { setModal } = useModal(); const handleShowModalClick = () => { return event => { diff --git a/projects/plugins/protect/src/js/components/modal/index.jsx b/projects/plugins/protect/src/js/components/modal/index.jsx index 1f629fc955989..2d8728964fb03 100644 --- a/projects/plugins/protect/src/js/components/modal/index.jsx +++ b/projects/plugins/protect/src/js/components/modal/index.jsx @@ -1,7 +1,6 @@ -import { useDispatch, useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { close as closeIcon, Icon } from '@wordpress/icons'; -import { STORE_ID } from '../../state/store'; +import useModal from '../../hooks/use-modal'; import CredentialsNeededModal from '../credentials-needed-modal'; import FixAllThreatsModal from '../fix-all-threats-modal'; import FixThreatModal from '../fix-threat-modal'; @@ -20,11 +19,9 @@ const MODAL_COMPONENTS = { }; const Modal = () => { - const modalType = useSelect( select => select( STORE_ID ).getModalType() ); - const modalProps = useSelect( select => select( STORE_ID ).getModalProps() ); - const { setModal } = useDispatch( STORE_ID ); + const { modal, setModal } = useModal(); - if ( ! modalType ) { + if ( ! modal.type ) { return null; } @@ -35,7 +32,7 @@ const Modal = () => { }; }; - const ModalComponent = MODAL_COMPONENTS[ modalType ]; + const ModalComponent = MODAL_COMPONENTS[ modal.type ]; return (
@@ -52,7 +49,7 @@ const Modal = () => { aria-label={ __( 'Close Modal Window', 'jetpack-protect' ) } /> - +
); 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 a8a1dae62e20c..fffabbaa138aa 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/seventy-five-layout/index.jsx b/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx index c5a6402f04b6a..3848d09864ec3 100644 --- a/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx +++ b/projects/plugins/protect/src/js/components/seventy-five-layout/index.jsx @@ -13,9 +13,10 @@ import React from 'react'; * @param {React.ReactNode} props.main - Main section component * @param {React.ReactNode} props.secondary - Secondary section component * @param {boolean} props.preserveSecondaryOnMobile - Whether to show secondary section on mobile + * @param {boolean} props.fluid - Whether to use fluid layout * @return {React.ReactNode} - React meta-component */ -const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false } ) => { +const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false, fluid } ) => { const [ isSmall, isLarge ] = useBreakpointMatch( [ 'sm', 'lg' ] ); /* @@ -26,7 +27,7 @@ const SeventyFiveLayout = ( { main, secondary, preserveSecondaryOnMobile = false const hideSecondarySection = ! preserveSecondaryOnMobile && isSmall; return ( - + { ! hideSecondarySection && ( <> 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/components/user-connection-needed-modal/index.jsx b/projects/plugins/protect/src/js/components/user-connection-needed-modal/index.jsx index 4fd0d97554db3..47d551248eea5 100644 --- a/projects/plugins/protect/src/js/components/user-connection-needed-modal/index.jsx +++ b/projects/plugins/protect/src/js/components/user-connection-needed-modal/index.jsx @@ -1,13 +1,12 @@ import { Button, Text } from '@automattic/jetpack-components'; import { useConnection } from '@automattic/jetpack-connection'; -import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { STORE_ID } from '../../state/store'; +import useModal from '../../hooks/use-modal'; import Notice from '../notice'; import styles from './styles.module.scss'; const UserConnectionNeededModal = () => { - const { setModal } = useDispatch( STORE_ID ); + const { setModal } = useModal(); const { userIsConnecting, handleConnectUser } = useConnection( { redirectUri: 'admin.php?page=jetpack-protect', } ); diff --git a/projects/plugins/protect/src/js/constants.js b/projects/plugins/protect/src/js/constants.js index a82cc01aa3a4e..5ec94bdccafc9 100644 --- a/projects/plugins/protect/src/js/constants.js +++ b/projects/plugins/protect/src/js/constants.js @@ -1,9 +1,11 @@ -export const FREE_PLUGIN_SUPPORT_URL = 'https://wordpress.org/support/plugin/jetpack-protect/'; +export const JETPACK_SCAN_SLUG = 'jetpack_scan'; +/** + * URLs + */ +export const FREE_PLUGIN_SUPPORT_URL = 'https://wordpress.org/support/plugin/jetpack-protect/'; export const PAID_PLUGIN_SUPPORT_URL = 'https://jetpack.com/contact-support/?rel=support'; -export const JETPACK_SCAN_SLUG = 'jetpack_scan'; - /** * Scan Status Constants */ @@ -17,3 +19,15 @@ export const SCAN_IN_PROGRESS_STATUSES = [ SCAN_STATUS_SCANNING, SCAN_STATUS_OPTIMISTICALLY_SCANNING, ]; + +/** + * Query names + */ +export const QUERY_CREDENTIALS_KEY = 'credentials'; +export const QUERY_FIXERS_KEY = 'fixers'; +export const QUERY_HAS_PLAN_KEY = 'has plan'; +export const QUERY_HISTORY_KEY = 'history'; +export const QUERY_ONBOARDING_PROGRESS_KEY = 'onboarding progress'; +export const QUERY_PRODUCT_DATA_KEY = 'product data'; +export const QUERY_SCAN_STATUS_KEY = 'scan status'; +export const QUERY_WAF_KEY = 'waf'; diff --git a/projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-mutator.ts b/projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-mutator.ts new file mode 100644 index 0000000000000..6c43fd53cc19b --- /dev/null +++ b/projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-mutator.ts @@ -0,0 +1,22 @@ +import { useMutation, type UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import API from '../../api'; +import { QUERY_ONBOARDING_PROGRESS_KEY } from '../../constants'; + +/** + * Onboarding Progress Mutation Hook + * + * @return {UseMutationResult} - useMutation result. + */ +export default function useOnboardingProgressMutation(): UseMutationResult { + const queryClient = useQueryClient(); + return useMutation( { + mutationFn: API.completeOnboardingSteps, + onMutate: ( stepIds: string[] ) => { + // Optimistically update the query data. + queryClient.setQueryData( + [ QUERY_ONBOARDING_PROGRESS_KEY ], + ( currentProgress: string[] ) => [ ...currentProgress, ...stepIds ] + ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-query.ts b/projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-query.ts new file mode 100644 index 0000000000000..e8c08bda8e451 --- /dev/null +++ b/projects/plugins/protect/src/js/data/onboarding/use-onboarding-progress-query.ts @@ -0,0 +1,16 @@ +import { useQuery, type UseQueryResult } from '@tanstack/react-query'; +import API from '../../api'; +import { QUERY_ONBOARDING_PROGRESS_KEY } from '../../constants'; + +/** + * Use Onboarding Progress Query + * + * @return {UseQueryResult} - useQuery result. + */ +export default function useOnboardingProgressQuery(): UseQueryResult { + return useQuery( { + queryKey: [ QUERY_ONBOARDING_PROGRESS_KEY ], + queryFn: API.getOnboardingProgress, + initialData: window?.jetpackProtectInitialState?.onboardingProgress || [], + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts new file mode 100644 index 0000000000000..c50c3611c8592 --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-fixers-mutation.ts @@ -0,0 +1,35 @@ +import { useMutation, type UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_FIXERS_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; + +/** + * Fixers Mutatation Hook + * + * @return {UseMutationResult} Mutation result. + */ +export default function useFixersMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showSuccessNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: API.fixThreats, + onSuccess: data => { + // The data returned from the API is the same as the data we need to update the cache. + queryClient.setQueryData( [ QUERY_FIXERS_KEY ], data ); + + // Show a success notice. + showSuccessNotice( + __( + "We're hard at work fixing this threat in the background. Please check back shortly.", + 'jetpack-protect' + ) + ); + }, + onError: () => { + // Show an error notice. + showErrorNotice( __( 'An error occurred fixing threats.', 'jetpack-protect' ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts new file mode 100644 index 0000000000000..791555f1b16ca --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts @@ -0,0 +1,88 @@ +import { useConnection } from '@automattic/jetpack-connection'; +import { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_FIXERS_KEY, QUERY_HISTORY_KEY, QUERY_SCAN_STATUS_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; +import { FixersStatus } from '../../types/fixers'; + +/** + * Fixers Query Hook + * + * @param {object} args - Hook arguments. + * @param {number[]} args.threatIds - The threat IDs to monitor for fixer status. + * @param {boolean} args.usePolling - Whether to continuously poll for fixer status while fixers are in progress. + * + * @return {UseQueryResult} The query hook result. + */ +export default function useFixersQuery( { + threatIds, + usePolling, +}: { + threatIds: number[]; + usePolling?: boolean; +} ): UseQueryResult< FixersStatus > { + const queryClient = useQueryClient(); + const { showSuccessNotice, showErrorNotice } = useNotices(); + const { isRegistered } = useConnection( { + autoTrigger: false, + from: 'protect', + redirectUri: null, + skipUserConnection: true, + } ); + + return useQuery( { + queryKey: [ QUERY_FIXERS_KEY ], + queryFn: async () => { + const data = await API.getFixersStatus( threatIds ); + const cachedData = queryClient.getQueryData( [ QUERY_FIXERS_KEY ] ) as + | { threats: object } + | undefined; + + // Check if any fixers have completed, by comparing the latest data against the cache. + Object.keys( data?.threats ).forEach( ( threatId: string ) => { + // Find the specific threat in the cached data. + const threat = data?.threats[ threatId ]; + const cachedThreat = cachedData?.threats?.[ threatId ]; + + if ( + cachedThreat && + cachedThreat.status === 'in_progress' && + threat.status !== 'in_progress' + ) { + // Invalidate related queries when a fixer has completed. + queryClient.invalidateQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + + // Show a relevant notice. + if ( threat.status === 'fixed' ) { + showSuccessNotice( __( 'Threat fixed successfully.', 'jetpack-protect' ) ); + } else { + showErrorNotice( __( 'Threat could not be fixed.', 'jetpack-protect' ) ); + } + } + } ); + + return data; + }, + refetchInterval( query ) { + if ( ! usePolling || ! query.state.data ) { + return false; + } + + // Refetch while any threats are still in progress. + if ( + Object.values( query.state.data?.threats ).some( + ( threat: { status: string } ) => threat.status === 'in_progress' + ) + ) { + // Refetch on a shorter interval first, then slow down if it is taking a while. + return query.state.dataUpdateCount < 5 ? 5_000 : 15_000; + } + + return false; + }, + initialData: { threats: {} }, // to do: provide initial data in window.jetpackProtectInitialState + enabled: isRegistered, + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-history-query.ts b/projects/plugins/protect/src/js/data/scan/use-history-query.ts new file mode 100644 index 0000000000000..c5145e5c30768 --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-history-query.ts @@ -0,0 +1,26 @@ +import { useConnection } from '@automattic/jetpack-connection'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import camelize from 'camelize'; +import API from '../../api'; +import { QUERY_HISTORY_KEY } from '../../constants'; + +/** + * Use History Query + * + * @return {UseQueryResult} useQuery result. + */ +export default function useHistoryQuery(): UseQueryResult { + const { isRegistered } = useConnection( { + autoTrigger: false, + from: 'protect', + redirectUri: null, + skipUserConnection: true, + } ); + + return useQuery( { + queryKey: [ QUERY_HISTORY_KEY ], + queryFn: API.getScanHistory, + initialData: camelize( window.jetpackProtectInitialState?.scanHistory ), + enabled: isRegistered, + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts new file mode 100644 index 0000000000000..f15ac1086f442 --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-ignore-threat-mutation.ts @@ -0,0 +1,36 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_HISTORY_KEY, QUERY_SCAN_STATUS_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; + +/** + * Ignore Threat Mutatation + * + * @return {UseMutationResult} useMutation result. + */ +export default function useIgnoreThreatMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showSuccessNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: async ( threatId: number ) => { + const response = await API.ignoreThreat( threatId ); + + // Refetch the scan status and history queries as a part of the mutation function. + // This keeps the mutator in a loading state until the side effects of the mutation are handled. + await Promise.all( [ + queryClient.refetchQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ), + queryClient.refetchQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ), + ] ); + + return response; + }, + onSuccess: () => { + showSuccessNotice( __( 'Threat ignored.', 'jetpack-protect' ) ); + }, + onError: () => { + showErrorNotice( __( 'An error occurred ignoring the threat.', 'jetpack-protect' ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts b/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts new file mode 100644 index 0000000000000..e01d5fcfb1213 --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-scan-status-query.ts @@ -0,0 +1,63 @@ +import { useConnection } from '@automattic/jetpack-connection'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import camelize from 'camelize'; +import API from '../../api'; +import { + SCAN_IN_PROGRESS_STATUSES, + SCAN_STATUS_IDLE, + SCAN_STATUS_UNAVAILABLE, +} from '../../constants'; +import { ScanStatus } from '../../types/scans'; +import { QUERY_SCAN_STATUS_KEY } from './../../constants'; + +export const isScanInProgress = ( status: ScanStatus ) => { + // If there has never been a scan, and the scan status is idle or unavailable, then we must still be getting set up. + const scanIsInitializing = + ! status?.lastChecked && + [ SCAN_STATUS_IDLE, SCAN_STATUS_UNAVAILABLE ].includes( status?.status ); + + const scanIsInProgress = SCAN_IN_PROGRESS_STATUSES.indexOf( status?.status ) >= 0; + + return scanIsInitializing || scanIsInProgress; +}; + +/** + * Use Scan Status Query + * + * @param {object} args - Hook arguments. + * @param {boolean} args.usePolling - When enabled, the query will poll for updates when the scan is in progress. + * + * @return {UseQueryResult} useQuery result. + */ +export default function useScanStatusQuery( { + usePolling, +}: { usePolling?: boolean } = {} ): UseQueryResult< ScanStatus > { + const { isRegistered } = useConnection( { + autoTrigger: false, + from: 'protect', + redirectUri: null, + skipUserConnection: true, + } ); + + return useQuery( { + queryKey: [ QUERY_SCAN_STATUS_KEY ], + queryFn: API.getScanStatus, + initialData: camelize( window?.jetpackProtectInitialState?.status ), + enabled: isRegistered, + refetchInterval( query ) { + if ( ! usePolling ) { + return false; + } + + // Refetch on a shorter interval for the first few updates. + const interval = query.state.dataUpdateCount < 5 ? 5_000 : 15_000; + + // Refetch when scanning. + if ( isScanInProgress( query.state.data ) ) { + return interval; + } + + return false; + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-start-scan-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-start-scan-mutation.ts new file mode 100644 index 0000000000000..8f65b0410f83d --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-start-scan-mutation.ts @@ -0,0 +1,33 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import API from '../../api'; +import { QUERY_SCAN_STATUS_KEY, SCAN_STATUS_OPTIMISTICALLY_SCANNING } from './../../constants'; + +/** + * Use Start Scan Mutation + * + * @return {UseMutationResult} Mutation result. + */ +export default function useStartScanMutation(): UseMutationResult { + const queryClient = useQueryClient(); + return useMutation( { + mutationFn: API.scan, + onMutate() { + // Optimistically update the scan status to 'scanning'. + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], ( currentStatus: object ) => ( { + ...currentStatus, + status: SCAN_STATUS_OPTIMISTICALLY_SCANNING, + } ) ); + }, + onSuccess() { + // The scan has been enqueued successfully, ensure the scan status is still 'scanning'. + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], ( currentStatus: object ) => ( { + ...currentStatus, + status: SCAN_STATUS_OPTIMISTICALLY_SCANNING, + } ) ); + }, + onError() { + // The scan failed to enqueue, invalidate the scan status query to reset the current status. + queryClient.invalidateQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts b/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts new file mode 100644 index 0000000000000..fedae11299c8b --- /dev/null +++ b/projects/plugins/protect/src/js/data/scan/use-unignore-threat-mutation.ts @@ -0,0 +1,36 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_HISTORY_KEY, QUERY_SCAN_STATUS_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; + +/** + * Use Un-Ignore Threat Mutatation + * + * @return {UseMutationResult} Mutation result. + */ +export default function useUnIgnoreThreatMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showSuccessNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: async ( threatId: number ) => { + const response = await API.unIgnoreThreat( threatId ); + + // Refetch the scan status and history queries as a part of the mutation function. + // This keeps the mutator in a loading state until the side effects of the mutation are handled. + await Promise.all( [ + queryClient.refetchQueries( { queryKey: [ QUERY_SCAN_STATUS_KEY ] } ), + queryClient.refetchQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ), + ] ); + + return response; + }, + onSuccess: () => { + showSuccessNotice( __( 'Threat is no longer ignored.', 'jetpack-protect' ) ); + }, + onError: () => { + showErrorNotice( __( 'An error occurred un-ignoring the threat.', 'jetpack-protect' ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/use-connection-mutation.ts b/projects/plugins/protect/src/js/data/use-connection-mutation.ts new file mode 100644 index 0000000000000..90d8481f65edd --- /dev/null +++ b/projects/plugins/protect/src/js/data/use-connection-mutation.ts @@ -0,0 +1,52 @@ +import { useConnection } from '@automattic/jetpack-connection'; +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import { + QUERY_CREDENTIALS_KEY, + QUERY_HAS_PLAN_KEY, + QUERY_HISTORY_KEY, + QUERY_SCAN_STATUS_KEY, + QUERY_WAF_KEY, + SCAN_STATUS_OPTIMISTICALLY_SCANNING, +} from '../constants'; +import useNotices from '../hooks/use-notices'; +import { ScanStatus } from '../types/scans'; + +/** + * Connect Site Mutation + * + * Mutation hook that triggers the Jetpack connection process for a site. + * + * @return {UseMutationResult} useMutation result. + */ +export default function useConnectSiteMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showErrorNotice } = useNotices(); + + const { handleRegisterSite } = useConnection( { + autoTrigger: false, + from: 'protect', + redirectUri: null, + skipUserConnection: true, + } ); + + return useMutation( { + mutationFn: handleRegisterSite, + onSuccess: async () => { + // Optimistically update the scan status to 'scanning'. + queryClient.setQueryData( [ QUERY_SCAN_STATUS_KEY ], ( scanStatus: ScanStatus ) => ( { + ...scanStatus, + status: SCAN_STATUS_OPTIMISTICALLY_SCANNING, + } ) ); + + // Invalidate all queries that depend on the connection status. + queryClient.invalidateQueries( { queryKey: [ QUERY_HISTORY_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_WAF_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_HAS_PLAN_KEY ] } ); + queryClient.invalidateQueries( { queryKey: [ QUERY_CREDENTIALS_KEY ] } ); + }, + onError: () => { + showErrorNotice( __( 'Could not connect site.', 'jetpack-protect' ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/use-credentials-query.ts b/projects/plugins/protect/src/js/data/use-credentials-query.ts new file mode 100644 index 0000000000000..431f5c9045751 --- /dev/null +++ b/projects/plugins/protect/src/js/data/use-credentials-query.ts @@ -0,0 +1,43 @@ +import { useConnection } from '@automattic/jetpack-connection'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import API from '../api'; +import { QUERY_CREDENTIALS_KEY } from '../constants'; + +/** + * Credentials Query Hook + * + * @param {object} args - Args. + * @param {boolean} args.usePolling - Use polling. + * + * @return {UseQueryResult} useQuery result. + */ +export default function useCredentialsQuery( { + usePolling, +}: { usePolling?: boolean } = {} ): UseQueryResult< [ Record< string, unknown > ] > { + const { isRegistered } = useConnection( { + autoTrigger: false, + from: 'protect', + redirectUri: null, + skipUserConnection: true, + } ); + + return useQuery( { + queryKey: [ QUERY_CREDENTIALS_KEY ], + queryFn: API.checkCredentials, + initialData: window?.jetpackProtectInitialState?.credentials, + refetchInterval: query => { + if ( ! usePolling ) { + return false; + } + if ( ! query.state.data ) { + return false; + } + if ( query.state.data?.length ) { + return false; + } + + return 5_000; + }, + enabled: isRegistered, + } ); +} diff --git a/projects/plugins/protect/src/js/data/use-has-plan-query.ts b/projects/plugins/protect/src/js/data/use-has-plan-query.ts new file mode 100644 index 0000000000000..2e35ead294dbf --- /dev/null +++ b/projects/plugins/protect/src/js/data/use-has-plan-query.ts @@ -0,0 +1,25 @@ +import { useConnection } from '@automattic/jetpack-connection'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import API from '../api'; +import { QUERY_HAS_PLAN_KEY } from '../constants'; + +/** + * Plan Query Hook + * + * @return {UseQueryResult} useQuery result. + */ +export default function usePlanQuery(): UseQueryResult { + const { isRegistered, isUserConnected } = useConnection( { + autoTrigger: false, + from: 'protect', + redirectUri: null, + skipUserConnection: true, + } ); + + return useQuery( { + queryKey: [ QUERY_HAS_PLAN_KEY ], + queryFn: API.checkPlan, + initialData: !! window?.jetpackProtectInitialState?.hasPlan, + enabled: isRegistered && isUserConnected, + } ); +} diff --git a/projects/plugins/protect/src/js/data/use-product-data-query.ts b/projects/plugins/protect/src/js/data/use-product-data-query.ts new file mode 100644 index 0000000000000..902733ae9d00a --- /dev/null +++ b/projects/plugins/protect/src/js/data/use-product-data-query.ts @@ -0,0 +1,17 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import camelize from 'camelize'; +import API from '../api'; +import { QUERY_PRODUCT_DATA_KEY } from '../constants'; + +/** + * Product Data Query Hook + * + * @return {UseQueryResult} useQuery result. + */ +export default function useProductDataQuery(): UseQueryResult { + return useQuery( { + queryKey: [ QUERY_PRODUCT_DATA_KEY ], + queryFn: API.getProductData, + initialData: camelize( window?.jetpackProtectInitialState?.jetpackScan ), + } ); +} diff --git a/projects/plugins/protect/src/js/data/waf/use-toggle-waf-module-mutation.ts b/projects/plugins/protect/src/js/data/waf/use-toggle-waf-module-mutation.ts new file mode 100644 index 0000000000000..2313f67b5cc6f --- /dev/null +++ b/projects/plugins/protect/src/js/data/waf/use-toggle-waf-module-mutation.ts @@ -0,0 +1,28 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import API from '../../api'; +import { QUERY_WAF_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; + +/** + * Toggle WAF Mutatation + * + * @return {UseMutationResult} useMutation result. + */ +export default function useToggleWafMutation(): UseMutationResult { + const queryClient = useQueryClient(); + const { showSuccessNotice, showErrorNotice } = useNotices(); + + return useMutation( { + mutationFn: API.toggleWaf, + onSuccess: () => { + showSuccessNotice( __( 'WAF module enabled.', 'jetpack-protect' ) ); + }, + onError: () => { + showErrorNotice( __( 'An error occurred enabling the WAF module.', 'jetpack-protect' ) ); + }, + onSettled: () => { + queryClient.invalidateQueries( { queryKey: [ QUERY_WAF_KEY ] } ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/waf/use-waf-mutation.ts b/projects/plugins/protect/src/js/data/waf/use-waf-mutation.ts new file mode 100644 index 0000000000000..a89d0abb9dd5f --- /dev/null +++ b/projects/plugins/protect/src/js/data/waf/use-waf-mutation.ts @@ -0,0 +1,73 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import camelize from 'camelize'; +import API from '../../api'; +import { QUERY_WAF_KEY } from '../../constants'; +import useNotices from '../../hooks/use-notices'; +import { WafStatus } from '../../types/waf'; + +/** + * WAF Mutatation Hook + * + * @return {UseMutationResult} useMutation result. + */ +export default function useWafMutation(): UseMutationResult< + unknown, + { [ key: string ]: unknown }, + unknown, + { initialValue: WafStatus } +> { + const queryClient = useQueryClient(); + const { showSuccessNotice, showSavingNotice, showErrorNotice } = useNotices(); + + /** + * Get a custom error message based on the error code. + * + * @param {object} error - Error object. + * @return string|bool Custom error message or false if no custom message exists. + */ + const getCustomErrorMessage = useCallback( ( error: { [ key: string ]: unknown } ) => { + switch ( error.code ) { + case 'file_system_error': + return __( 'A filesystem error occurred.', 'jetpack-protect' ); + case 'rules_api_error': + return __( + 'An error occurred retrieving the latest firewall rules from Jetpack.', + 'jetpack-protect' + ); + default: + return __( 'An error occurred.', 'jetpack-protect' ); + } + }, [] ); + + return useMutation( { + mutationFn: API.updateWaf, + onMutate: config => { + showSavingNotice(); + + // Get the current WAF config. + const initialValue = queryClient.getQueryData( [ QUERY_WAF_KEY ] ) as WafStatus; + + // Optimistically update the WAF config. + queryClient.setQueryData( [ QUERY_WAF_KEY ], ( wafStatus: WafStatus ) => ( { + ...wafStatus, + config: { + ...wafStatus.config, + ...camelize( config ), + }, + } ) ); + + return { initialValue }; + }, + onSuccess: () => { + showSuccessNotice( __( 'Changes saved.', 'jetpack-protect' ) ); + }, + onError: ( error, variables, context ) => { + // Reset the WAF config to its previous state. + queryClient.setQueryData( [ QUERY_WAF_KEY ], context.initialValue ); + + showErrorNotice( getCustomErrorMessage( error ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/waf/use-waf-query.ts b/projects/plugins/protect/src/js/data/waf/use-waf-query.ts new file mode 100644 index 0000000000000..f8d9cce28bca1 --- /dev/null +++ b/projects/plugins/protect/src/js/data/waf/use-waf-query.ts @@ -0,0 +1,18 @@ +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import camelize from 'camelize'; +import API from '../../api'; +import { QUERY_WAF_KEY } from '../../constants'; +import { WafStatus } from '../../types/waf'; + +/** + * WAF Query Hook + * + * @return {UseQueryResult} useQuery result. + */ +export default function useWafQuery(): UseQueryResult< WafStatus > { + return useQuery( { + queryKey: [ QUERY_WAF_KEY ], + queryFn: API.getWaf, + initialData: camelize( window?.jetpackProtectInitialState?.waf ), + } ); +} diff --git a/projects/plugins/protect/src/js/data/waf/use-waf-seen-mutation.ts b/projects/plugins/protect/src/js/data/waf/use-waf-seen-mutation.ts new file mode 100644 index 0000000000000..0485a01f525aa --- /dev/null +++ b/projects/plugins/protect/src/js/data/waf/use-waf-seen-mutation.ts @@ -0,0 +1,21 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import API from '../../api'; +import { QUERY_WAF_KEY } from '../../constants'; + +/** + * WAF Seen Mutation Hook + * + * @return {UseMutationResult} - Mutation result. + */ +export default function useWafSeenMutation(): UseMutationResult { + const queryClient = useQueryClient(); + return useMutation( { + mutationFn: API.wafSeen, + onMutate: () => { + queryClient.setQueryData( [ QUERY_WAF_KEY ], ( currentWaf: object ) => ( { + ...currentWaf, + isSeen: true, + } ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/data/waf/use-waf-upgrade-seen-mutation.ts b/projects/plugins/protect/src/js/data/waf/use-waf-upgrade-seen-mutation.ts new file mode 100644 index 0000000000000..794581756ec99 --- /dev/null +++ b/projects/plugins/protect/src/js/data/waf/use-waf-upgrade-seen-mutation.ts @@ -0,0 +1,21 @@ +import { useMutation, UseMutationResult, useQueryClient } from '@tanstack/react-query'; +import API from '../../api'; +import { QUERY_WAF_KEY } from '../../constants'; + +/** + * WAF Upgrade Seen Mutation + * + * @return {UseMutationResult} - Mutation result. + */ +export default function useWafUpgradeSeenMutation(): UseMutationResult { + const queryClient = useQueryClient(); + return useMutation( { + mutationFn: API.wafUpgradeSeen, + onMutate: () => { + queryClient.setQueryData( [ QUERY_WAF_KEY ], ( currentWaf: object ) => ( { + ...currentWaf, + upgradeIsSeen: true, + } ) ); + }, + } ); +} diff --git a/projects/plugins/protect/src/js/global.d.ts b/projects/plugins/protect/src/js/global.d.ts deleted file mode 100644 index d5cf927a7cd3e..0000000000000 --- a/projects/plugins/protect/src/js/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module '*.scss'; diff --git a/projects/plugins/protect/src/js/hooks/use-fixers.ts b/projects/plugins/protect/src/js/hooks/use-fixers.ts new file mode 100644 index 0000000000000..6c7c8062e98f1 --- /dev/null +++ b/projects/plugins/protect/src/js/hooks/use-fixers.ts @@ -0,0 +1,47 @@ +import { useMemo } from 'react'; +import useFixersMutation from '../data/scan/use-fixers-mutation'; +import useFixersQuery from '../data/scan/use-fixers-query'; +import useScanStatusQuery from '../data/scan/use-scan-status-query'; +import { FixersStatus } from '../types/fixers'; +import { Threat } from '../types/threats'; + +type UseFixersResult = { + fixableThreats: Threat[]; + fixersStatus: FixersStatus; + fixThreats: ( threatIds: number[] ) => Promise< unknown >; + isLoading: boolean; +}; + +/** + * Use Fixers Hook + * + * @return {UseFixersResult} Fixers object + */ +export default function useFixers(): UseFixersResult { + const { data: status } = useScanStatusQuery(); + const fixersMutation = useFixersMutation(); + + const fixableThreats = useMemo( () => { + const threats = [ + ...( status?.core?.threats || [] ), + ...( status?.plugins?.map( plugin => plugin.threats ).flat() || [] ), + ...( status?.themes?.map( theme => theme.threats ).flat() || [] ), + ...( status?.files || [] ), + ...( status?.database || [] ), + ]; + + return threats.filter( threat => threat.fixable ); + }, [ status ] ); + + const { data: fixersStatus } = useFixersQuery( { + threatIds: fixableThreats.map( threat => threat.id ), + usePolling: true, + } ); + + return { + fixableThreats, + fixersStatus, + fixThreats: fixersMutation.mutateAsync, + isLoading: fixersMutation.isPending, + }; +} diff --git a/projects/plugins/protect/src/js/hooks/use-modal.tsx b/projects/plugins/protect/src/js/hooks/use-modal.tsx new file mode 100644 index 0000000000000..5718348cc3d01 --- /dev/null +++ b/projects/plugins/protect/src/js/hooks/use-modal.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { createContext, useContext, useState } from 'react'; + +interface ModalState { + type?: string; + props?: Record< string, unknown >; +} + +interface ModalContextValue { + modal: ModalState | null; + setModal: React.Dispatch< React.SetStateAction< ModalState | null > > | null; +} + +const ModalContext = createContext< ModalContextValue >( { modal: null, setModal: null } ); + +export const ModalProvider: React.FC< { children: React.ReactNode } > = ( { children } ) => { + const [ modal, setModal ] = useState< ModalState | null >( {} ); + + return { children }; +}; + +/** + * Modal Hook + * + * @return {object} Modals object + */ +export default function useModal() { + const { modal, setModal } = useContext( ModalContext ); + + return { + modal, + setModal, + }; +} diff --git a/projects/plugins/protect/src/js/hooks/use-notices.tsx b/projects/plugins/protect/src/js/hooks/use-notices.tsx new file mode 100644 index 0000000000000..bbc1f888aa3df --- /dev/null +++ b/projects/plugins/protect/src/js/hooks/use-notices.tsx @@ -0,0 +1,101 @@ +import { ExternalLink } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { createContext, useCallback, useContext, useState } from 'react'; +import { FREE_PLUGIN_SUPPORT_URL, PAID_PLUGIN_SUPPORT_URL } from '../constants'; +import usePlan from './use-plan'; + +interface NoticeState { + message?: string | JSX.Element; + dismissable?: boolean; + duration?: number; + type?: 'success' | 'info' | 'error'; +} + +interface NoticeContextValue { + notice: NoticeState; + setNotice: React.Dispatch< React.SetStateAction< NoticeState > >; +} + +const NoticeContext = createContext< NoticeContextValue | undefined >( undefined ); + +export const NoticeProvider: React.FC< { children: React.ReactNode } > = ( { children } ) => { + const [ notice, setNotice ] = useState< NoticeState >( null ); + + return ( + { children } + ); +}; + +/** + * Notices Hook + * + * @return {object} Notices object + */ +export default function useNotices() { + const { hasPlan } = usePlan(); + const { notice, setNotice } = useContext( NoticeContext ); + + const clearNotice = useCallback( () => { + setNotice( null ); + }, [ setNotice ] ); + + const showSuccessNotice = useCallback( + ( message: string ) => { + setNotice( { + type: 'success', + dismissable: true, + duration: 7_500, + message, + } ); + }, + [ setNotice ] + ); + + const showSavingNotice = useCallback( + ( message?: string ) => { + setNotice( { + type: 'info', + dismissable: false, + message: message || __( 'Saving Changes…', 'jetpack-protect' ), + } ); + }, + [ setNotice ] + ); + + const showErrorNotice = useCallback( + ( message: string ) => { + setNotice( { + type: 'error', + dismissable: true, + message: ( + <> + { message || __( 'An error occurred.', 'jetpack-protect' ) }{ ' ' } + { createInterpolateElement( + __( + 'Please try again or contact support.', + 'jetpack-protect' + ), + { + supportLink: ( + + ), + } + ) } + + ), + } ); + }, + [ hasPlan, setNotice ] + ); + + return { + notice, + clearNotice, + showSavingNotice, + showSuccessNotice, + showErrorNotice, + }; +} diff --git a/projects/plugins/protect/src/js/hooks/use-onboarding/index.jsx b/projects/plugins/protect/src/js/hooks/use-onboarding/index.jsx index 73ca3a7144bef..ea7d2330126b1 100644 --- a/projects/plugins/protect/src/js/hooks/use-onboarding/index.jsx +++ b/projects/plugins/protect/src/js/hooks/use-onboarding/index.jsx @@ -1,7 +1,6 @@ -import { useDispatch, useSelect } from '@wordpress/data'; -import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import API from '../../api'; -import { STORE_ID } from '../../state/store'; +import { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import useOnboardingProgressMutation from '../../data/onboarding/use-onboarding-progress-mutator'; +import useOnboardingProgressQuery from '../../data/onboarding/use-onboarding-progress-query'; export const OnboardingContext = createContext( [] ); export const OnboardingRenderedContext = createContext( [] ); @@ -17,13 +16,11 @@ export const OnboardingRenderedContextProvider = ( { children } ) => { }; const useOnboarding = () => { - const { completeOnboardingSteps, fetchOnboardingProgress } = API; - const steps = useContext( OnboardingContext ); const { renderedSteps } = useContext( OnboardingRenderedContext ); - const progress = useSelect( select => select( STORE_ID ).getOnboardingProgress() ); - const { setOnboardingProgress } = useDispatch( STORE_ID ); + const { data: progress } = useOnboardingProgressQuery(); + const onboardingProgressMutation = useOnboardingProgressMutation(); /** * Current Step @@ -52,12 +49,9 @@ const useOnboarding = () => { const completeCurrentStep = useCallback( () => { if ( currentStep ) { - // Complete the step immediately in the UI - setOnboardingProgress( [ ...progress, currentStep.id ] ); - // Save the completion in the background - completeOnboardingSteps( [ currentStep.id ] ); + onboardingProgressMutation.mutate( [ currentStep.id ] ); } - }, [ currentStep, setOnboardingProgress, progress, completeOnboardingSteps ] ); + }, [ currentStep, onboardingProgressMutation ] ); /** * Complete All Free Steps @@ -70,12 +64,8 @@ const useOnboarding = () => { return carry; }, [] ); - // Complete the free steps immediately in the UI - const combinedProgress = [ ...progress, ...freeStepIds ]; - setOnboardingProgress( [ ...new Set( combinedProgress ) ] ); - // Save the completions in the background - completeOnboardingSteps( freeStepIds ); - }, [ steps, progress, setOnboardingProgress, completeOnboardingSteps ] ); + onboardingProgressMutation.mutate( freeStepIds ); + }, [ steps, onboardingProgressMutation ] ); /** * Complete All Paid Steps @@ -88,12 +78,8 @@ const useOnboarding = () => { return carry; }, [] ); - // Complete the paid steps immediately in the UI - const combinedProgress = [ ...progress, ...paidStepIds ]; - setOnboardingProgress( [ ...new Set( combinedProgress ) ] ); - // Save the completions in the background - completeOnboardingSteps( paidStepIds ); - }, [ steps, progress, setOnboardingProgress, completeOnboardingSteps ] ); + onboardingProgressMutation.mutate( paidStepIds ); + }, [ steps, onboardingProgressMutation ] ); /** * Complete All Current Steps @@ -107,12 +93,6 @@ const useOnboarding = () => { } }, [ completeAllFreeSteps, completeAllPaidSteps, currentStep ] ); - useEffect( () => { - if ( null === progress ) { - fetchOnboardingProgress().then( latestProgress => setOnboardingProgress( latestProgress ) ); - } - }, [ fetchOnboardingProgress, progress, setOnboardingProgress ] ); - return { progress, stepsCount, diff --git a/projects/plugins/protect/src/js/hooks/use-plan.tsx b/projects/plugins/protect/src/js/hooks/use-plan.tsx new file mode 100644 index 0000000000000..b5ab18da01875 --- /dev/null +++ b/projects/plugins/protect/src/js/hooks/use-plan.tsx @@ -0,0 +1,72 @@ +import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; +import { createContext, useContext, useState, useCallback } from 'react'; +import API from '../api'; +import { JETPACK_SCAN_SLUG } from '../constants'; +import usePlanQuery from '../data/use-has-plan-query'; + +type CheckoutContextType = { + hasCheckoutStarted: boolean; + setHasCheckoutStarted: ( hasCheckoutStarted: boolean ) => void; +}; + +const CheckoutContext = createContext< CheckoutContextType >( { + hasCheckoutStarted: false, + setHasCheckoutStarted: () => {}, +} ); + +export const CheckoutProvider = ( { children } ) => { + const [ hasCheckoutStarted, setHasCheckoutStarted ] = useState( false ); + + return ( + + { children } + + ); +}; + +export const useCheckoutContext = () => useContext( CheckoutContext ); + +/** + * Plan hook. + * + * Provides data and functions related to the site's current plan. + * + * @param {object} props - Hook props. + * @param {string} props.redirectUrl - Post-checkout redirect URL. + * + * @return {object} Hook data + */ +export default function usePlan( { redirectUrl }: { redirectUrl?: string } = {} ) { + const { adminUrl } = window.jetpackProtectInitialState || {}; + const { data: hasPlan, isLoading: isPlanLoading } = usePlanQuery(); + const { hasCheckoutStarted, setHasCheckoutStarted } = useCheckoutContext(); + + const { run: checkout } = useProductCheckoutWorkflow( { + productSlug: JETPACK_SCAN_SLUG, + redirectUrl: redirectUrl || adminUrl, + siteProductAvailabilityHandler: API.checkPlan, + useBlogIdSuffix: true, + connectAfterCheckout: false, + from: () => 'protect', + } ) as unknown as { + run: ( event?: Event, redirect?: string ) => void; + isRegistered: boolean; + hasCheckoutStarted: boolean; + }; + + const upgradePlan = useCallback( () => { + setHasCheckoutStarted( true ); + checkout(); + }, [ checkout, setHasCheckoutStarted ] ); + + return { + hasPlan, + upgradePlan, + isLoading: isPlanLoading || hasCheckoutStarted, + }; +} diff --git a/projects/plugins/protect/src/js/hooks/use-protect-data/index.js b/projects/plugins/protect/src/js/hooks/use-protect-data/index.js index 1ce57b341678c..91daaf6a468e6 100644 --- a/projects/plugins/protect/src/js/hooks/use-protect-data/index.js +++ b/projects/plugins/protect/src/js/hooks/use-protect-data/index.js @@ -1,7 +1,8 @@ -import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { useMemo } from 'react'; -import { STORE_ID } from '../../state/store'; +import useHistoryQuery from '../../data/scan/use-history-query'; +import useScanStatusQuery from '../../data/scan/use-scan-status-query'; +import useProductDataQuery from '../../data/use-product-data-query'; // Valid "key" values for filtering. const KEY_FILTERS = [ 'all', 'core', 'plugins', 'themes', 'files', 'database' ]; @@ -50,12 +51,9 @@ export default function useProtectData( filter: { status: null, key: null }, } ) { - const { status, scanHistory, jetpackScan, hasRequiredPlan } = useSelect( select => ( { - status: select( STORE_ID ).getStatus(), - scanHistory: select( STORE_ID ).getScanHistory(), - jetpackScan: select( STORE_ID ).getJetpackScan(), - hasRequiredPlan: select( STORE_ID ).hasRequiredPlan(), - } ) ); + const { data: status } = useScanStatusQuery(); + const { data: scanHistory } = useHistoryQuery(); + const { data: jetpackScan } = useProductDataQuery(); const { counts, results, error, lastChecked, hasUncheckedItems } = useMemo( () => { // This hook can provide data from two sources: the current scan or the scan history. @@ -166,6 +164,5 @@ export default function useProtectData( lastChecked, hasUncheckedItems, jetpackScan, - hasRequiredPlan, }; } diff --git a/projects/plugins/protect/src/js/hooks/use-waf-data/index.jsx b/projects/plugins/protect/src/js/hooks/use-waf-data/index.jsx index f0eccf83fb8e3..5912439b893d3 100644 --- a/projects/plugins/protect/src/js/hooks/use-waf-data/index.jsx +++ b/projects/plugins/protect/src/js/hooks/use-waf-data/index.jsx @@ -1,7 +1,8 @@ -import { useDispatch, useSelect } from '@wordpress/data'; -import { useCallback, useEffect } from 'react'; -import API from '../../api'; -import { STORE_ID } from '../../state/store'; +import { useCallback } from 'react'; +import useToggleWafMutation from '../../data/waf/use-toggle-waf-module-mutation'; +import useWafMutation from '../../data/waf/use-waf-mutation'; +import useWafQuery from '../../data/waf/use-waf-query'; +import useAnalyticsTracks from '../use-analytics-tracks'; /** * Use WAF Data Hook @@ -9,53 +10,29 @@ import { STORE_ID } from '../../state/store'; * @return {object} WAF data and methods for interacting with it. */ const useWafData = () => { - const { setWafConfig, setWafStats, setWafIsEnabled, setWafIsUpdating, setWafIsToggling } = - useDispatch( STORE_ID ); - const waf = useSelect( select => select( STORE_ID ).getWaf() ); - - /** - * Refresh WAF Configuration - * - * Fetches the firewall data and updates it in application state. - */ - const refreshWaf = useCallback( () => { - setWafIsUpdating( true ); - return API.fetchWaf() - .then( response => { - setWafIsEnabled( response?.isEnabled ); - setWafConfig( response?.config ); - setWafStats( response?.stats ); - } ) - .finally( () => setWafIsUpdating( false ) ); - }, [ setWafConfig, setWafStats, setWafIsEnabled, setWafIsUpdating ] ); + const { recordEvent } = useAnalyticsTracks(); + const { data: waf } = useWafQuery(); + const wafMutation = useWafMutation(); + const toggleWafMutation = useToggleWafMutation(); /** * Toggle WAF Module * * Flips the switch on the WAF module, and then refreshes the data. */ - const toggleWaf = useCallback( () => { - if ( ! waf.isEnabled ) { - setWafIsToggling( true ); - } - setWafIsUpdating( true ); - return API.toggleWaf() - .then( refreshWaf ) - .finally( () => { - setWafIsToggling( false ); - setWafIsUpdating( false ); - } ); - }, [ refreshWaf, waf.isEnabled, setWafIsToggling, setWafIsUpdating ] ); + const toggleWaf = useCallback( async () => { + toggleWafMutation.mutate(); + }, [ toggleWafMutation ] ); /** * Ensure WAF Module Is Enabled */ - const ensureModuleIsEnabled = useCallback( () => { + const ensureModuleIsEnabled = useCallback( async () => { if ( ! waf.isEnabled ) { - return toggleWaf(); + return await toggleWaf(); } - return Promise.resolve(); + return true; }, [ toggleWaf, waf.isEnabled ] ); /** @@ -63,117 +40,133 @@ const useWafData = () => { * * Flips the switch on the WAF automatic rules feature, and then refreshes the data. */ - const toggleAutomaticRules = useCallback( () => { - setWafIsUpdating( true ); - return ensureModuleIsEnabled() - .then( () => - API.updateWaf( { jetpack_waf_automatic_rules: ! waf.config.jetpackWafAutomaticRules } ) - ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, [ ensureModuleIsEnabled, refreshWaf, setWafIsUpdating, waf.config.jetpackWafAutomaticRules ] ); + const toggleAutomaticRules = useCallback( async () => { + const value = ! waf.config.jetpackWafAutomaticRules; + await ensureModuleIsEnabled(); + await wafMutation.mutateAsync( { + jetpack_waf_automatic_rules: value, + } ); + recordEvent( + value ? 'jetpack_protect_automatic_rules_enabled' : 'jetpack_protect_automatic_rules_disabled' + ); + }, [ ensureModuleIsEnabled, recordEvent, waf.config.jetpackWafAutomaticRules, wafMutation ] ); /** * Toggle IP Allow List * * Flips the switch on the WAF IP allow list feature, and then refreshes the data. */ - const toggleIpAllowList = useCallback( () => { - setWafIsUpdating( true ); - return API.updateWaf( { - jetpack_waf_ip_allow_list_enabled: ! waf.config.jetpackWafIpAllowListEnabled, - } ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, [ refreshWaf, setWafIsUpdating, waf.config.jetpackWafIpAllowListEnabled ] ); + const toggleIpAllowList = useCallback( async () => { + const value = ! waf.config.jetpackWafIpAllowListEnabled; + await wafMutation.mutateAsync( { + jetpack_waf_ip_allow_list_enabled: value, + } ); + recordEvent( + value ? 'jetpack_protect_ip_allow_list_enabled' : 'jetpack_protect_ip_allow_list_disabled' + ); + }, [ recordEvent, waf.config.jetpackWafIpAllowListEnabled, wafMutation ] ); + + /** + * Save IP Allow List + */ + const saveIpAllowList = useCallback( + async value => { + await wafMutation.mutateAsync( { + jetpack_waf_ip_allow_list: value, + } ); + recordEvent( 'jetpack_protect_ip_allow_list_updated' ); + }, + [ recordEvent, wafMutation ] + ); /** * Toggle IP Block List * * Flips the switch on the WAF IP block list feature, and then refreshes the data. */ - const toggleIpBlockList = useCallback( () => { - setWafIsUpdating( true ); - return API.updateWaf( { - jetpack_waf_ip_block_list_enabled: ! waf.config.jetpackWafIpBlockListEnabled, - } ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, [ refreshWaf, setWafIsUpdating, waf.config.jetpackWafIpBlockListEnabled ] ); + const toggleIpBlockList = useCallback( async () => { + const value = ! waf.config.jetpackWafIpBlockListEnabled; + await ensureModuleIsEnabled(); + await wafMutation.mutateAsync( { + jetpack_waf_ip_block_list_enabled: value, + } ); + recordEvent( + value ? 'jetpack_protect_ip_block_list_enabled' : 'jetpack_protect_ip_block_list_disabled' + ); + }, [ ensureModuleIsEnabled, recordEvent, waf.config.jetpackWafIpBlockListEnabled, wafMutation ] ); + + /** + * Save IP Block List + */ + const saveIpBlockList = useCallback( + async value => { + await ensureModuleIsEnabled(); + await wafMutation.mutateAsync( { + jetpack_waf_ip_block_list: value, + } ); + recordEvent( 'jetpack_protect_ip_block_list_updated' ); + }, + [ ensureModuleIsEnabled, wafMutation, recordEvent ] + ); /** * Toggle Brute Force Protection * * Flips the switch on the brute force protection feature, and then refreshes the data. */ - const toggleBruteForceProtection = useCallback( () => { - setWafIsUpdating( true ); - return API.updateWaf( { brute_force_protection: ! waf.config.bruteForceProtection } ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, [ refreshWaf, setWafIsUpdating, waf.config.bruteForceProtection ] ); + const toggleBruteForceProtection = useCallback( async () => { + const value = ! waf.config.bruteForceProtection; + await wafMutation.mutateAsync( { brute_force_protection: value } ); + recordEvent( + value + ? 'jetpack_protect_brute_force_protection_enabled' + : 'jetpack_protect_brute_force_protection_disabled' + ); + }, [ recordEvent, waf.config.bruteForceProtection, wafMutation ] ); /** * Toggle Share Data * * Flips the switch on the share data option, and then refreshes the data. */ - const toggleShareData = useCallback( () => { - setWafIsUpdating( true ); - return ensureModuleIsEnabled() - .then( () => API.updateWaf( { jetpack_waf_share_data: ! waf.config.jetpackWafShareData } ) ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, [ ensureModuleIsEnabled, refreshWaf, setWafIsUpdating, waf.config.jetpackWafShareData ] ); + const toggleShareData = useCallback( async () => { + const value = ! waf.config.jetpackWafShareData; + await wafMutation.mutateAsync( { jetpack_waf_share_data: value } ); + recordEvent( + value ? 'jetpack_protect_share_data_enabled' : 'jetpack_protect_share_data_disabled' + ); + }, [ recordEvent, waf.config.jetpackWafShareData, wafMutation ] ); /** * Toggle Share Debug Data * * Flips the switch on the share debug data option, and then refreshes the data. */ - const toggleShareDebugData = useCallback( () => { - setWafIsUpdating( true ); - return ensureModuleIsEnabled() - .then( () => - API.updateWaf( { jetpack_waf_share_debug_data: ! waf.config.jetpackWafShareDebugData } ) - ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, [ ensureModuleIsEnabled, refreshWaf, setWafIsUpdating, waf.config.jetpackWafShareDebugData ] ); - - /** - * Update WAF Config - */ - const updateConfig = useCallback( - update => { - setWafIsUpdating( true ); - return API.updateWaf( update ) - .then( refreshWaf ) - .finally( () => setWafIsUpdating( false ) ); - }, - [ refreshWaf, setWafIsUpdating ] - ); - - /** - * Ensures the WAF data is loaded each time the hook is used. - */ - useEffect( () => { - if ( waf.config === undefined && ! waf.isFetching ) { - refreshWaf(); - } - }, [ waf.config, waf.isFetching, setWafIsUpdating, refreshWaf ] ); + const toggleShareDebugData = useCallback( async () => { + const value = ! waf.config.jetpackWafShareDebugData; + await wafMutation.mutateAsync( { + jetpack_waf_share_debug_data: value, + } ); + recordEvent( + value + ? 'jetpack_protect_share_debug_data_enabled' + : 'jetpack_protect_share_debug_data_disabled' + ); + }, [ recordEvent, waf.config.jetpackWafShareDebugData, wafMutation ] ); return { ...waf, - refreshWaf, + isUpdating: wafMutation.isPending, + isToggling: toggleWafMutation.isPending, toggleWaf, toggleAutomaticRules, toggleIpAllowList, + saveIpAllowList, toggleIpBlockList, + saveIpBlockList, toggleBruteForceProtection, toggleShareData, toggleShareDebugData, - updateConfig, }; }; diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx index d486dfb530479..b8983d65bb836 100644 --- a/projects/plugins/protect/src/js/index.tsx +++ b/projects/plugins/protect/src/js/index.tsx @@ -1,18 +1,28 @@ import { ThemeProvider } from '@automattic/jetpack-components'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import * as WPElement from '@wordpress/element'; import React, { useEffect } from 'react'; import { HashRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom'; import Modal from './components/modal'; import PaidPlanGate from './components/paid-plan-gate'; +import { ModalProvider } from './hooks/use-modal'; +import { NoticeProvider } from './hooks/use-notices'; import { OnboardingRenderedContextProvider } from './hooks/use-onboarding'; +import { CheckoutProvider } from './hooks/use-plan'; import FirewallRoute from './routes/firewall'; import ScanRoute from './routes/scan'; import ScanHistoryRoute from './routes/scan/history'; -import { initStore } from './state/store'; +import SetupRoute from './routes/setup'; import './styles.module.scss'; -// Initialize Jetpack Protect store -initStore(); +const queryClient = new QueryClient( { + defaultOptions: { + queries: { + staleTime: Infinity, + }, + }, +} ); /** * Component to scroll window to top on route change. @@ -37,35 +47,45 @@ function render() { } const component = ( - - - - - - } /> - - - - } - /> - - - - } - /> - } /> - } /> - - - - - + + + + + + + + + + } /> + } /> + + + + } + /> + + + + } + /> + } /> + } /> + + + + + + + + + + ); WPElement.createRoot( container ).render( component ); } diff --git a/projects/plugins/protect/src/js/routes/firewall/index.jsx b/projects/plugins/protect/src/js/routes/firewall/index.jsx index 4af9d28a64ddb..60bf9b02a4f12 100644 --- a/projects/plugins/protect/src/js/routes/firewall/index.jsx +++ b/projects/plugins/protect/src/js/routes/firewall/index.jsx @@ -7,38 +7,30 @@ import { useBreakpointMatch, Notice as JetpackNotice, } from '@automattic/jetpack-components'; -import { useProductCheckoutWorkflow } from '@automattic/jetpack-connection'; -import { ExternalLink, Popover } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; +import { Popover } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { Icon, closeSmall } from '@wordpress/icons'; import moment from 'moment'; import { useCallback, useEffect, useState, useMemo } from 'react'; -import API from '../../api'; import AdminPage from '../../components/admin-page'; import FirewallFooter from '../../components/firewall-footer'; import ConnectedFirewallHeader from '../../components/firewall-header'; import FormToggle from '../../components/form-toggle'; import ScanFooter from '../../components/scan-footer'; import Textarea from '../../components/textarea'; -import { - JETPACK_SCAN_SLUG, - FREE_PLUGIN_SUPPORT_URL, - PAID_PLUGIN_SUPPORT_URL, -} from '../../constants'; +import { FREE_PLUGIN_SUPPORT_URL, PAID_PLUGIN_SUPPORT_URL } from '../../constants'; +import useWafSeenMutation from '../../data/waf/use-waf-seen-mutation'; +import useWafUpgradeSeenMutation from '../../data/waf/use-waf-upgrade-seen-mutation'; 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 { STORE_ID } from '../../state/store'; import styles from './styles.module.scss'; const ADMIN_URL = window?.jetpackProtectInitialState?.adminUrl; -const SUCCESS_NOTICE_DURATION = 5000; const FirewallPage = () => { const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] ); - const { setWafIsSeen, setWafUpgradeIsSeen, setNotice } = useDispatch( STORE_ID ); const { config: { jetpackWafAutomaticRules, @@ -59,19 +51,17 @@ const FirewallPage = () => { stats: { automaticRulesLastUpdated }, toggleAutomaticRules, toggleIpAllowList, + saveIpAllowList, toggleIpBlockList, + saveIpBlockList, toggleBruteForceProtection, toggleWaf, - updateConfig, } = useWafData(); - const { hasRequiredPlan } = useProtectData(); - const { run: runCheckoutWorkflow } = useProductCheckoutWorkflow( { - productSlug: JETPACK_SCAN_SLUG, - redirectUrl: `${ ADMIN_URL }#/firewall`, - useBlogIdSuffix: true, - } ); - const { recordEventHandler, recordEvent } = useAnalyticsTracks(); - + const { hasPlan } = usePlan(); + const { upgradePlan } = usePlan( { redirectUrl: `${ ADMIN_URL }#/firewall` } ); + const { recordEvent } = useAnalyticsTracks(); + const wafSeenMutation = useWafSeenMutation(); + const wafUpgradeSeenMutation = useWafUpgradeSeenMutation(); /** * Automatic Rules Installation Error State * @@ -85,137 +75,28 @@ const FirewallPage = () => { * @member {object} formState - Current form values. */ const [ formState, setFormState ] = useState( { - jetpack_waf_automatic_rules: jetpackWafAutomaticRules, - jetpack_waf_ip_block_list_enabled: jetpackWafIpBlockListEnabled, - jetpack_waf_ip_allow_list_enabled: jetpackWafIpAllowListEnabled, jetpack_waf_ip_block_list: jetpackWafIpBlockList, jetpack_waf_ip_allow_list: jetpackWafIpAllowList, - brute_force_protection: isBruteForceModuleEnabled, } ); - const [ formIsSubmitting, setFormIsSubmitting ] = useState( false ); - const [ ipAllowListIsUpdating, setIpAllowListIsUpdating ] = useState( false ); - const [ ipBlockListIsUpdating, setIpBlockListIsUpdating ] = useState( false ); - - const canEditFirewallSettings = isWafModuleEnabled && ! formIsSubmitting; - const canToggleAutomaticRules = - isWafModuleEnabled && ( hasRequiredPlan || automaticRulesAvailable ); - const canEditIpAllowList = ! formIsSubmitting && !! formState.jetpack_waf_ip_allow_list_enabled; + const canEditFirewallSettings = isWafModuleEnabled && ! isUpdating; + const canToggleAutomaticRules = isWafModuleEnabled && ( hasPlan || automaticRulesAvailable ); + const canEditIpAllowList = ! isUpdating && jetpackWafIpAllowListEnabled; const ipBlockListHasChanges = formState.jetpack_waf_ip_block_list !== jetpackWafIpBlockList; const ipAllowListHasChanges = formState.jetpack_waf_ip_allow_list !== jetpackWafIpAllowList; const ipBlockListHasContent = !! formState.jetpack_waf_ip_block_list; const ipAllowListHasContent = !! formState.jetpack_waf_ip_allow_list; - const ipBlockListEnabled = isWafModuleEnabled && formState.jetpack_waf_ip_block_list_enabled; - - /** - * Get a custom error message based on the error code. - * - * @param {object} error - Error object. - * @return string|bool Custom error message or false if no custom message exists. - */ - const getCustomErrorMessage = useCallback( error => { - switch ( error.code ) { - case 'file_system_error': - return __( 'A filesystem error occurred.', 'jetpack-protect' ); - case 'rules_api_error': - return __( - 'An error occurred retrieving the latest firewall rules from Jetpack.', - 'jetpack-protect' - ); - default: - return false; - } - }, [] ); - - /** - * Handle errors returned by the API. - */ - const handleApiError = useCallback( - error => { - const errorMessage = - getCustomErrorMessage( error ) || __( 'An error occurred.', 'jetpack-protect' ); - const supportMessage = createInterpolateElement( - __( 'Please try again or contact support.', 'jetpack-protect' ), - { - supportLink: ( - - ), - } - ); - - setNotice( { - type: 'error', - message: ( - <> - { errorMessage } { supportMessage } - - ), - } ); - }, - [ getCustomErrorMessage, setNotice, hasRequiredPlan ] - ); + const ipBlockListEnabled = isWafModuleEnabled && jetpackWafIpBlockListEnabled; /** * Get Scan * * Records an event and then starts the checkout flow for Jetpack Scan */ - const getScan = recordEventHandler( - 'jetpack_protect_waf_page_get_scan_link_click', - runCheckoutWorkflow - ); - - /** - * Save IP Allow List Changes - * - * Updates the WAF settings with the current form state values. - * - * @return void - */ - const saveIpAllowListChanges = useCallback( () => { - setFormIsSubmitting( true ); - setIpAllowListIsUpdating( true ); - updateConfig( formState ) - .then( () => - setNotice( { - type: 'success', - duration: SUCCESS_NOTICE_DURATION, - message: __( 'Allow list changes saved.', 'jetpack-protect' ), - } ) - ) - .catch( handleApiError ) - .finally( () => { - setFormIsSubmitting( false ); - setIpAllowListIsUpdating( false ); - } ); - }, [ updateConfig, formState, handleApiError, setNotice ] ); - - /** - * Save IP Block List Changes - * - * Updates the WAF settings with the current form state values. - * - * @return void - */ - const saveIpBlockListChanges = useCallback( () => { - setFormIsSubmitting( true ); - setIpBlockListIsUpdating( true ); - updateConfig( formState ) - .then( () => - setNotice( { - type: 'success', - duration: SUCCESS_NOTICE_DURATION, - message: __( 'Block list changes saved.', 'jetpack-protect' ), - } ) - ) - .catch( handleApiError ) - .finally( () => { - setFormIsSubmitting( false ); - setIpBlockListIsUpdating( false ); - } ); - }, [ updateConfig, formState, handleApiError, setNotice ] ); + const getScan = useCallback( () => { + recordEvent( 'jetpack_protect_waf_page_get_scan_link_click' ); + upgradePlan(); + }, [ recordEvent, upgradePlan ] ); /** * Handle Change @@ -234,172 +115,69 @@ const FirewallPage = () => { ); /** - * Handle Automatic Rules Change + * Returns an event listener that syncs the target input's value with form state, before calling a callback. * - * Toggles the WAF's automatic rules option. - * - * @return void + * @param {*} callback - The function to call with the input's value. + * @return {Function} - Event listener */ - const handleAutomaticRulesChange = useCallback( () => { - setFormIsSubmitting( true ); - const newValue = ! formState.jetpack_waf_automatic_rules; - setFormState( { - ...formState, - jetpack_waf_automatic_rules: newValue, - } ); - toggleAutomaticRules() - .then( () => { - setAutomaticRulesInstallationError( false ); - setNotice( { - type: 'success', - duration: SUCCESS_NOTICE_DURATION, - message: newValue - ? __( `Automatic firewall protection is enabled.`, 'jetpack-protect' ) - : __( - `Automatic firewall protection is disabled.`, - 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 - ), - } ); - recordEvent( - newValue - ? 'jetpack_protect_automatic_rules_enabled' - : 'jetpack_protect_automatic_rules_disabled' - ); - } ) - .then( () => { - if ( ! upgradeIsSeen ) { - setWafUpgradeIsSeen( true ); - API.wafUpgradeSeen(); - } - } ) - .catch( error => { - setAutomaticRulesInstallationError( true ); - handleApiError( error ); - } ) - .finally( () => setFormIsSubmitting( false ) ); - }, [ - formState, - toggleAutomaticRules, - setNotice, - recordEvent, - upgradeIsSeen, - setWafUpgradeIsSeen, - handleApiError, - ] ); + const withFormState = callback => { + return event => { + const { id, value, ariaChecked } = event.target; + const inputValue = ariaChecked ? ariaChecked !== 'true' : value; + setFormState( prevState => ( { + ...prevState, + [ id ]: inputValue, + } ) ); + return callback( inputValue ); + }; + }; /** - * Handle Brute Force Protection Change + * Handle Automatic Rules Change * - * Toggles the brute force protection module. + * Toggles the WAF's automatic rules option. * * @return void */ - const handleBruteForceProtectionChange = useCallback( () => { - setFormIsSubmitting( true ); - const newValue = ! formState.brute_force_protection; - setFormState( { - ...formState, - brute_force_protection: newValue, - } ); - toggleBruteForceProtection() - .then( () => { - setNotice( { - type: 'success', - duration: SUCCESS_NOTICE_DURATION, - message: newValue - ? __( `Brute force protection is enabled.`, 'jetpack-protect' ) - : __( - `Brute force protection is disabled.`, - 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 - ), - } ); - recordEvent( - newValue - ? 'jetpack_protect_brute_force_protection_enabled' - : 'jetpack_protect_brute_force_protection_disabled' - ); - } ) - .catch( handleApiError ) - .finally( () => setFormIsSubmitting( false ) ); - }, [ formState, toggleBruteForceProtection, handleApiError, setNotice, recordEvent ] ); + const handleAutomaticRulesChange = useCallback( () => { + setFormState( prevState => ( { + ...prevState, + jetpack_waf_automatic_rules: ! prevState.jetpack_waf_automatic_rules, + } ) ); + + try { + toggleAutomaticRules(); + setAutomaticRulesInstallationError( false ); + } catch ( error ) { + setAutomaticRulesInstallationError( true ); + setFormState( prevState => ( { + ...prevState, + jetpack_waf_automatic_rules: ! prevState.jetpack_waf_automatic_rules, + } ) ); + } + }, [ toggleAutomaticRules ] ); /** - * Handle IP Allow List Change + * Save IP Block List Changes * - * Toggles the WAF's IP allow list option. + * Updates the WAF settings with the current form state values. * * @return void */ - const handleIpAllowListChange = useCallback( () => { - const newIpAllowListStatus = ! formState.jetpack_waf_ip_allow_list_enabled; - setFormIsSubmitting( true ); - setIpAllowListIsUpdating( true ); - setFormState( { ...formState, jetpack_waf_ip_allow_list_enabled: newIpAllowListStatus } ); - toggleIpAllowList() - .then( () => { - setNotice( { - type: 'success', - duration: SUCCESS_NOTICE_DURATION, - message: newIpAllowListStatus - ? __( 'Allow list active.', 'jetpack-protect' ) - : __( - 'Allow list is disabled.', - 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 - ), - } ); - recordEvent( - newIpAllowListStatus - ? 'jetpack_protect_ip_allow_list_enabled' - : 'jetpack_protect_ip_allow_list_disabled' - ); - } ) - .catch( handleApiError ) - .finally( () => { - setFormIsSubmitting( false ); - setIpAllowListIsUpdating( false ); - } ); - }, [ formState, toggleIpAllowList, handleApiError, setNotice, recordEvent ] ); + const saveIpBlockListChanges = useCallback( async () => { + await saveIpBlockList( formState.jetpack_waf_ip_block_list ); + }, [ saveIpBlockList, formState.jetpack_waf_ip_block_list ] ); /** - * Handle IP Block List Change + * Save IP Allow List Changes * - * Toggles the WAF's IP block list option. + * Updates the WAF settings with the current form state values. * * @return void */ - const handleIpBlockListChange = useCallback( () => { - const newIpBlockListStatus = ! formState.jetpack_waf_ip_block_list_enabled; - setFormIsSubmitting( true ); - setIpBlockListIsUpdating( true ); - setFormState( { ...formState, jetpack_waf_ip_block_list_enabled: newIpBlockListStatus } ); - toggleIpBlockList() - .then( () => { - setNotice( { - type: 'success', - duration: SUCCESS_NOTICE_DURATION, - message: newIpBlockListStatus - ? __( 'Block list is active.', 'jetpack-protect' ) - : __( - 'Block list is disabled.', - 'jetpack-protect', - /* dummy arg to avoid bad minification */ 0 - ), - } ); - recordEvent( - newIpBlockListStatus - ? 'jetpack_protect_ip_block_list_enabled' - : 'jetpack_protect_ip_block_list_disabled' - ); - } ) - .catch( handleApiError ) - .finally( () => { - setFormIsSubmitting( false ); - setIpBlockListIsUpdating( false ); - } ); - }, [ formState, toggleIpBlockList, handleApiError, setNotice, recordEvent ] ); + const saveIpAllowListChanges = useCallback( async () => { + await saveIpAllowList( formState.jetpack_waf_ip_allow_list ); + }, [ saveIpAllowList, formState.jetpack_waf_ip_allow_list ] ); /** * Handle Close Popover Click @@ -409,9 +187,8 @@ const FirewallPage = () => { * @return void */ const handleClosePopoverClick = useCallback( () => { - setWafUpgradeIsSeen( true ); - API.wafUpgradeSeen(); - }, [ setWafUpgradeIsSeen ] ); + wafUpgradeSeenMutation.mutate(); + }, [ wafUpgradeSeenMutation ] ); /** * Checks if the current IP address is allow listed. @@ -419,7 +196,7 @@ const FirewallPage = () => { * @return {boolean} - Indicates whether the current IP address is allow listed. */ const isCurrentIpAllowed = useMemo( () => { - return formState.jetpack_waf_ip_allow_list.includes( currentIp ); + return formState.jetpack_waf_ip_allow_list?.includes( currentIp ); }, [ formState.jetpack_waf_ip_allow_list, currentIp ] ); /** @@ -445,23 +222,11 @@ const FirewallPage = () => { useEffect( () => { if ( ! isUpdating ) { setFormState( { - jetpack_waf_automatic_rules: jetpackWafAutomaticRules, - jetpack_waf_ip_block_list_enabled: jetpackWafIpBlockListEnabled, - jetpack_waf_ip_allow_list_enabled: jetpackWafIpAllowListEnabled, jetpack_waf_ip_block_list: jetpackWafIpBlockList, jetpack_waf_ip_allow_list: jetpackWafIpAllowList, - brute_force_protection: isBruteForceModuleEnabled, } ); } - }, [ - jetpackWafIpBlockListEnabled, - jetpackWafIpAllowListEnabled, - jetpackWafIpBlockList, - jetpackWafIpAllowList, - jetpackWafAutomaticRules, - isBruteForceModuleEnabled, - isUpdating, - ] ); + }, [ jetpackWafIpBlockList, jetpackWafIpAllowList, isUpdating ] ); /** * "WAF Seen" useEffect() @@ -471,18 +236,14 @@ const FirewallPage = () => { return; } - // remove the "new" badge immediately - setWafIsSeen( true ); - - // update the meta value in the background - API.wafSeen(); - }, [ isSeen, setWafIsSeen ] ); + wafSeenMutation.mutate(); + }, [ isSeen, wafSeenMutation ] ); // Track view for Protect WAF page. useAnalyticsTracks( { pageViewEventName: 'protect_waf', pageViewEventProperties: { - has_plan: hasRequiredPlan, + has_plan: hasPlan, }, } ); @@ -520,11 +281,11 @@ const FirewallPage = () => { >
- { hasRequiredPlan && upgradeIsSeen === false && ( + { hasPlan && upgradeIsSeen === false && (
@@ -565,7 +326,7 @@ const FirewallPage = () => { { __( 'Automatic firewall protection', 'jetpack-protect' ) } - { ! isSmall && hasRequiredPlan && displayUpgradeBadge && ( + { ! isSmall && hasPlan && displayUpgradeBadge && ( { __( 'NOW AVAILABLE', 'jetpack-protect' ) } ) }
@@ -605,12 +366,11 @@ const FirewallPage = () => { variant={ 'body-small' } mt={ 2 } > - { __( 'Failed to update automatic firewall rules.', 'jetpack-protect' ) }{ ' ' } - { getCustomErrorMessage( automaticRulesInstallationError ) } + { __( 'Failed to update automatic firewall rules.', 'jetpack-protect' ) }
- { ! hasRequiredPlan && ( + { ! hasPlan && (
{
@@ -684,7 +444,7 @@ const FirewallPage = () => {
@@ -714,7 +474,7 @@ const FirewallPage = () => {