1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

[FLASK] Add permission cell component (#18372)

* Add permission cell component

Add storybook and handling for revoked permission colors

* Fix things after conflict resolve after rebase

* Add code refactoring and minor UI changes

* Add permission cell component to snap-update flow

* Update storybook

* Add unit tests for permission cell

* Update component padding

* Fix spacing between permission cells

* Fix main icon color for approved permissions

* Update width value with constant
This commit is contained in:
David Drazic 2023-04-03 19:33:54 +02:00 committed by GitHub
parent f51055802f
commit 8603a4b067
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 309 additions and 164 deletions

View File

@ -2927,7 +2927,7 @@
"description": "An extended description for the `snap_getBip32Entropy` permission. $1 is a derivation path (name)"
},
"permission_manageBip44Keys": {
"message": "Control your \"$1\" accounts and assets.",
"message": "Control your $1 accounts and assets.",
"description": "The description for the `snap_getBip44Entropy` permission. $1 is the name of a protocol, e.g. 'Filecoin'."
},
"permission_manageBip44KeysDescription": {

View File

@ -59,6 +59,7 @@
@import 'permissions-connect-footer/index';
@import 'permissions-connect-header/index';
@import 'permissions-connect-permission-list/index';
@import 'permission-cell/index';
@import 'recovery-phrase-reminder/index';
@import 'set-approval-for-all-warning/index';
@import 'step-progress-bar/index.scss';

View File

@ -1,14 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isFunction } from 'lodash';
import {
getRightIcon,
getWeightedPermissions,
} from '../../../../helpers/utils/permission';
import { getWeightedPermissions } from '../../../../helpers/utils/permission';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { formatDate } from '../../../../helpers/utils/util';
import Typography from '../../../ui/typography/typography';
import { TextColor } from '../../../../helpers/constants/design-system';
import PermissionCell from '../../permission-cell';
import Box from '../../../ui/box';
export default function UpdateSnapPermissionList({
approvedPermissions,
@ -17,52 +12,50 @@ export default function UpdateSnapPermissionList({
}) {
const t = useI18nContext();
const Permissions = ({ className, permissions, subText }) => {
return getWeightedPermissions(t, permissions).map((permission) => {
const { label, permissionName, permissionValue } = permission;
return (
<div className={className} key={permissionName}>
<i className="fas fa-x" />
<div className="permission-description">
{label}
<Typography
color={TextColor.textAlternative}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{isFunction(subText)
? subText(permissionName, permissionValue)
: subText}
</Typography>
</div>
{getRightIcon(permission)}
</div>
);
});
};
return (
<div className="update-snap-permission-list">
<Permissions
className="new-permission"
permissions={newPermissions}
subText={t('permissionRequested')}
/>
<Permissions
className="approved-permission"
permissions={approvedPermissions}
subText={(_, permissionValue) => {
const { date } = permissionValue;
const formattedDate = formatDate(date, 'yyyy-MM-dd');
return t('approvedOn', [formattedDate]);
}}
/>
<Permissions
className="revoked-permission"
permissions={revokedPermissions}
subText={t('permissionRevoked')}
/>
</div>
<Box paddingTop={1}>
{getWeightedPermissions(t, newPermissions).map((permission, index) => {
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
/>
);
})}
{getWeightedPermissions(t, approvedPermissions).map(
(permission, index) => {
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
/>
);
},
)}
{getWeightedPermissions(t, revokedPermissions).map(
(permission, index) => {
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
revoked
/>
);
},
)}
</Box>
);
}

View File

@ -0,0 +1 @@
export { default } from './permission-cell';

View File

@ -0,0 +1,5 @@
.permission-cell {
&__title-revoked {
text-decoration: line-through;
}
}

View File

@ -0,0 +1,126 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Box from '../../ui/box';
import {
AlignItems,
Color,
IconColor,
JustifyContent,
Size,
TextColor,
TextVariant,
} from '../../../helpers/constants/design-system';
import {
AvatarIcon,
Icon,
ICON_NAMES,
ICON_SIZES,
Text,
} from '../../component-library';
import { formatDate } from '../../../helpers/utils/util';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Tooltip from '../../ui/tooltip';
const PermissionCell = ({
title,
description,
weight,
avatarIcon,
dateApproved,
revoked,
}) => {
const t = useI18nContext();
let infoIconColor = IconColor.iconMuted;
let infoIcon = ICON_NAMES.INFO;
let iconColor = Color.primaryDefault;
let iconBackgroundColor = Color.primaryMuted;
if (!revoked && weight === 1) {
iconColor = Color.warningDefault;
iconBackgroundColor = Color.warningMuted;
infoIconColor = IconColor.warningDefault;
infoIcon = ICON_NAMES.DANGER;
}
if (dateApproved) {
iconColor = Color.iconMuted;
iconBackgroundColor = Color.backgroundAlternative;
}
if (revoked) {
iconColor = Color.iconMuted;
iconBackgroundColor = Color.backgroundAlternative;
}
return (
<Box
className="permission-cell"
justifyContent={JustifyContent.center}
alignItems={AlignItems.flexStart}
marginLeft={4}
marginRight={4}
paddingTop={2}
paddingBottom={2}
>
<Box>
{typeof avatarIcon === 'string' ? (
<AvatarIcon
iconName={avatarIcon}
size={ICON_SIZES.MD}
iconProps={{
size: ICON_SIZES.SM,
}}
color={iconColor}
backgroundColor={iconBackgroundColor}
/>
) : (
avatarIcon
)}
</Box>
<Box width="full" marginLeft={4} marginRight={4}>
<Text
size={Size.MD}
variant={TextVariant.bodyMd}
className={classnames('permission-cell__title', {
'permission-cell__title-revoked': revoked,
})}
>
{title}
</Text>
<Text
size={Size.XS}
className="permission-cell__status"
variant={TextVariant.bodyXs}
color={TextColor.textAlternative}
>
{!revoked &&
(dateApproved
? t('approvedOn', [formatDate(dateApproved, 'yyyy-MM-dd')])
: t('permissionRequested'))}
{revoked ? t('permissionRevoked') : ''}
</Text>
</Box>
<Box>
<Tooltip html={<div>{description}</div>} position="bottom">
<Icon color={infoIconColor} name={infoIcon} size={ICON_SIZES.SM} />
</Tooltip>
</Box>
</Box>
);
};
PermissionCell.propTypes = {
title: PropTypes.oneOfType([
PropTypes.string.isRequired,
PropTypes.object.isRequired,
]),
description: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
weight: PropTypes.number,
avatarIcon: PropTypes.any.isRequired,
dateApproved: PropTypes.number,
revoked: PropTypes.bool,
};
export default PermissionCell;

View File

@ -0,0 +1,22 @@
import React from 'react';
import PermissionCell from '.';
export default {
title: 'Components/App/PermissionCell',
component: PermissionCell,
};
export const DefaultStory = (args) => <PermissionCell {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
title: 'Access the Ethereum provider.',
description:
'Allow the snap to communicate with MetaMask direct…blockchain and suggest messages and transactions.',
weight: 1,
avatarIcon: 'ethereum',
dateApproved: 1680185432326,
revoked: false,
};

View File

@ -0,0 +1,69 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/jest';
import PermissionCell from './permission-cell';
describe('Permission Cell', () => {
const mockPermissionData = {
label: 'Access the Ethereum provider.',
description:
'Allow the snap to communicate with MetaMask direct…blockchain and suggest messages and transactions.',
weight: 1,
leftIcon: 'ethereum',
permissionValue: {
date: 1680185432326,
},
permissionName: 'ethereum-provider',
};
it('renders approved permission cell', () => {
renderWithProvider(
<PermissionCell
title={mockPermissionData.label}
description={mockPermissionData.description}
weight={mockPermissionData.weight}
avatarIcon={mockPermissionData.leftIcon}
dateApproved={mockPermissionData?.permissionValue?.date}
key={`${mockPermissionData.permissionName}-${1}`}
/>,
);
expect(
screen.getByText('Access the Ethereum provider.'),
).toBeInTheDocument();
expect(screen.getByText('Approved on 2023-03-30')).toBeInTheDocument();
});
it('renders revoked permission cell', () => {
renderWithProvider(
<PermissionCell
title={mockPermissionData.label}
description={mockPermissionData.description}
weight={mockPermissionData.weight}
avatarIcon={mockPermissionData.leftIcon}
dateApproved={mockPermissionData?.permissionValue?.date}
key={`${mockPermissionData.permissionName}-${1}`}
revoked
/>,
);
expect(
screen.getByText('Access the Ethereum provider.'),
).toBeInTheDocument();
expect(screen.getByText('Revoked in this update')).toBeInTheDocument();
});
it('renders requested permission cell', () => {
renderWithProvider(
<PermissionCell
title={mockPermissionData.label}
description={mockPermissionData.description}
weight={mockPermissionData.weight}
avatarIcon={mockPermissionData.leftIcon}
key={`${mockPermissionData.permissionName}-${1}`}
/>,
);
expect(
screen.getByText('Access the Ethereum provider.'),
).toBeInTheDocument();
expect(screen.getByText('Requested now')).toBeInTheDocument();
});
});

View File

@ -1,37 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
getRightIcon,
getWeightedPermissions,
} from '../../../helpers/utils/permission';
import { getWeightedPermissions } from '../../../helpers/utils/permission';
import { useI18nContext } from '../../../hooks/useI18nContext';
/**
* Get one or more permission descriptions for a permission name.
*
* @param permission - The permission to render.
* @param index - The index of the permission.
* @returns {JSX.Element} A permission description node.
*/
function getDescriptionNode(permission, index) {
const { label, leftIcon, permissionName } = permission;
return (
<div className="permission" key={`${permissionName}-${index}`}>
{typeof leftIcon === 'string' ? <i className={leftIcon} /> : leftIcon}
{label}
{getRightIcon(permission)}
</div>
);
}
import PermissionCell from '../permission-cell';
import Box from '../../ui/box';
export default function PermissionsConnectPermissionList({ permissions }) {
const t = useI18nContext();
return (
<div className="permissions-connect-permission-list">
{getWeightedPermissions(t, permissions).map(getDescriptionNode)}
</div>
<Box paddingTop={2} paddingBottom={2}>
{getWeightedPermissions(t, permissions).map((permission, index) => {
return (
<PermissionCell
title={permission.label}
description={permission.description}
weight={permission.weight}
avatarIcon={permission.leftIcon}
dateApproved={permission?.permissionValue?.date}
key={`${permission.permissionName}-${index}`}
/>
);
})}
</Box>
);
}

View File

@ -15,21 +15,13 @@ import {
} from '../../../shared/constants/permissions';
import Tooltip from '../../components/ui/tooltip';
import {
AvatarIcon,
///: BEGIN:ONLY_INCLUDE_IN(flask)
Icon,
Text,
///: END:ONLY_INCLUDE_IN
ICON_NAMES,
ICON_SIZES,
} from '../../components/component-library';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import {
IconColor,
Color,
FONT_WEIGHT,
TextVariant,
} from '../constants/design-system';
import { Color, FONT_WEIGHT, TextVariant } from '../constants/design-system';
import {
coinTypeToProtocolName,
getSnapDerivationPathName,
@ -39,63 +31,29 @@ import {
const UNKNOWN_PERMISSION = Symbol('unknown');
///: BEGIN:ONLY_INCLUDE_IN(flask)
const RIGHT_WARNING_ICON = (
<Icon
name={ICON_NAMES.DANGER}
size={ICON_SIZES.SM}
color={IconColor.warningDefault}
/>
);
const RIGHT_INFO_ICON = (
<Icon
name={ICON_NAMES.INFO}
size={ICON_SIZES.SM}
color={IconColor.iconMuted}
/>
);
///: END:ONLY_INCLUDE_IN
function getLeftIcon(iconName) {
return (
<AvatarIcon
iconName={iconName}
size={ICON_SIZES.SM}
iconProps={{
size: ICON_SIZES.XS,
}}
/>
);
}
export const PERMISSION_DESCRIPTIONS = deepFreeze({
[RestrictedMethods.eth_accounts]: ({ t }) => ({
label: t('permission_ethereumAccounts'),
leftIcon: getLeftIcon(ICON_NAMES.EYE),
rightIcon: null,
leftIcon: ICON_NAMES.EYE,
weight: 2,
}),
///: BEGIN:ONLY_INCLUDE_IN(flask)
[RestrictedMethods.snap_confirm]: ({ t }) => ({
label: t('permission_customConfirmation'),
description: t('permission_customConfirmationDescription'),
leftIcon: getLeftIcon(ICON_NAMES.SECURITY_TICK),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.SECURITY_TICK,
weight: 3,
}),
[RestrictedMethods.snap_dialog]: ({ t }) => ({
label: t('permission_dialog'),
description: t('permission_dialogDescription'),
leftIcon: getLeftIcon(ICON_NAMES.MESSAGES),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.MESSAGES,
weight: 3,
}),
[RestrictedMethods.snap_notify]: ({ t }) => ({
label: t('permission_notifications'),
description: t('permission_notificationsDescription'),
leftIcon: getLeftIcon(ICON_NAMES.NOTIFICATION),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.NOTIFICATION,
weight: 3,
}),
[RestrictedMethods.snap_getBip32PublicKey]: ({
@ -105,8 +63,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
}) =>
permissionValue.caveats[0].value.map(({ path, curve }, i) => {
const baseDescription = {
leftIcon: getLeftIcon(ICON_NAMES.SECURITY_SEARCH),
rightIcon: RIGHT_WARNING_ICON,
leftIcon: ICON_NAMES.SECURITY_SEARCH,
weight: 1,
id: `public-key-access-bip32-${path
.join('-')
@ -176,8 +133,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
}) =>
permissionValue.caveats[0].value.map(({ path, curve }, i) => {
const baseDescription = {
leftIcon: getLeftIcon(ICON_NAMES.KEY),
rightIcon: RIGHT_WARNING_ICON,
leftIcon: ICON_NAMES.KEY,
weight: 1,
id: `key-access-bip32-${path
.join('-')
@ -261,8 +217,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
t('unrecognizedProtocol', [coinType])}
</span>,
]),
leftIcon: getLeftIcon(ICON_NAMES.KEY),
rightIcon: RIGHT_WARNING_ICON,
leftIcon: ICON_NAMES.KEY,
weight: 1,
id: `key-access-bip44-${coinType}-${i}`,
message: t('snapInstallWarningKeyAccess', [
@ -284,22 +239,19 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
[RestrictedMethods.snap_getEntropy]: ({ t }) => ({
label: t('permission_getEntropy'),
description: t('permission_getEntropyDescription'),
leftIcon: getLeftIcon(ICON_NAMES.SECURITY_KEY),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.SECURITY_KEY,
weight: 3,
}),
[RestrictedMethods.snap_manageState]: ({ t }) => ({
label: t('permission_manageState'),
description: t('permission_manageStateDescription'),
leftIcon: getLeftIcon(ICON_NAMES.ADD_SQUARE),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.ADD_SQUARE,
weight: 3,
}),
[RestrictedMethods.wallet_snap]: ({ t, permissionValue }) => {
const snaps = permissionValue.caveats[0].value;
const baseDescription = {
leftIcon: getLeftIcon(ICON_NAMES.FLASH),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.FLASH,
};
return Object.keys(snaps).map((snapId) => {
@ -326,8 +278,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
[EndowmentPermissions['endowment:network-access']]: ({ t }) => ({
label: t('permission_accessNetwork'),
description: t('permission_accessNetworkDescription'),
leftIcon: getLeftIcon(ICON_NAMES.GLOBAL),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.GLOBAL,
weight: 2,
}),
[EndowmentPermissions['endowment:webassembly']]: ({ t }) => ({
@ -340,8 +291,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
[EndowmentPermissions['endowment:long-running']]: ({ t }) => ({
label: t('permission_longRunning'),
description: t('permission_longRunningDescription'),
leftIcon: getLeftIcon(ICON_NAMES.LINK),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.LINK,
weight: 3,
}),
[EndowmentPermissions['endowment:transaction-insight']]: ({
@ -349,8 +299,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
permissionValue,
}) => {
const baseDescription = {
leftIcon: getLeftIcon(ICON_NAMES.SPEEDOMETER),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.SPEEDOMETER,
weight: 3,
};
@ -371,7 +320,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
...baseDescription,
label: t('permission_transactionInsightOrigin'),
description: t('permission_transactionInsightOriginDescription'),
leftIcon: getLeftIcon(ICON_NAMES.EXPLORE),
leftIcon: ICON_NAMES.EXPLORE,
});
}
@ -380,8 +329,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
[EndowmentPermissions['endowment:cronjob']]: ({ t }) => ({
label: t('permission_cronjob'),
description: t('permission_cronjobDescription'),
leftIcon: getLeftIcon(ICON_NAMES.CLOCK),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.CLOCK,
weight: 2,
}),
[EndowmentPermissions['endowment:ethereum-provider']]: ({
@ -390,16 +338,14 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
}) => ({
label: t('permission_ethereumProvider'),
description: t('permission_ethereumProviderDescription'),
leftIcon: getLeftIcon(ICON_NAMES.ETHEREUM),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.ETHEREUM,
weight: 1,
id: 'ethereum-provider-access',
message: t('ethereumProviderAccess', [targetSubjectMetadata?.origin]),
}),
[EndowmentPermissions['endowment:rpc']]: ({ t, permissionValue }) => {
const baseDescription = {
leftIcon: getLeftIcon(ICON_NAMES.HIERARCHY),
rightIcon: RIGHT_INFO_ICON,
leftIcon: ICON_NAMES.HIERARCHY,
weight: 2,
};
@ -427,8 +373,7 @@ export const PERMISSION_DESCRIPTIONS = deepFreeze({
///: END:ONLY_INCLUDE_IN
[UNKNOWN_PERMISSION]: ({ t, permissionName }) => ({
label: t('permission_unknown', [permissionName ?? 'undefined']),
leftIcon: getLeftIcon(ICON_NAMES.QUESTION),
rightIcon: null,
leftIcon: ICON_NAMES.QUESTION,
weight: 4,
}),
});

View File

@ -4,14 +4,6 @@
.headers {
flex: 1;
.permissions-connect-permission-list {
padding: 0 24px;
.permission {
padding: 8px 0;
}
}
.loader-container {
height: 100%;
}
@ -21,7 +13,6 @@
}
}
.page-container__footer {
width: 100%;
margin-top: 12px;

View File

@ -13,6 +13,7 @@ import {
TEXT_ALIGN,
FRACTIONS,
TextColor,
BLOCK_SIZES,
} from '../../../../helpers/constants/design-system';
import SnapAuthorship from '../../../../components/app/flask/snap-authorship';
import Box from '../../../../components/ui/box';
@ -176,7 +177,7 @@ function ViewSnap() {
>
{t('snapAccess', [snap.manifest.proposedName])}
</Typography>
<Box width={FRACTIONS.TEN_TWELFTHS}>
<Box width={BLOCK_SIZES.FULL}>
<PermissionsConnectPermissionList
permissions={permissions ?? {}}
/>