diff --git a/projects/js-packages/components/components/threat-modal/credentials-gate.tsx b/projects/js-packages/components/components/threat-modal/credentials-gate.tsx new file mode 100644 index 0000000000000..34ee632a56af6 --- /dev/null +++ b/projects/js-packages/components/components/threat-modal/credentials-gate.tsx @@ -0,0 +1,65 @@ +import { Text, Button } from '@automattic/jetpack-components'; +import { Notice } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import React, { ReactElement } from 'react'; +import styles from './styles.module.scss'; + +const CredentialsGate = ( { + siteCredentialsNeeded, + credentialsIsFetching, + credentialsRedirectUrl, + children, +}: { + siteCredentialsNeeded: boolean; + credentialsIsFetching: boolean; + credentialsRedirectUrl: string; + children: ReactElement; +} ): JSX.Element => { + if ( ! siteCredentialsNeeded ) { + return children; + } + + return ( + <> + + { __( + 'Before Jetpack can auto-fix threats on your site, it needs your server credentials.', + 'jetpack' + ) } + + } + /> + + + { __( + 'Your server credentials allow Jetpack to access the server that’s powering your website. This information is securely saved and only used to perform fix threats detected on your site.', + 'jetpack' + ) } + + + + { __( + 'Once you’ve entered server credentials, Jetpack will be fixing the selected threats.', + 'jetpack' + ) } + + +
+ +
+ + ); +}; + +export default CredentialsGate; diff --git a/projects/js-packages/components/components/threat-modal/index.tsx b/projects/js-packages/components/components/threat-modal/index.tsx index 50846c463422a..2ed59e960c946 100644 --- a/projects/js-packages/components/components/threat-modal/index.tsx +++ b/projects/js-packages/components/components/threat-modal/index.tsx @@ -4,16 +4,25 @@ import { Modal, Notice } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useMemo } from 'react'; import Text from '../text'; +import CredentialsGate from './credentials-gate'; import styles from './styles.module.scss'; import ThreatActions from './threat-actions'; import ThreatFixDetails from './threat-fix-details'; import ThreatTechnicalDetails from './threat-technical-details'; +import UserConnectionGate from './user-connection-gate'; /** * ThreatModal component * * @param {object} props - The props. * @param {object} props.threat - The threat. + * @param {boolean} props.isUserConnected - Whether the user is connected. + * @param {boolean} props.hasConnectedOwner - Whether the user has a connected owner. + * @param {boolean} props.userIsConnecting - Whether the user is connecting. + * @param {Function} props.handleConnectUser - The handleConnectUser function. + * @param {object} props.credentials - The credentials. + * @param {boolean} props.credentialsIsFetching - Whether the credentials are fetching. + * @param {string} props.credentialsRedirectUrl - The credentials redirect URL. * @param {Function} props.handleUpgradeClick - The handleUpgradeClick function. * @param {Function} props.handleFixThreatClick - The handleFixThreatClick function. * @param {Function} props.handleIgnoreThreatClick - The handleIgnoreThreatClick function. @@ -23,6 +32,13 @@ import ThreatTechnicalDetails from './threat-technical-details'; */ export default function ThreatModal( { threat, + isUserConnected, + hasConnectedOwner, + userIsConnecting, + handleConnectUser, + credentials, + credentialsIsFetching, + credentialsRedirectUrl, handleUpgradeClick, handleFixThreatClick, handleIgnoreThreatClick, @@ -30,71 +46,107 @@ export default function ThreatModal( { ...modalProps }: { threat: Threat; + isUserConnected: boolean; + hasConnectedOwner: boolean; + userIsConnecting: boolean; + handleConnectUser: () => void; + credentials: false | Record< string, unknown >[]; + credentialsIsFetching: boolean; + credentialsRedirectUrl: string; handleUpgradeClick?: () => void; handleFixThreatClick?: ( threats: Threat[] ) => void; handleIgnoreThreatClick?: ( threats: Threat[] ) => void; handleUnignoreThreatClick?: ( threats: Threat[] ) => void; } & React.ComponentProps< typeof Modal > ): JSX.Element { + const userConnectionNeeded = ! isUserConnected || ! hasConnectedOwner; + const siteCredentialsNeeded = ! credentials || credentials.length === 0; + const fixerState = useMemo( () => { return getFixerState( threat.fixer ); }, [ threat.fixer ] ); + const getModalTitle = useMemo( () => { + if ( userConnectionNeeded ) { + return { __( 'User connection needed', 'jetpack' ) }; + } + + if ( siteCredentialsNeeded ) { + return { __( 'Site credentials needed', 'jetpack' ) }; + } + + return ( + <> + { threat.title } + { !! threat.severity && } + + ); + }, [ userConnectionNeeded, siteCredentialsNeeded, threat.title, threat.severity ] ); + return ( - { threat.title } - { !! threat.severity && } - - } + title={
{ getModalTitle }
} { ...modalProps } >
- { fixerState.error && ( - - { __( 'An error occurred auto-fixing this threat.', 'jetpack' ) } - - ) } - { fixerState.stale && ( - - { __( 'The auto-fixer is taking longer than expected.', 'jetpack' ) } - - ) } - { fixerState.inProgress && ! fixerState.stale && ( - - { __( 'The auto-fixer is in progress.', 'jetpack' ) } - - ) } -
- { !! threat.description && { threat.description } } + + + <> + { fixerState.error && ( + + { __( 'An error occurred auto-fixing this threat.', 'jetpack' ) } + + ) } + { fixerState.stale && ( + + { __( 'The auto-fixer is taking longer than expected.', 'jetpack' ) } + + ) } + { fixerState.inProgress && ! fixerState.stale && ( + + { __( 'The auto-fixer is in progress.', 'jetpack' ) } + + ) } +
+ { !! threat.description && { threat.description } } - { !! threat.source && ( -
- -
- ) } -
+ { !! threat.source && ( +
+ +
+ ) } +
- + - + - + + + +
); diff --git a/projects/js-packages/components/components/threat-modal/stories/index.stories.tsx b/projects/js-packages/components/components/threat-modal/stories/index.stories.tsx index e1e2f954c6184..c217a39f59ee4 100644 --- a/projects/js-packages/components/components/threat-modal/stories/index.stories.tsx +++ b/projects/js-packages/components/components/threat-modal/stories/index.stories.tsx @@ -41,6 +41,71 @@ ThreatResult.args = { marks: {}, }, }, + isUserConnected: true, + hasConnectedOwner: true, + credentials: [ { type: 'managed', role: 'main', still_valid: true } ], + handleFixThreatClick: () => {}, + handleIgnoreThreatClick: () => {}, + handleUnignoreThreatClick: () => {}, +}; + +export const UserConnectionNeeded = Base.bind( {} ); +UserConnectionNeeded.args = { + threat: { + id: 185869885, + signature: 'EICAR_AV_Test', + title: 'Malicious code found in file: index.php', + description: + "This is the standard EICAR antivirus test code, and not a real infection. If your site contains this code when you don't expect it to, contact Jetpack support for some help.", + firstDetected: '2024-10-07T20:45:06.000Z', + fixedIn: null, + severity: 8, + fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' }, + fixer: { status: 'not_started' }, + status: 'current', + filename: '/var/www/html/wp-content/index.php', + context: { + '1': 'echo << {}, + handleFixThreatClick: () => {}, + handleIgnoreThreatClick: () => {}, + handleUnignoreThreatClick: () => {}, +}; + +export const CredentialsNeeded = Base.bind( {} ); +CredentialsNeeded.args = { + threat: { + id: 185869885, + signature: 'EICAR_AV_Test', + title: 'Malicious code found in file: index.php', + description: + "This is the standard EICAR antivirus test code, and not a real infection. If your site contains this code when you don't expect it to, contact Jetpack support for some help.", + firstDetected: '2024-10-07T20:45:06.000Z', + fixedIn: null, + severity: 8, + fixable: { fixer: 'rollback', target: 'January 26, 2024, 6:49 am', extensionStatus: '' }, + fixer: { status: 'not_started' }, + status: 'current', + filename: '/var/www/html/wp-content/index.php', + context: { + '1': 'echo << {}, handleIgnoreThreatClick: () => {}, handleUnignoreThreatClick: () => {}, @@ -63,5 +128,8 @@ VulnerableExtension.args = { type: 'plugin', }, }, + isUserConnected: true, + hasConnectedOwner: true, + credentials: [ { type: 'managed', role: 'main', still_valid: true } ], handleUpgradeClick: () => {}, }; diff --git a/projects/js-packages/components/components/threat-modal/user-connection-gate.tsx b/projects/js-packages/components/components/threat-modal/user-connection-gate.tsx new file mode 100644 index 0000000000000..c016739a4d585 --- /dev/null +++ b/projects/js-packages/components/components/threat-modal/user-connection-gate.tsx @@ -0,0 +1,64 @@ +import { Text, Button } from '@automattic/jetpack-components'; +import { Notice } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import React, { ReactElement } from 'react'; +import styles from './styles.module.scss'; + +const UserConnectionGate = ( { + userConnectionNeeded, + userIsConnecting, + handleConnectUser, + children, +}: { + userConnectionNeeded: boolean; + userIsConnecting: boolean; + handleConnectUser: () => void; + children: ReactElement; +} ): JSX.Element => { + if ( ! userConnectionNeeded ) { + return children; + } + return ( + <> + + { __( + 'Before Jetpack can ignore and auto-fix threats on your site, a user connection is needed.', + 'jetpack' + ) } + + } + /> + + + { __( + 'A user connection provides Jetpack the access necessary to perform these tasks.', + 'jetpack' + ) } + + + + { __( + 'Once you’ve secured a user connection, all Jetpack features will be available for use.', + 'jetpack' + ) } + + +
+ +
+ + ); +}; + +export default UserConnectionGate;