1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Sort permissions based on weight (#17660)

This commit is contained in:
Maarten Zuidhoorn 2023-02-08 15:28:48 +01:00 committed by GitHub
parent b5c1de900d
commit ff85b040cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 124 deletions

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getPermissionDescription } from '../../../../helpers/utils/permission'; import { isFunction } from 'lodash';
import { getWeightedPermissions } from '../../../../helpers/utils/permission';
import { useI18nContext } from '../../../../hooks/useI18nContext'; import { useI18nContext } from '../../../../hooks/useI18nContext';
import { formatDate } from '../../../../helpers/utils/util'; import { formatDate } from '../../../../helpers/utils/util';
import Typography from '../../../ui/typography/typography'; import Typography from '../../../ui/typography/typography';
@ -13,97 +14,50 @@ export default function UpdateSnapPermissionList({
}) { }) {
const t = useI18nContext(); const t = useI18nContext();
const ApprovedPermissions = () => { const Permissions = ({ className, permissions, subText }) => {
return Object.entries(approvedPermissions).map( return getWeightedPermissions(t, permissions).map(
([permissionName, permissionValue]) => { ({ label, rightIcon, permissionName, permissionValue }) => (
const permissions = getPermissionDescription( <div className={className} key={permissionName}>
t, <i className="fas fa-x" />
permissionName, <div className="permission-description">
permissionValue, {label}
); <Typography
const { date } = permissionValue; color={TextColor.textAlternative}
const formattedDate = formatDate(date, 'yyyy-MM-dd'); boxProps={{ paddingTop: 1 }}
return permissions.map(({ label, rightIcon }) => ( className="permission-description-subtext"
<div className="approved-permission" key={permissionName}> >
<i className="fas fa-check" /> {isFunction(subText)
<div className="permission-description"> ? subText(permissionName, permissionValue)
{label} : subText}
<Typography </Typography>
color={TextColor.textAlternative}
className="permission-description-subtext"
boxProps={{ paddingTop: 1 }}
>
{t('approvedOn', [formattedDate])}
</Typography>
</div>
{rightIcon && <i className={rightIcon} />}
</div> </div>
)); {rightIcon && <i className={rightIcon} />}
}, </div>
); ),
};
const RevokedPermissions = () => {
return Object.entries(revokedPermissions).map(
([permissionName, permissionValue]) => {
const permissions = getPermissionDescription(
t,
permissionName,
permissionValue,
);
return permissions.map(({ label, rightIcon }) => (
<div className="revoked-permission" key={permissionName}>
<i className="fas fa-x" />
<div className="permission-description">
{label}
<Typography
color={TextColor.textAlternative}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{t('permissionRevoked')}
</Typography>
</div>
{rightIcon && <i className={rightIcon} />}
</div>
));
},
);
};
const NewPermissions = () => {
return Object.entries(newPermissions).map(
([permissionName, permissionValue]) => {
const permissions = getPermissionDescription(
t,
permissionName,
permissionValue,
);
return permissions.map(({ label, rightIcon }) => (
<div className="new-permission" key={permissionName}>
<i className="fas fa-arrow-right" />
<div className="permission-description">
{label}
<Typography
color={TextColor.textAlternative}
boxProps={{ paddingTop: 1 }}
className="permission-description-subtext"
>
{t('permissionRequested')}
</Typography>
</div>
{rightIcon && <i className={rightIcon} />}
</div>
));
},
); );
}; };
return ( return (
<div className="update-snap-permission-list"> <div className="update-snap-permission-list">
<NewPermissions /> <Permissions
<ApprovedPermissions /> className="new-permission"
<RevokedPermissions /> 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> </div>
); );
} }

View File

@ -0,0 +1,51 @@
import React from 'react';
import UpdateSnapPermissionList from './update-snap-permission-list';
export default {
title: 'Components/App/UpdateSnapPermissionList',
component: UpdateSnapPermissionList,
argTypes: {
permissions: {
control: 'object',
},
},
};
export const DefaultStory = (args) => <UpdateSnapPermissionList {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
approvedPermissions: {
'endowment:network-access': {
date: 1620710693178,
},
snap_getBip32PublicKey: {
date: 1620710693178,
caveats: [
{
value: [
{
path: ['m', `44'`, `0'`],
curve: 'secp256k1',
},
],
},
],
},
},
revokedPermissions: {
snap_notify: {
date: 1620710693178,
},
eth_accounts: {
date: 1620710693178,
},
},
newPermissions: {
snap_dialog: {
date: 1620710693178,
},
},
};

View File

@ -1,30 +1,30 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getPermissionDescription } from '../../../helpers/utils/permission'; import { getWeightedPermissions } from '../../../helpers/utils/permission';
import { useI18nContext } from '../../../hooks/useI18nContext'; import { useI18nContext } from '../../../hooks/useI18nContext';
/** /**
* Get one or more permission descriptions for a permission name. * Get one or more permission descriptions for a permission name.
* *
* @param t - The translation function. * @param permission - The permission to render.
* @param permissionName - The name of the permission to request. * @param permission.label - The text label.
* @param permissionValue - The value of the permission to request. * @param permission.leftIcon - The left icon.
* @param permission.rightIcon - The right icon.
* @param permission.permissionName - The name of the permission.
* @param index - The index of the permission in the permissions array.
* @returns {JSX.Element[]} An array of permission description nodes. * @returns {JSX.Element[]} An array of permission description nodes.
*/ */
function getDescriptionNodes(t, permissionName, permissionValue) { function getDescriptionNode(
const permissions = getPermissionDescription( { label, leftIcon, rightIcon, permissionName },
t, index,
permissionName, ) {
permissionValue, return (
);
return permissions.map(({ label, leftIcon, rightIcon }, index) => (
<div className="permission" key={`${permissionName}-${index}`}> <div className="permission" key={`${permissionName}-${index}`}>
<i className={leftIcon} /> <i className={leftIcon} />
{label} {label}
{rightIcon && <i className={rightIcon} />} {rightIcon && <i className={rightIcon} />}
</div> </div>
)); );
} }
export default function PermissionsConnectPermissionList({ permissions }) { export default function PermissionsConnectPermissionList({ permissions }) {
@ -32,13 +32,7 @@ export default function PermissionsConnectPermissionList({ permissions }) {
return ( return (
<div className="permissions-connect-permission-list"> <div className="permissions-connect-permission-list">
{Object.entries(permissions).reduce( {getWeightedPermissions(t, permissions).map(getDescriptionNode)}
(target, [permissionName, permissionValue]) =>
target.concat(
getDescriptionNodes(t, permissionName, permissionValue),
),
[],
)}
</div> </div>
); );
} }

View File

@ -19,6 +19,19 @@ DefaultStory.storyName = 'Default';
DefaultStory.args = { DefaultStory.args = {
permissions: { permissions: {
eth_accounts: true, eth_accounts: {},
snap_dialog: {},
snap_getBip32PublicKey: {
caveats: [
{
value: [
{
path: ['m', `44'`, `0'`],
curve: 'secp256k1',
},
],
},
],
},
}, },
}; };

View File

@ -24,75 +24,87 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
label: t('permission_ethereumAccounts'), label: t('permission_ethereumAccounts'),
leftIcon: 'fas fa-eye', leftIcon: 'fas fa-eye',
rightIcon: null, rightIcon: null,
weight: 2,
}), }),
///: BEGIN:ONLY_INCLUDE_IN(flask) ///: BEGIN:ONLY_INCLUDE_IN(flask)
[RestrictedMethods.snap_confirm]: (t) => ({ [RestrictedMethods.snap_confirm]: (t) => ({
label: t('permission_customConfirmation'), label: t('permission_customConfirmation'),
leftIcon: 'fas fa-user-check', leftIcon: 'fas fa-user-check',
rightIcon: null, rightIcon: null,
weight: 3,
}), }),
[RestrictedMethods.snap_dialog]: (t) => ({ [RestrictedMethods.snap_dialog]: (t) => ({
label: t('permission_dialog'), label: t('permission_dialog'),
leftIcon: 'fas fa-user-check', leftIcon: 'fas fa-user-check',
rightIcon: null, rightIcon: null,
weight: 3,
}), }),
[RestrictedMethods.snap_notify]: (t) => ({ [RestrictedMethods.snap_notify]: (t) => ({
leftIcon: 'fas fa-bell', leftIcon: 'fas fa-bell',
label: t('permission_notifications'), label: t('permission_notifications'),
rightIcon: null, rightIcon: null,
weight: 3,
}), }),
[RestrictedMethods.snap_getBip32PublicKey]: (t, _, permissionValue) => [RestrictedMethods.snap_getBip32PublicKey]: (t, _, permissionValue) =>
permissionValue.caveats[0].value.map(({ path, curve }) => { permissionValue.caveats[0].value.map(({ path, curve }) => {
const baseDescription = {
leftIcon: 'fas fa-eye',
rightIcon: null,
weight: 1,
};
const friendlyName = getSnapDerivationPathName(path, curve); const friendlyName = getSnapDerivationPathName(path, curve);
if (friendlyName) { if (friendlyName) {
return { return {
...baseDescription,
label: t('permission_viewNamedBip32PublicKeys', [ label: t('permission_viewNamedBip32PublicKeys', [
<span className="permission-label-item" key={path.join('/')}> <span className="permission-label-item" key={path.join('/')}>
{friendlyName} {friendlyName}
</span>, </span>,
path.join('/'), path.join('/'),
]), ]),
leftIcon: 'fas fa-eye',
rightIcon: null,
}; };
} }
return { return {
...baseDescription,
label: t('permission_viewBip32PublicKeys', [ label: t('permission_viewBip32PublicKeys', [
<span className="permission-label-item" key={path.join('/')}> <span className="permission-label-item" key={path.join('/')}>
{path.join('/')} {path.join('/')}
</span>, </span>,
curve, curve,
]), ]),
leftIcon: 'fas fa-eye',
rightIcon: null,
}; };
}), }),
[RestrictedMethods.snap_getBip32Entropy]: (t, _, permissionValue) => [RestrictedMethods.snap_getBip32Entropy]: (t, _, permissionValue) =>
permissionValue.caveats[0].value.map(({ path, curve }) => { permissionValue.caveats[0].value.map(({ path, curve }) => {
const baseDescription = {
leftIcon: 'fas fa-door-open',
rightIcon: null,
weight: 1,
};
const friendlyName = getSnapDerivationPathName(path, curve); const friendlyName = getSnapDerivationPathName(path, curve);
if (friendlyName) { if (friendlyName) {
return { return {
...baseDescription,
label: t('permission_manageNamedBip32Keys', [ label: t('permission_manageNamedBip32Keys', [
<span className="permission-label-item" key={path.join('/')}> <span className="permission-label-item" key={path.join('/')}>
{friendlyName} {friendlyName}
</span>, </span>,
path.join('/'), path.join('/'),
]), ]),
leftIcon: 'fas fa-door-open',
rightIcon: null,
}; };
} }
return { return {
...baseDescription,
label: t('permission_manageBip32Keys', [ label: t('permission_manageBip32Keys', [
<span className="permission-label-item" key={path.join('/')}> <span className="permission-label-item" key={path.join('/')}>
{path.join('/')} {path.join('/')}
</span>, </span>,
curve, curve,
]), ]),
leftIcon: 'fas fa-door-open',
rightIcon: null,
}; };
}), }),
[RestrictedMethods.snap_getBip44Entropy]: (t, _, permissionValue) => [RestrictedMethods.snap_getBip44Entropy]: (t, _, permissionValue) =>
@ -105,59 +117,71 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
]), ]),
leftIcon: 'fas fa-door-open', leftIcon: 'fas fa-door-open',
rightIcon: null, rightIcon: null,
weight: 1,
})), })),
[RestrictedMethods.snap_getEntropy]: (t) => ({ [RestrictedMethods.snap_getEntropy]: (t) => ({
label: t('permission_getEntropy'), label: t('permission_getEntropy'),
leftIcon: 'fas fa-key', leftIcon: 'fas fa-key',
rightIcon: null, rightIcon: null,
weight: 3,
}), }),
[RestrictedMethods.snap_manageState]: (t) => ({ [RestrictedMethods.snap_manageState]: (t) => ({
label: t('permission_manageState'), label: t('permission_manageState'),
leftIcon: 'fas fa-download', leftIcon: 'fas fa-download',
rightIcon: null, rightIcon: null,
weight: 3,
}), }),
[RestrictedMethods['wallet_snap_*']]: (t, permissionName) => { [RestrictedMethods['wallet_snap_*']]: (t, permissionName) => {
const baseDescription = {
leftIcon: 'fas fa-bolt',
rightIcon: null,
};
const snapId = permissionName.split('_').slice(-1); const snapId = permissionName.split('_').slice(-1);
const friendlyName = SNAPS_METADATA[snapId]?.name; const friendlyName = SNAPS_METADATA[snapId]?.name;
if (friendlyName) { if (friendlyName) {
return { return {
...baseDescription,
label: t('permission_accessNamedSnap', [ label: t('permission_accessNamedSnap', [
<span className="permission-label-item" key={snapId}> <span className="permission-label-item" key={snapId}>
{friendlyName} {friendlyName}
</span>, </span>,
]), ]),
leftIcon: 'fas fa-bolt',
rightIcon: null,
}; };
} }
return { return {
...baseDescription,
label: t('permission_accessSnap', [snapId]), label: t('permission_accessSnap', [snapId]),
leftIcon: 'fas fa-bolt',
rightIcon: null,
}; };
}, },
[EndowmentPermissions['endowment:network-access']]: (t) => ({ [EndowmentPermissions['endowment:network-access']]: (t) => ({
label: t('permission_accessNetwork'), label: t('permission_accessNetwork'),
leftIcon: 'fas fa-wifi', leftIcon: 'fas fa-wifi',
rightIcon: null, rightIcon: null,
weight: 2,
}), }),
[EndowmentPermissions['endowment:long-running']]: (t) => ({ [EndowmentPermissions['endowment:long-running']]: (t) => ({
label: t('permission_longRunning'), label: t('permission_longRunning'),
leftIcon: 'fas fa-infinity', leftIcon: 'fas fa-infinity',
rightIcon: null, rightIcon: null,
weight: 3,
}), }),
[EndowmentPermissions['endowment:transaction-insight']]: ( [EndowmentPermissions['endowment:transaction-insight']]: (
t, t,
_, _,
permissionValue, permissionValue,
) => { ) => {
const baseDescription = {
leftIcon: 'fas fa-info',
rightIcon: null,
};
const result = [ const result = [
{ {
...baseDescription,
label: t('permission_transactionInsight'), label: t('permission_transactionInsight'),
leftIcon: 'fas fa-info',
rightIcon: null,
}, },
]; ];
@ -167,22 +191,25 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
permissionValue.caveats[0].value permissionValue.caveats[0].value
) { ) {
result.push({ result.push({
...baseDescription,
label: t('permission_transactionInsightOrigin'), label: t('permission_transactionInsightOrigin'),
leftIcon: 'fas fa-compass', leftIcon: 'fas fa-compass',
rightIcon: null,
}); });
} }
return result; return result;
}, },
[EndowmentPermissions['endowment:cronjob']]: (t) => ({ [EndowmentPermissions['endowment:cronjob']]: (t) => ({
label: t('permission_cronjob'), label: t('permission_cronjob'),
leftIcon: 'fas fa-clock', leftIcon: 'fas fa-clock',
rightIcon: null, rightIcon: null,
weight: 2,
}), }),
[EndowmentPermissions['endowment:ethereum-provider']]: (t) => ({ [EndowmentPermissions['endowment:ethereum-provider']]: (t) => ({
label: t('permission_ethereumProvider'), label: t('permission_ethereumProvider'),
leftIcon: 'fab fa-ethereum', leftIcon: 'fab fa-ethereum',
rightIcon: null, rightIcon: null,
weight: 1,
}), }),
[EndowmentPermissions['endowment:rpc']]: (t, _, permissionValue) => { [EndowmentPermissions['endowment:rpc']]: (t, _, permissionValue) => {
const { snaps, dapps } = getRpcCaveatOrigins(permissionValue); const { snaps, dapps } = getRpcCaveatOrigins(permissionValue);
@ -200,6 +227,7 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
label, label,
leftIcon: 'fas fa-plug', leftIcon: 'fas fa-plug',
rightIcon: null, rightIcon: null,
weight: 2,
})); }));
}, },
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
@ -207,6 +235,7 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
label: t('permission_unknown', [permissionName ?? 'undefined']), label: t('permission_unknown', [permissionName ?? 'undefined']),
leftIcon: 'fas fa-times-circle', leftIcon: 'fas fa-times-circle',
rightIcon: null, rightIcon: null,
weight: 4,
}), }),
}); });
@ -215,6 +244,9 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
* @property {string} label - The text label. * @property {string} label - The text label.
* @property {string} leftIcon - The left icon. * @property {string} leftIcon - The left icon.
* @property {string} rightIcon - The right icon. * @property {string} rightIcon - The right icon.
* @property {number} weight - The weight of the permission.
* @property {string} permissionName - The name of the permission.
* @property {string} permissionValue - The raw value of the permission.
*/ */
/** /**
@ -243,7 +275,32 @@ export const getPermissionDescription = (
const result = value(t, permissionName, permissionValue); const result = value(t, permissionName, permissionValue);
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
return [result]; return [{ ...result, permissionName, permissionValue }];
} }
return result;
return result.map((item) => ({
...item,
permissionName,
permissionValue,
}));
}; };
/**
* Get the weighted permissions from a permissions object. The weight is used to
* sort the permissions in the UI.
*
* @param {Function} t - The translation function
* @param {object} permissions - The permissions object.
* @returns {PermissionLabelObject[]}
*/
export function getWeightedPermissions(t, permissions) {
return Object.entries(permissions)
.reduce(
(target, [permissionName, permissionValue]) =>
target.concat(
getPermissionDescription(t, permissionName, permissionValue),
),
[],
)
.sort((left, right) => left.weight - right.weight);
}