{
const getProtectFree = useCallback( async () => {
recordEvent( 'jetpack_protect_connected_product_activated' );
await connectSiteMutation.mutateAsync();
- navigate( '/scan' );
+ navigate( '/' );
}, [ connectSiteMutation, recordEvent, navigate ] );
const args = {
diff --git a/projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx b/projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx
deleted file mode 100644
index 19ee4309e55a5..0000000000000
--- a/projects/plugins/protect/src/js/components/seventy-five-layout/index.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Container, Col, useBreakpointMatch } from '@automattic/jetpack-components';
-import React from 'react';
-
-// Define the props interface for the SeventyFiveLayout component
-interface SeventyFiveLayoutProps {
- spacing?: number;
- gap?: number;
- main: React.ReactNode;
- mainClassName?: string;
- secondary: React.ReactNode;
- secondaryClassName?: string;
- preserveSecondaryOnMobile?: boolean;
- fluid?: boolean;
-}
-
-/**
- * SeventyFive layout meta component
- * The component name references to
- * the sections disposition of the layout.
- * FiftyFifty, 75, thus 7|5 means the cols numbers
- * for main and secondary sections respectively,
- * in large lg viewport size.
- *
- * @param {object} props - Component props
- * @param {number} props.spacing - Horizontal spacing
- * @param {number} props.gap - Horizontal gap
- * @param {React.ReactNode} props.main - Main section component
- * @param {string} props.mainClassName - Main section class name
- * @param {React.ReactNode} props.secondary - Secondary section component
- * @param {string} props.secondaryClassName - Secondary section class name
- * @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: React.FC< SeventyFiveLayoutProps > = ( {
- spacing = 0,
- gap = 0,
- main,
- mainClassName,
- secondary,
- secondaryClassName,
- preserveSecondaryOnMobile = false,
- fluid,
-} ) => {
- // Ensure the correct typing for useBreakpointMatch
- const [ isSmall, isLarge ] = useBreakpointMatch( [ 'sm', 'lg' ] );
-
- /*
- * By convention, secondary section is not shown when:
- * - preserveSecondaryOnMobile is false
- * - on mobile breakpoint (sm)
- */
- const hideSecondarySection = ! preserveSecondaryOnMobile && isSmall;
-
- return (
-
- { ! hideSecondarySection && (
- <>
-
- { main }
-
- { isLarge && }
-
- { secondary }
-
- >
- ) }
- { hideSecondarySection && { main } }
-
- );
-};
-
-export default SeventyFiveLayout;
diff --git a/projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss b/projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss
deleted file mode 100644
index 5405c6e28a9b4..0000000000000
--- a/projects/plugins/protect/src/js/components/seventy-five-layout/styles.module.scss
+++ /dev/null
@@ -1,13 +0,0 @@
-// seventy-five layout
-// Handle large lg size from here,
-// adding a gap on one column
-// in between main and secondary sections.
-@media ( min-width: 960px ) {
- .main {
- grid-column: 1 / span 6;
- }
-
- .secondary {
- grid-column: 8 / span 5;
- }
-}
diff --git a/projects/plugins/protect/src/js/hooks/use-plan.tsx b/projects/plugins/protect/src/js/hooks/use-plan.tsx
index b5ab18da01875..f5cd1d54943b9 100644
--- a/projects/plugins/protect/src/js/hooks/use-plan.tsx
+++ b/projects/plugins/protect/src/js/hooks/use-plan.tsx
@@ -48,7 +48,7 @@ export default function usePlan( { redirectUrl }: { redirectUrl?: string } = {}
const { run: checkout } = useProductCheckoutWorkflow( {
productSlug: JETPACK_SCAN_SLUG,
- redirectUrl: redirectUrl || adminUrl,
+ redirectUrl: redirectUrl || adminUrl + '#/scan',
siteProductAvailabilityHandler: API.checkPlan,
useBlogIdSuffix: true,
connectAfterCheckout: false,
diff --git a/projects/plugins/protect/src/js/index.tsx b/projects/plugins/protect/src/js/index.tsx
index 2b91f4b090b92..4438d5021a664 100644
--- a/projects/plugins/protect/src/js/index.tsx
+++ b/projects/plugins/protect/src/js/index.tsx
@@ -11,6 +11,7 @@ import { NoticeProvider } from './hooks/use-notices';
import { OnboardingRenderedContextProvider } from './hooks/use-onboarding';
import { CheckoutProvider } from './hooks/use-plan';
import FirewallRoute from './routes/firewall';
+import HomeRoute from './routes/home';
import ScanRoute from './routes/scan';
import SetupRoute from './routes/setup';
import './styles.module.scss';
@@ -56,6 +57,7 @@ function render() {
} />
+ } />
} />
} />
- } />
+ } />
diff --git a/projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx b/projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx
new file mode 100644
index 0000000000000..12d887e933f43
--- /dev/null
+++ b/projects/plugins/protect/src/js/routes/home/home-admin-section-hero.tsx
@@ -0,0 +1,50 @@
+import { Text, Button } from '@automattic/jetpack-components';
+import { __ } from '@wordpress/i18n';
+import { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
+import AdminSectionHero from '../../components/admin-section-hero';
+import usePlan from '../../hooks/use-plan';
+import HomeStatCards from './home-statcards';
+import styles from './styles.module.scss';
+
+const HomeAdminSectionHero: React.FC = () => {
+ const { hasPlan } = usePlan();
+ const navigate = useNavigate();
+ const handleScanReportClick = useCallback( () => {
+ navigate( '/scan' );
+ }, [ navigate ] );
+
+ return (
+
+
+ <>
+
+ { __( 'Your site is safe with us', 'jetpack-protect' ) }
+
+
+ { hasPlan
+ ? __(
+ 'We stay ahead of security threats to keep your site protected.',
+ 'jetpack-protect'
+ )
+ : __(
+ 'We stay ahead of security vulnerabilities to keep your site protected.',
+ 'jetpack-protect'
+ ) }
+
+
+ >
+
+ { }
+
+ );
+};
+
+export default HomeAdminSectionHero;
diff --git a/projects/plugins/protect/src/js/routes/home/home-statcards.jsx b/projects/plugins/protect/src/js/routes/home/home-statcards.jsx
new file mode 100644
index 0000000000000..2d1dc34cac147
--- /dev/null
+++ b/projects/plugins/protect/src/js/routes/home/home-statcards.jsx
@@ -0,0 +1,274 @@
+import { Text, useBreakpointMatch, StatCard, ShieldIcon } from '@automattic/jetpack-components';
+import { Spinner, Tooltip } from '@wordpress/components';
+import { dateI18n } from '@wordpress/date';
+import { __, _n, sprintf } from '@wordpress/i18n';
+import { useMemo } from 'react';
+import useScanStatusQuery, { isScanInProgress } from '../../data/scan/use-scan-status-query';
+import usePlan from '../../hooks/use-plan';
+import useWafData from '../../hooks/use-waf-data';
+import styles from './styles.module.scss';
+
+const IconWithLabel = ( { label, isSmall, icon } ) => (
+
+ { icon }
+ { ! isSmall && (
+
+ { label }
+
+ ) }
+
+);
+
+const HomeStatCard = ( { text, args } ) => (
+
+
+
+
+
+);
+
+const HomeStatCards = () => {
+ const ICON_HEIGHT = 20;
+
+ const { hasPlan } = usePlan();
+ const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] );
+
+ const { data: status } = useScanStatusQuery();
+ const scanning = isScanInProgress( status );
+ const numThreats = status.threats.length;
+ const scanError = status.error;
+
+ let lastCheckedLocalTimestamp = null;
+ if ( status.lastChecked ) {
+ // Convert the lastChecked UTC date to a local timestamp
+ lastCheckedLocalTimestamp = dateI18n(
+ 'F jS g:i A',
+ new Date( status.lastChecked + ' UTC' ).getTime(),
+ false
+ );
+ }
+
+ const {
+ config: { bruteForceProtection: isBruteForceModuleEnabled },
+ isEnabled: isWafModuleEnabled,
+ wafSupported,
+ stats,
+ } = useWafData();
+
+ const {
+ blockedRequests: { allTime: allTimeBlockedRequestsCount = 0 } = {},
+ blockedLogins: allTimeBlockedLoginsCount = 0,
+ } = stats || {};
+
+ const variant = useMemo( () => ( isSmall ? 'horizontal' : 'square' ), [ isSmall ] );
+
+ const lastCheckedMessage = useMemo( () => {
+ if ( scanning ) {
+ return __( 'Your results will be ready soon.', 'jetpack-protect' );
+ }
+
+ if ( scanError ) {
+ return __(
+ 'Please check your connection or try scanning again in a few minutes.',
+ 'jetpack-protect'
+ );
+ }
+
+ if ( lastCheckedLocalTimestamp ) {
+ if ( numThreats > 0 ) {
+ if ( hasPlan ) {
+ return sprintf(
+ // translators: %1$s: date/time, %2$d: number
+ _n(
+ 'Last checked on %1$s: We found %2$d threat.',
+ 'Last checked on %1$s: We found %2$d threats.',
+ numThreats,
+ 'jetpack-protect'
+ ),
+ lastCheckedLocalTimestamp,
+ numThreats
+ );
+ }
+ return sprintf(
+ // translators: %1$s: date/time, %2$d: number
+ _n(
+ 'Last checked on %1$s: We found %2$d vulnerability.',
+ 'Last checked on %1$s: We found %2$d vulnerabilities.',
+ numThreats,
+ 'jetpack-protect'
+ ),
+ lastCheckedLocalTimestamp,
+ numThreats
+ );
+ }
+ return sprintf(
+ // translators: %s: date/time
+ __( 'Last checked on %s: Your site is secure.', 'jetpack-protect' ),
+ lastCheckedLocalTimestamp
+ );
+ }
+ if ( hasPlan ) {
+ return sprintf(
+ // translators: %d: number
+ _n(
+ 'Last scan we found %d threat.',
+ 'Last scan we found %d threats.',
+ numThreats,
+ 'jetpack-protect'
+ ),
+ numThreats
+ );
+ }
+ return sprintf(
+ // translators: %d: number
+ _n(
+ 'Last scan we found %2$d vulnerability.',
+ 'Last scan we found %2$d vulnerabilities.',
+ numThreats,
+ 'jetpack-protect'
+ ),
+ numThreats
+ );
+ }, [ scanError, scanning, numThreats, lastCheckedLocalTimestamp, hasPlan ] );
+
+ const scanArgs = useMemo( () => {
+ let scanIcon;
+ if ( scanning ) {
+ scanIcon = ;
+ } else if ( scanError ) {
+ scanIcon = ;
+ } else {
+ scanIcon = (
+
+ );
+ }
+
+ let scanLabel;
+ if ( scanning ) {
+ scanLabel = __( 'One moment, pleaseā¦', 'jetpack-protect' );
+ } else if ( scanError ) {
+ scanLabel = __( 'An error occurred', 'jetpack-protect' );
+ } else if ( hasPlan ) {
+ scanLabel = _n( 'Threat identified', 'Threats identified', numThreats, 'jetpack-protect' );
+ } else {
+ scanLabel = _n(
+ 'Vulnerability identified',
+ 'Vulnerabilities identified',
+ numThreats,
+ 'jetpack-protect'
+ );
+ }
+
+ return {
+ variant,
+ icon: (
+
+ ),
+ label: { scanLabel },
+ value: numThreats,
+ hideValue: !! ( scanError || scanning ),
+ };
+ }, [ variant, scanning, ICON_HEIGHT, scanError, numThreats, hasPlan, isSmall ] );
+
+ const wafArgs = useMemo(
+ () => ( {
+ variant: variant,
+ className: isWafModuleEnabled ? styles.active : styles.disabled,
+ icon: (
+
+
+ { ! isSmall && (
+
+ { __( 'Firewall', 'jetpack-protect' ) }
+
+ ) }
+
+ ),
+ label: (
+
+ { __( 'Blocked requests', 'jetpack-protect' ) }
+
+ ),
+ value: allTimeBlockedRequestsCount,
+ hideValue: ! isWafModuleEnabled,
+ } ),
+ [ variant, isWafModuleEnabled, ICON_HEIGHT, isSmall, allTimeBlockedRequestsCount ]
+ );
+
+ const bruteForceArgs = useMemo(
+ () => ( {
+ variant: variant,
+ className: isBruteForceModuleEnabled ? styles.active : styles.disabled,
+ icon: (
+
+
+ { ! isSmall && (
+
+ { __( 'Brute force', 'jetpack-protect' ) }
+
+ ) }
+
+ ),
+ label: (
+
+ { __( 'Blocked login attempts', 'jetpack-protect' ) }
+
+ ),
+ value: allTimeBlockedLoginsCount,
+ hideValue: ! isBruteForceModuleEnabled,
+ } ),
+ [ variant, isBruteForceModuleEnabled, ICON_HEIGHT, isSmall, allTimeBlockedLoginsCount ]
+ );
+
+ return (
+
+
+ { wafSupported && (
+
+ ) }
+
+
+ );
+};
+
+export default HomeStatCards;
diff --git a/projects/plugins/protect/src/js/routes/home/index.jsx b/projects/plugins/protect/src/js/routes/home/index.jsx
new file mode 100644
index 0000000000000..718349caaac3f
--- /dev/null
+++ b/projects/plugins/protect/src/js/routes/home/index.jsx
@@ -0,0 +1,25 @@
+import { AdminSection, Container, Col } from '@automattic/jetpack-components';
+import AdminPage from '../../components/admin-page';
+import HomeAdminSectionHero from './home-admin-section-hero';
+
+/**
+ * Home Page
+ *
+ * The entry point for the Home page.
+ *
+ * @return {Component} The root component for the scan page.
+ */
+const HomePage = () => {
+ return (
+
+
+
+
+ { /* TODO: Add ScanReport component here */ }
+
+
+
+ );
+};
+
+export default HomePage;
diff --git a/projects/plugins/protect/src/js/routes/home/styles.module.scss b/projects/plugins/protect/src/js/routes/home/styles.module.scss
new file mode 100644
index 0000000000000..b99bead52dbdb
--- /dev/null
+++ b/projects/plugins/protect/src/js/routes/home/styles.module.scss
@@ -0,0 +1,69 @@
+.product-section, .info-section {
+ margin-top: calc( var( --spacing-base ) * 7 ); // 56px
+ margin-bottom: calc( var( --spacing-base ) * 7 ); // 56px
+}
+
+.view-scan-report {
+ margin-top: calc( var( --spacing-base ) * 4 ); // 32px
+}
+
+.stat-cards-wrapper {
+ display: flex;
+ justify-content: flex-start;
+
+ > *:not( last-child ) {
+ margin-right: calc( var( --spacing-base ) * 3 ); // 24px
+ }
+
+ .disabled {
+ opacity: 0.5;
+ }
+}
+
+.stat-card-icon {
+ width: 100%;
+ margin-bottom: calc( var( --spacing-base ) * 3 ); // 24px
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ svg {
+ margin: 0;
+ }
+
+ .active {
+ fill: var( --jp-green-40 );
+ }
+
+ .warning {
+ fill: var( --jp-yellow-40 );
+ }
+
+ .disabled {
+ fill: var( --jp-gray-40 );
+ }
+
+ &-label {
+ color: var( --jp-black );
+ white-space: nowrap;
+ }
+}
+
+.stat-card-tooltip {
+ margin-top: 8px;
+ max-width: 240px;
+ border-radius: 4px;
+ text-align: left;
+}
+
+
+@media ( max-width: 599px ) {
+ .stat-cards-wrapper {
+ flex-direction: column;
+ gap: var( --spacing-base ); // 8px
+ }
+
+ .stat-card-icon {
+ margin-bottom: 0;
+ }
+}
\ No newline at end of file