From 8603a4b06760618a63f2da0443ab770cc0dd878d Mon Sep 17 00:00:00 2001 From: David Drazic Date: Mon, 3 Apr 2023 19:33:54 +0200 Subject: [PATCH] [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 --- app/_locales/en/messages.json | 2 +- ui/components/app/app-components.scss | 1 + .../update-snap-permission-list.js | 99 +++++++------- ui/components/app/permission-cell/index.js | 1 + ui/components/app/permission-cell/index.scss | 5 + .../app/permission-cell/permission-cell.js | 126 ++++++++++++++++++ .../permission-cell.stories.js | 22 +++ .../permission-cell/permission-cell.test.js | 69 ++++++++++ .../permissions-connect-permission-list.js | 43 +++--- ui/helpers/utils/permission.js | 93 +++---------- .../flask/snap-install/index.scss | 9 -- .../settings/flask/view-snap/view-snap.js | 3 +- 12 files changed, 309 insertions(+), 164 deletions(-) create mode 100644 ui/components/app/permission-cell/index.js create mode 100644 ui/components/app/permission-cell/index.scss create mode 100644 ui/components/app/permission-cell/permission-cell.js create mode 100644 ui/components/app/permission-cell/permission-cell.stories.js create mode 100644 ui/components/app/permission-cell/permission-cell.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index abda8fecd..fb45872eb 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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": { diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 9659eaad3..196ab6c90 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -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'; diff --git a/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js b/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js index f787628e9..85f3373d4 100644 --- a/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js +++ b/ui/components/app/flask/update-snap-permission-list/update-snap-permission-list.js @@ -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 ( -
- -
- {label} - - {isFunction(subText) - ? subText(permissionName, permissionValue) - : subText} - -
- {getRightIcon(permission)} -
- ); - }); - }; - return ( -
- - { - const { date } = permissionValue; - const formattedDate = formatDate(date, 'yyyy-MM-dd'); - return t('approvedOn', [formattedDate]); - }} - /> - -
+ + {getWeightedPermissions(t, newPermissions).map((permission, index) => { + return ( + + ); + })} + {getWeightedPermissions(t, approvedPermissions).map( + (permission, index) => { + return ( + + ); + }, + )} + {getWeightedPermissions(t, revokedPermissions).map( + (permission, index) => { + return ( + + ); + }, + )} + ); } diff --git a/ui/components/app/permission-cell/index.js b/ui/components/app/permission-cell/index.js new file mode 100644 index 000000000..4972ccc53 --- /dev/null +++ b/ui/components/app/permission-cell/index.js @@ -0,0 +1 @@ +export { default } from './permission-cell'; diff --git a/ui/components/app/permission-cell/index.scss b/ui/components/app/permission-cell/index.scss new file mode 100644 index 000000000..95034015d --- /dev/null +++ b/ui/components/app/permission-cell/index.scss @@ -0,0 +1,5 @@ +.permission-cell { + &__title-revoked { + text-decoration: line-through; + } +} diff --git a/ui/components/app/permission-cell/permission-cell.js b/ui/components/app/permission-cell/permission-cell.js new file mode 100644 index 000000000..1090b3958 --- /dev/null +++ b/ui/components/app/permission-cell/permission-cell.js @@ -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 ( + + + {typeof avatarIcon === 'string' ? ( + + ) : ( + avatarIcon + )} + + + + {title} + + + {!revoked && + (dateApproved + ? t('approvedOn', [formatDate(dateApproved, 'yyyy-MM-dd')]) + : t('permissionRequested'))} + {revoked ? t('permissionRevoked') : ''} + + + + {description}} position="bottom"> + + + + + ); +}; + +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; diff --git a/ui/components/app/permission-cell/permission-cell.stories.js b/ui/components/app/permission-cell/permission-cell.stories.js new file mode 100644 index 000000000..986b9d326 --- /dev/null +++ b/ui/components/app/permission-cell/permission-cell.stories.js @@ -0,0 +1,22 @@ +import React from 'react'; +import PermissionCell from '.'; + +export default { + title: 'Components/App/PermissionCell', + + component: PermissionCell, +}; + +export const DefaultStory = (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, +}; diff --git a/ui/components/app/permission-cell/permission-cell.test.js b/ui/components/app/permission-cell/permission-cell.test.js new file mode 100644 index 000000000..f4567827f --- /dev/null +++ b/ui/components/app/permission-cell/permission-cell.test.js @@ -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( + , + ); + expect( + screen.getByText('Access the Ethereum provider.'), + ).toBeInTheDocument(); + expect(screen.getByText('Approved on 2023-03-30')).toBeInTheDocument(); + }); + + it('renders revoked permission cell', () => { + renderWithProvider( + , + ); + expect( + screen.getByText('Access the Ethereum provider.'), + ).toBeInTheDocument(); + expect(screen.getByText('Revoked in this update')).toBeInTheDocument(); + }); + + it('renders requested permission cell', () => { + renderWithProvider( + , + ); + expect( + screen.getByText('Access the Ethereum provider.'), + ).toBeInTheDocument(); + expect(screen.getByText('Requested now')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js index 99b554ec5..9e43d0b9d 100644 --- a/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js +++ b/ui/components/app/permissions-connect-permission-list/permissions-connect-permission-list.js @@ -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 ( -
- {typeof leftIcon === 'string' ? : leftIcon} - {label} - {getRightIcon(permission)} -
- ); -} +import PermissionCell from '../permission-cell'; +import Box from '../../ui/box'; export default function PermissionsConnectPermissionList({ permissions }) { const t = useI18nContext(); return ( -
- {getWeightedPermissions(t, permissions).map(getDescriptionNode)} -
+ + {getWeightedPermissions(t, permissions).map((permission, index) => { + return ( + + ); + })} + ); } diff --git a/ui/helpers/utils/permission.js b/ui/helpers/utils/permission.js index 6987b1d7f..2d54064aa 100644 --- a/ui/helpers/utils/permission.js +++ b/ui/helpers/utils/permission.js @@ -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 = ( - -); - -const RIGHT_INFO_ICON = ( - -); -///: END:ONLY_INCLUDE_IN - -function getLeftIcon(iconName) { - return ( - - ); -} - 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])} , ]), - 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, }), }); diff --git a/ui/pages/permissions-connect/flask/snap-install/index.scss b/ui/pages/permissions-connect/flask/snap-install/index.scss index dc725ce10..654653ab9 100644 --- a/ui/pages/permissions-connect/flask/snap-install/index.scss +++ b/ui/pages/permissions-connect/flask/snap-install/index.scss @@ -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; diff --git a/ui/pages/settings/flask/view-snap/view-snap.js b/ui/pages/settings/flask/view-snap/view-snap.js index 42ac59c8c..d8f5b6e93 100644 --- a/ui/pages/settings/flask/view-snap/view-snap.js +++ b/ui/pages/settings/flask/view-snap/view-snap.js @@ -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])} - +