Device manager - device security recommendation card (PSG-637) (#9158)

* add security card and style

* deprecate warning and verified svgs that use hard coded color

* style icons, test

* i18n

* stylelint

* redo lost lint fixes

* fix svg ref

* actually fix svg

* fix stupid copy pasting

* use rgba for e2e light variations

* add security card and style

* deprecate warning and verified svgs that use hard coded color

* style icons, test

* i18n

* stylelint

* fix svg ref

* actually fix svg

* fix stupid copy pasting

* use rgba for e2e light variations

* use device security card in current session section

* lint

* update snapshot test after dev merge
This commit is contained in:
Kerry 2022-08-11 12:39:14 +02:00 committed by GitHub
parent 0be622e7f0
commit 7b52145461
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 401 additions and 11 deletions

View file

@ -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";

View file

@ -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;
}

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="#010101"/>
</svg>

After

Width:  |  Height:  |  Size: 658 B

View file

@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="#010101"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 658 B

After

Width:  |  Height:  |  Size: 663 B

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="#020202"/>
</svg>

After

Width:  |  Height:  |  Size: 673 B

View file

@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="#020202"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 678 B

View file

@ -0,0 +1,3 @@
<svg width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33333 0.333008C0.6 0.333008 0 0.933008 0 1.66634L0.00666682 3.78634C0.00666682 4.13967 0.146667 4.47301 0.393333 4.72634L2.66667 6.99967L0.393333 9.28634C0.146667 9.53301 0.00666682 9.87301 0.00666682 10.2263L0 12.333C0 13.0663 0.6 13.6663 1.33333 13.6663H6.66667C7.4 13.6663 8 13.0663 8 12.333V10.2263C8 9.87301 7.86 9.53301 7.61333 9.28634L5.33333 6.99967L7.60667 4.73301C7.86 4.47967 8 4.13967 8 3.78634V1.66634C8 0.933008 7.4 0.333008 6.66667 0.333008H1.33333ZM6.66667 10.273V11.6663C6.66667 12.033 6.36667 12.333 6 12.333H2C1.63333 12.333 1.33333 12.033 1.33333 11.6663V10.273C1.33333 10.093 1.40667 9.92634 1.52667 9.79967L4 7.33301L6.47333 9.80634C6.59333 9.92634 6.66667 10.0997 6.66667 10.273Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 839 B

View file

@ -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;

View file

@ -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 */

View file

@ -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;
}
}

View file

@ -85,7 +85,7 @@ const EncryptionPanel: React.FC<IProps> = (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: <div>
{ _t("One of the following may be compromised:") }

View file

@ -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, React.FC<React.SVGProps<SVGSVGElement>>> = {
[DeviceSecurityVariation.Inactive]: InactiveIcon,
[DeviceSecurityVariation.Verified]: VerifiedIcon,
[DeviceSecurityVariation.Unverified]: UnverifiedIcon,
};
const DeviceSecurityIcon: React.FC<{ variation: DeviceSecurityVariation }> = ({ variation }) => {
const Icon = VariationIcon[variation];
return <div className={classNames('mx_DeviceSecurityCard_icon', variation)}>
<Icon height={16} width={16} />
</div>;
};
const DeviceSecurityCard: React.FC<Props> = ({ variation, heading, description, children }) => {
return <div className='mx_DeviceSecurityCard'>
<DeviceSecurityIcon variation={variation} />
<div className='mx_DeviceSecurityCard_content'>
<p className='mx_DeviceSecurityCard_heading'>{ heading }</p>
<p className='mx_DeviceSecurityCard_description'>{ description }</p>
{ children }
</div>
</div>;
};
export default DeviceSecurityCard;

View file

@ -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 <SettingsTab heading={_t('Sessions')}>
<SettingsSubsection
heading={_t('Current session')}
data-testid='current-session-section'
>
{ isLoading && <Spinner /> }
{ !!currentDevice && <DeviceTile
device={currentDevice}
/> }
{ !!currentDevice && <>
<DeviceTile
device={currentDevice}
/>
<br />
<DeviceSecurityCard
{...securityCardProps}
/>
</>
}
</SettingsSubsection>
{
shouldShowOtherSessions &&

View file

@ -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",

View file

@ -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('<DeviceSecurityCard />', () => {
const defaultProps = {
variation: DeviceSecurityVariation.Verified,
heading: 'Verified session',
description: 'nice',
};
const getComponent = (props = {}): React.ReactElement =>
<DeviceSecurityCard {...defaultProps} {...props} />;
it('renders basic card', () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it('renders with children', () => {
const { container } = render(getComponent({
children: <div>hey</div>,
variation: DeviceSecurityVariation.Unverified,
}));
expect(container).toMatchSnapshot();
});
});

View file

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DeviceSecurityCard /> renders basic card 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
nice
</p>
</div>
</div>
</div>
`;
exports[`<DeviceSecurityCard /> renders with children 1`] = `
<div>
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
nice
</p>
<div>
hey
</div>
</div>
</div>
</div>
`;

View file

@ -143,7 +143,7 @@ describe('<SessionManagerTab />', () => {
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('<SessionManagerTab />', () => {
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('<SessionManagerTab />', () => {
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],
});

View file

@ -1,6 +1,78 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SessionManagerTab /> renders current session section 1`] = `
exports[`<SessionManagerTab /> renders current session section with a verified session 1`] = `
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
>
<h3
class="mx_Heading_h3 mx_SettingsSubsection_heading"
>
Current session
</h3>
<div
class="mx_SettingsSubsection_content"
>
<div
class="mx_DeviceTile"
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceTile_info"
>
<h4
class="mx_Heading_h4"
>
alices_device
</h4>
<div
class="mx_DeviceTile_metadata"
>
<span
data-testid="device-metadata-isVerified"
>
Verified
</span>
·
·
</div>
</div>
<div
class="mx_DeviceTile_actions"
/>
</div>
<br />
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Verified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Verified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
This session is ready for secure messaging.
</p>
</div>
</div>
</div>
</div>
`;
exports[`<SessionManagerTab /> renders current session section with an unverified session 1`] = `
<div
class="mx_SettingsSubsection"
data-testid="current-session-section"
@ -41,6 +113,33 @@ exports[`<SessionManagerTab /> renders current session section 1`] = `
class="mx_DeviceTile_actions"
/>
</div>
<br />
<div
class="mx_DeviceSecurityCard"
>
<div
class="mx_DeviceSecurityCard_icon Unverified"
>
<div
height="16"
width="16"
/>
</div>
<div
class="mx_DeviceSecurityCard_content"
>
<p
class="mx_DeviceSecurityCard_heading"
>
Unverified session
</p>
<p
class="mx_DeviceSecurityCard_description"
>
Verify or sign out from this session for best security and reliability.
</p>
</div>
</div>
</div>
</div>
`;