diff --git a/res/css/_components.pcss b/res/css/_components.pcss index cf4a7914ca1..5e43b2810db 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -28,6 +28,7 @@ @import "./components/views/messages/_MBeaconBody.pcss"; @import "./components/views/messages/shared/_MediaProcessingError.pcss"; @import "./components/views/settings/devices/_DeviceDetails.pcss"; +@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/_SelectableDeviceTile.pcss"; diff --git a/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss b/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss new file mode 100644 index 00000000000..e6a207bf27e --- /dev/null +++ b/res/css/components/views/settings/devices/_DeviceSecurityCard.pcss @@ -0,0 +1,69 @@ +/* +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_DeviceSecurityCard { + width: 100%; + display: flex; + flex-direction: row; + align-items: flex-start; + + padding: $spacing-16; + + border: 1px solid $quinary-content; + border-radius: 8px; +} + +.mx_DeviceSecurityCard_icon { + flex: 0 0 40px; + display: flex; + align-items: center; + justify-content: center; + margin-right: $spacing-16; + border-radius: 8px; + + height: 40px; + width: 40px; + + color: var(--icon-color); + background-color: var(--background-color); + + &.Verified { + --icon-color: $e2e-verified-color; + --background-color: $e2e-verified-color-light; + } + + &.Unverified { + --icon-color: $e2e-warning-color; + --background-color: $e2e-warning-color-light; + } + + &.Inactive { + --icon-color: $secondary-content; + --background-color: $system; + } +} + +.mx_DeviceSecurityCard_content { + flex: 1 1; +} +.mx_DeviceSecurityCard_heading { + margin: 0 0 $spacing-4 0; +} +.mx_DeviceSecurityCard_description { + margin: 0 0 $spacing-8 0; + font-size: $font-12px; + color: $secondary-content; +} diff --git a/res/img/e2e/verified-deprecated.svg b/res/img/e2e/verified-deprecated.svg new file mode 100644 index 00000000000..f90d9db554c --- /dev/null +++ b/res/img/e2e/verified-deprecated.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/e2e/verified.svg b/res/img/e2e/verified.svg index f90d9db554c..9213d2b05d9 100644 --- a/res/img/e2e/verified.svg +++ b/res/img/e2e/verified.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/e2e/warning-deprecated.svg b/res/img/e2e/warning-deprecated.svg new file mode 100644 index 00000000000..58f5c3b7d1c --- /dev/null +++ b/res/img/e2e/warning-deprecated.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/e2e/warning.svg b/res/img/e2e/warning.svg index 58f5c3b7d1c..1acbb53bb71 100644 --- a/res/img/e2e/warning.svg +++ b/res/img/e2e/warning.svg @@ -1,3 +1,3 @@ - + diff --git a/res/img/element-icons/settings/inactive.svg b/res/img/element-icons/settings/inactive.svg new file mode 100644 index 00000000000..63b6b97bd59 --- /dev/null +++ b/res/img/element-icons/settings/inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/legacy-light/css/_legacy-light.pcss b/res/themes/legacy-light/css/_legacy-light.pcss index 4da7e8e1230..20ca67a3cf7 100644 --- a/res/themes/legacy-light/css/_legacy-light.pcss +++ b/res/themes/legacy-light/css/_legacy-light.pcss @@ -217,6 +217,8 @@ $e2e-verified-color: #76cfa5; /* N.B. *NOT* the same as $accent */ $e2e-unknown-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; +$e2e-verified-color-light: rgba($e2e-verified-color, 0.06); +$e2e-warning-color-light: rgba($e2e-warning-color, 0.06); /*** ImageView ***/ $lightbox-bg-color: #454545; diff --git a/res/themes/light/css/_light.pcss b/res/themes/light/css/_light.pcss index abde9cf52d1..fc9168ccf81 100644 --- a/res/themes/light/css/_light.pcss +++ b/res/themes/light/css/_light.pcss @@ -201,6 +201,8 @@ $e2e-verified-color: #76cfa5; /* N.B. *NOT* the same as $accent */ $e2e-unknown-color: #e8bf37; $e2e-unverified-color: #e8bf37; $e2e-warning-color: #ba6363; +$e2e-verified-color-light: rgba($e2e-verified-color, 0.06); +$e2e-warning-color-light: rgba($e2e-warning-color, 0.06); /* ******************** */ /* Tabbed views */ diff --git a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx index 1a945405023..63d9ad1d2cd 100644 --- a/src/components/views/dialogs/security/SetupEncryptionDialog.tsx +++ b/src/components/views/dialogs/security/SetupEncryptionDialog.tsx @@ -24,9 +24,9 @@ import { IDialogProps } from "../IDialogProps"; function iconFromPhase(phase: Phase) { if (phase === Phase.Done) { - return require("../../../../../res/img/e2e/verified.svg").default; + return require("../../../../../res/img/e2e/verified-deprecated.svg").default; } else { - return require("../../../../../res/img/e2e/warning.svg").default; + return require("../../../../../res/img/e2e/warning-deprecated.svg").default; } } diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index a9ab3e6ab38..d0d6c7bf5b9 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -85,7 +85,7 @@ const EncryptionPanel: React.FC = (props: IProps) => { // handle transitions -> cancelled for mismatches which fire a modal instead of showing a card if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) { Modal.createDialog(ErrorDialog, { - headerImage: require("../../../../res/img/e2e/warning.svg").default, + headerImage: require("../../../../res/img/e2e/warning-deprecated.svg").default, title: _t("Your messages are not secure"), description:
{ _t("One of the following may be compromised:") } diff --git a/src/components/views/settings/devices/DeviceSecurityCard.tsx b/src/components/views/settings/devices/DeviceSecurityCard.tsx new file mode 100644 index 00000000000..bf5a3e5729b --- /dev/null +++ b/src/components/views/settings/devices/DeviceSecurityCard.tsx @@ -0,0 +1,60 @@ +/* +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 classNames from 'classnames'; +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'; + +export enum DeviceSecurityVariation { + Verified = 'Verified', + Unverified = 'Unverified', + Inactive = 'Inactive', +} +interface Props { + variation: DeviceSecurityVariation; + heading: string; + description: string | React.ReactNode; + children?: React.ReactNode; +} + +const VariationIcon: Record>> = { + [DeviceSecurityVariation.Inactive]: InactiveIcon, + [DeviceSecurityVariation.Verified]: VerifiedIcon, + [DeviceSecurityVariation.Unverified]: UnverifiedIcon, +}; + +const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => { + const Icon = VariationIcon[variation]; + return
+ +
; +}; + +const DeviceSecurityCard: React.FC = ({ variation, heading, description, children }) => { + return
+ +
+

{ heading }

+

{ description }

+ { children } +
+
; +}; + +export default DeviceSecurityCard; diff --git a/src/components/views/settings/tabs/user/SessionManagerTab.tsx b/src/components/views/settings/tabs/user/SessionManagerTab.tsx index cba3234caec..707759fbcab 100644 --- a/src/components/views/settings/tabs/user/SessionManagerTab.tsx +++ b/src/components/views/settings/tabs/user/SessionManagerTab.tsx @@ -20,6 +20,7 @@ import { _t } from "../../../../../languageHandler"; import Spinner from '../../../elements/Spinner'; import { useOwnDevices } from '../../devices/useOwnDevices'; import DeviceTile from '../../devices/DeviceTile'; +import DeviceSecurityCard, { DeviceSecurityVariation } from '../../devices/DeviceSecurityCard'; import SettingsSubsection from '../../shared/SettingsSubsection'; import SettingsTab from '../SettingsTab'; import FilteredDeviceList from '../../devices/FilteredDeviceList'; @@ -30,15 +31,32 @@ const SessionManagerTab: React.FC = () => { const { [currentDeviceId]: currentDevice, ...otherDevices } = devices; const shouldShowOtherSessions = Object.keys(otherDevices).length > 0; + const securityCardProps = currentDevice?.isVerified ? { + variation: DeviceSecurityVariation.Verified, + heading: _t('Verified session'), + description: _t('This session is ready for secure messaging.'), + } : { + variation: DeviceSecurityVariation.Unverified, + heading: _t('Unverified session'), + description: _t('Verify or sign out from this session for best security and reliability.'), + }; + return { isLoading && } - { !!currentDevice && } + { !!currentDevice && <> + +
+ + + }
{ shouldShowOtherSessions && diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e97849c9fdd..cfd67aba9b2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1553,6 +1553,10 @@ "Share anonymous data to help us identify issues. Nothing personal. No third parties.": "Share anonymous data to help us identify issues. Nothing personal. No third parties.", "Where you're signed in": "Where you're signed in", "Manage your signed-in devices below. A device's name is visible to people you communicate with.": "Manage your signed-in devices below. A device's name is visible to people you communicate with.", + "Verified session": "Verified session", + "This session is ready for secure messaging.": "This session is ready for secure messaging.", + "Unverified session": "Unverified session", + "Verify or sign out from this session for best security and reliability.": "Verify or sign out from this session for best security and reliability.", "Sessions": "Sessions", "Current session": "Current session", "Other sessions": "Other sessions", diff --git a/test/components/views/settings/devices/DeviceSecurityCard-test.tsx b/test/components/views/settings/devices/DeviceSecurityCard-test.tsx new file mode 100644 index 00000000000..045478fa803 --- /dev/null +++ b/test/components/views/settings/devices/DeviceSecurityCard-test.tsx @@ -0,0 +1,45 @@ +/* +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 { render } from '@testing-library/react'; +import React from 'react'; + +import DeviceSecurityCard, { + DeviceSecurityVariation, +} from '../../../../../src/components/views/settings/devices/DeviceSecurityCard'; + +describe('', () => { + const defaultProps = { + variation: DeviceSecurityVariation.Verified, + heading: 'Verified session', + description: 'nice', + }; + const getComponent = (props = {}): React.ReactElement => + ; + + it('renders basic card', () => { + const { container } = render(getComponent()); + expect(container).toMatchSnapshot(); + }); + + it('renders with children', () => { + const { container } = render(getComponent({ + children:
hey
, + variation: DeviceSecurityVariation.Unverified, + })); + expect(container).toMatchSnapshot(); + }); +}); diff --git a/test/components/views/settings/devices/__snapshots__/DeviceSecurityCard-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/DeviceSecurityCard-test.tsx.snap new file mode 100644 index 00000000000..900c94d451d --- /dev/null +++ b/test/components/views/settings/devices/__snapshots__/DeviceSecurityCard-test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders basic card 1`] = ` +
+
+
+
+
+
+

+ Verified session +

+

+ nice +

+
+
+
+`; + +exports[` renders with children 1`] = ` +
+
+
+
+
+
+

+ Verified session +

+

+ nice +

+
+ hey +
+
+
+
+`; diff --git a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx index f01c69a485a..84e9372ccb1 100644 --- a/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx +++ b/test/components/views/settings/tabs/user/SessionManagerTab-test.tsx @@ -143,7 +143,7 @@ describe('', () => { expect(getByTestId(`device-tile-${alicesDevice.device_id}`)).toMatchSnapshot(); }); - it('renders current session section', async () => { + it('renders current session section with an unverified session', async () => { mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); const { getByTestId } = render(getComponent()); @@ -154,6 +154,21 @@ describe('', () => { expect(getByTestId('current-session-section')).toMatchSnapshot(); }); + it('renders current session section with a verified session', async () => { + mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesMobileDevice] }); + mockClient.getStoredDevice.mockImplementation(() => new DeviceInfo(alicesDevice.device_id)); + mockCrossSigningInfo.checkDeviceTrust + .mockReturnValue(new DeviceTrustLevel(true, true, false, false)); + + const { getByTestId } = render(getComponent()); + + await act(async () => { + await flushPromisesWithFakeTimers(); + }); + + expect(getByTestId('current-session-section')).toMatchSnapshot(); + }); + it('does not render other sessions section when user has only one device', async () => { mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice] }); const { queryByTestId } = render(getComponent()); @@ -165,7 +180,7 @@ describe('', () => { expect(queryByTestId('other-sessions-section')).toBeFalsy(); }); - it('renders other sessions section', async () => { + it('renders other sessions section when user has more than one device', async () => { mockClient.getDevices.mockResolvedValue({ devices: [alicesDevice, alicesOlderMobileDevice, alicesMobileDevice], }); diff --git a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap index e0910063ab4..2e56729eadc 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/SessionManagerTab-test.tsx.snap @@ -1,6 +1,78 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` renders current session section 1`] = ` +exports[` renders current session section with a verified session 1`] = ` +
+

+ Current session +

+
+
+
+

+ alices_device +

+ +
+
+
+
+
+
+
+
+
+

+ Verified session +

+

+ This session is ready for secure messaging. +

+
+
+
+
+`; + +exports[` renders current session section with an unverified session 1`] = `
renders current session section 1`] = ` class="mx_DeviceTile_actions" />
+
+
+
+
+
+
+

+ Unverified session +

+

+ Verify or sign out from this session for best security and reliability. +

+
+
`;