diff --git a/res/css/_components.pcss b/res/css/_components.pcss index 0e5c5fd3a5..6515bd90d3 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -31,6 +31,7 @@ @import "./components/views/settings/devices/_DeviceSecurityCard.pcss"; @import "./components/views/settings/devices/_DeviceTile.pcss"; @import "./components/views/settings/devices/_FilteredDeviceList.pcss"; +@import "./components/views/settings/devices/_SecurityRecommendations.pcss"; @import "./components/views/settings/devices/_SelectableDeviceTile.pcss"; @import "./components/views/settings/shared/_SettingsSubsection.pcss"; @import "./components/views/spaces/_QuickThemeSwitcher.pcss"; diff --git a/res/css/components/views/settings/devices/_SecurityRecommendations.pcss b/res/css/components/views/settings/devices/_SecurityRecommendations.pcss new file mode 100644 index 0000000000..d0a5333559 --- /dev/null +++ b/res/css/components/views/settings/devices/_SecurityRecommendations.pcss @@ -0,0 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_SecurityRecommendations_spacing { + height: $spacing-16; +} diff --git a/src/components/views/settings/devices/DeviceSecurityCard.tsx b/src/components/views/settings/devices/DeviceSecurityCard.tsx index d71409cb4f..01fe488882 100644 --- a/src/components/views/settings/devices/DeviceSecurityCard.tsx +++ b/src/components/views/settings/devices/DeviceSecurityCard.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg'; import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg'; import { Icon as InactiveIcon } from '../../../../../res/img/element-icons/settings/inactive.svg'; -import { DeviceSecurityVariation } from './filter'; +import { DeviceSecurityVariation } from './types'; interface Props { variation: DeviceSecurityVariation; heading: string; diff --git a/src/components/views/settings/devices/DeviceTile.tsx b/src/components/views/settings/devices/DeviceTile.tsx index 68c355316e..3b553ac401 100644 --- a/src/components/views/settings/devices/DeviceTile.tsx +++ b/src/components/views/settings/devices/DeviceTile.tsx @@ -23,7 +23,7 @@ import TooltipTarget from "../../elements/TooltipTarget"; import { Alignment } from "../../elements/Tooltip"; import Heading from "../../typography/Heading"; import { INACTIVE_DEVICE_AGE_MS, isDeviceInactive } from "./filter"; -import { DeviceWithVerification } from "./useOwnDevices"; +import { DeviceWithVerification } from "./types"; export interface DeviceTileProps { device: DeviceWithVerification; children?: React.ReactNode; diff --git a/src/components/views/settings/devices/FilteredDeviceList.tsx b/src/components/views/settings/devices/FilteredDeviceList.tsx index 5920d67132..116066d78f 100644 --- a/src/components/views/settings/devices/FilteredDeviceList.tsx +++ b/src/components/views/settings/devices/FilteredDeviceList.tsx @@ -18,7 +18,7 @@ import React from 'react'; import DeviceTile from './DeviceTile'; import { filterDevicesBySecurityRecommendation } from './filter'; -import { DevicesDictionary, DeviceWithVerification } from './useOwnDevices'; +import { DevicesDictionary, DeviceWithVerification } from './types'; interface Props { devices: DevicesDictionary; diff --git a/src/components/views/settings/devices/SecurityRecommendations.tsx b/src/components/views/settings/devices/SecurityRecommendations.tsx new file mode 100644 index 0000000000..fcb8f086c9 --- /dev/null +++ b/src/components/views/settings/devices/SecurityRecommendations.tsx @@ -0,0 +1,100 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; + +import { _t } from '../../../../languageHandler'; +import AccessibleButton from '../../elements/AccessibleButton'; +import SettingsSubsection from '../shared/SettingsSubsection'; +import DeviceSecurityCard from './DeviceSecurityCard'; +import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_MS } from './filter'; +import { DevicesDictionary, DeviceSecurityVariation } from './types'; + +interface Props { + devices: DevicesDictionary; +} +const MS_DAY = 24 * 60 * 60 * 1000; + +const SecurityRecommendations: React.FC = ({ devices }) => { + const devicesArray = Object.values(devices); + + const unverifiedDevicesCount = filterDevicesBySecurityRecommendation( + devicesArray, + [DeviceSecurityVariation.Unverified], + ).length; + const inactiveDevicesCount = filterDevicesBySecurityRecommendation( + devicesArray, + [DeviceSecurityVariation.Inactive], + ).length; + + if (!(unverifiedDevicesCount | inactiveDevicesCount)) { + return null; + } + + const inactiveAgeDays = INACTIVE_DEVICE_AGE_MS / MS_DAY; + + // TODO(kerrya) stubbed until PSG-640/652 + const noop = () => {}; + + return + { + !!unverifiedDevicesCount && + + + { _t('View all') + ` (${unverifiedDevicesCount})` } + + + } + { + !!inactiveDevicesCount && + <> + { !!unverifiedDevicesCount &&
} + + + { _t('View all') + ` (${inactiveDevicesCount})` } + + + + } + ; +}; + +export default SecurityRecommendations; diff --git a/src/components/views/settings/devices/filter.ts b/src/components/views/settings/devices/filter.ts index 82bd9e5905..302c66969b 100644 --- a/src/components/views/settings/devices/filter.ts +++ b/src/components/views/settings/devices/filter.ts @@ -14,13 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DeviceWithVerification } from "./useOwnDevices"; - -export enum DeviceSecurityVariation { - Verified = 'Verified', - Unverified = 'Unverified', - Inactive = 'Inactive', -} +import { DeviceWithVerification, DeviceSecurityVariation } from "./types"; type DeviceFilterCondition = (device: DeviceWithVerification) => boolean; diff --git a/src/components/views/settings/devices/types.ts b/src/components/views/settings/devices/types.ts new file mode 100644 index 0000000000..1f3328c09e --- /dev/null +++ b/src/components/views/settings/devices/types.ts @@ -0,0 +1,26 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { IMyDevice } from "matrix-js-sdk/src/matrix"; + +export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null }; +export type DevicesDictionary = Record; + +export enum DeviceSecurityVariation { + Verified = 'Verified', + Unverified = 'Unverified', + Inactive = 'Inactive', +} diff --git a/src/components/views/settings/devices/useOwnDevices.ts b/src/components/views/settings/devices/useOwnDevices.ts index f48d35acdd..ec5ee1ca18 100644 --- a/src/components/views/settings/devices/useOwnDevices.ts +++ b/src/components/views/settings/devices/useOwnDevices.ts @@ -20,8 +20,7 @@ import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning"; import { logger } from "matrix-js-sdk/src/logger"; import MatrixClientContext from "../../../../contexts/MatrixClientContext"; - -export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null }; +import { DevicesDictionary } from "./types"; const isDeviceVerified = ( matrixClient: MatrixClient, @@ -56,11 +55,11 @@ const fetchDevicesWithVerification = async (matrixClient: MatrixClient): Promise return devicesDict; }; + export enum OwnDevicesError { Unsupported = 'Unsupported', Default = 'Default', } -export type DevicesDictionary = Record; type DevicesState = { devices: DevicesDictionary; currentDeviceId: string; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index ebda5ebe82..a7201e360a 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -23,7 +23,8 @@ import DeviceTile from '../../devices/DeviceTile'; import DeviceSecurityCard from '../../devices/DeviceSecurityCard'; import SettingsSubsection from '../../shared/SettingsSubsection'; import FilteredDeviceList from '../../devices/FilteredDeviceList'; -import { DeviceSecurityVariation } from '../../devices/filter'; +import { DeviceSecurityVariation } from '../../devices/types'; +import SecurityRecommendations from '../../devices/SecurityRecommendations'; import SettingsTab from '../SettingsTab'; const SessionManagerTab: React.FC = () => { @@ -43,6 +44,7 @@ const SessionManagerTab: React.FC = () => { }; return + ', () => { const defaultProps = { diff --git a/test/components/views/settings/devices/SecurityRecommendations-test.tsx b/test/components/views/settings/devices/SecurityRecommendations-test.tsx new file mode 100644 index 0000000000..b54745545b --- /dev/null +++ b/test/components/views/settings/devices/SecurityRecommendations-test.tsx @@ -0,0 +1,72 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import SecurityRecommendations from '../../../../../src/components/views/settings/devices/SecurityRecommendations'; + +const MS_DAY = 24 * 60 * 60 * 1000; +describe('', () => { + const unverifiedNoMetadata = { device_id: 'unverified-no-metadata', isVerified: false }; + const verifiedNoMetadata = { device_id: 'verified-no-metadata', isVerified: true }; + const hundredDaysOld = { device_id: '100-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 100) }; + const hundredDaysOldUnverified = { + device_id: 'unverified-100-days-old', + isVerified: false, + last_seen_ts: Date.now() - (MS_DAY * 100), + }; + + const defaultProps = { + devices: {}, + }; + const getComponent = (props = {}) => + (); + + it('renders null when no devices', () => { + const { container } = render(getComponent()); + expect(container.firstChild).toBeNull(); + }); + + it('renders unverified devices section when user has unverified devices', () => { + const devices = { + [unverifiedNoMetadata.device_id]: unverifiedNoMetadata, + [verifiedNoMetadata.device_id]: verifiedNoMetadata, + [hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified, + }; + const { container } = render(getComponent({ devices })); + expect(container).toMatchSnapshot(); + }); + + it('renders inactive devices section when user has inactive devices', () => { + const devices = { + [verifiedNoMetadata.device_id]: verifiedNoMetadata, + [hundredDaysOldUnverified.device_id]: hundredDaysOldUnverified, + }; + const { container } = render(getComponent({ devices })); + expect(container).toMatchSnapshot(); + }); + + it('renders both cards when user has both unverified and inactive devices', () => { + const devices = { + [verifiedNoMetadata.device_id]: verifiedNoMetadata, + [hundredDaysOld.device_id]: hundredDaysOld, + [unverifiedNoMetadata.device_id]: unverifiedNoMetadata, + }; + const { container } = render(getComponent({ devices })); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap new file mode 100644 index 0000000000..6de70e4f50 --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/SecurityRecommendations-test.tsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders both cards when user has both unverified and inactive devices 1`] = ` +
+
+

+ Security recommendations +

+
+ Improve your account security by following these recommendations +
+
+
+
+
+
+
+

+ Unverified sessions +

+

+ Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore. +

+ +
+
+
+
+
+
+
+
+

+ Inactive sessions +

+

+ Consider signing out from old sessions (90 days or older) you don't use anymore +

+ +
+
+
+
+
+`; + +exports[` renders inactive devices section when user has inactive devices 1`] = ` +
+
+

+ Security recommendations +

+
+ Improve your account security by following these recommendations +
+
+
+
+
+
+
+

+ Unverified sessions +

+

+ Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore. +

+ +
+
+
+
+
+
+
+
+

+ Inactive sessions +

+

+ Consider signing out from old sessions (90 days or older) you don't use anymore +

+ +
+
+
+
+
+`; + +exports[` renders unverified devices section when user has unverified devices 1`] = ` +
+
+

+ Security recommendations +

+
+ Improve your account security by following these recommendations +
+
+
+
+
+
+
+

+ Unverified sessions +

+

+ Verify your sessions for enhanced secure messaging or sign out from those you don't recognize or use anymore. +

+ +
+
+
+
+
+
+
+
+

+ Inactive sessions +

+

+ Consider signing out from old sessions (90 days or older) you don't use anymore +

+ +
+
+
+
+
+`; diff --git a/test/components/views/settings/devices/filter-test.ts b/test/components/views/settings/devices/filter-test.ts index 073efc79cd..f46b8838ca 100644 --- a/test/components/views/settings/devices/filter-test.ts +++ b/test/components/views/settings/devices/filter-test.ts @@ -15,9 +15,11 @@ limitations under the License. */ import { - DeviceSecurityVariation, filterDevicesBySecurityRecommendation, } from "../../../../../src/components/views/settings/devices/filter"; +import { + DeviceSecurityVariation, +} from "../../../../../src/components/views/settings/devices/types"; const MS_DAY = 86400000; describe('filterDevicesBySecurityRecommendation()', () => {