diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5d9b0ba06..b7615c80d 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -366,6 +366,9 @@ "allowThisSiteTo": { "message": "Allow this site to:" }, + "allowThisSnapTo": { + "message": "Allow this snap to:" + }, "allowWithdrawAndSpend": { "message": "Allow $1 to withdraw and spend up to the following amount:", "description": "The url of the site that requested permission to 'withdraw and spend'" @@ -3560,6 +3563,9 @@ "message": "This revokes the permission for a third party to access and transfer all of your NFTs from $1 without further notice.", "description": "$1 is a link to contract on the block explorer when we're not able to retrieve a erc721 or erc1155 name" }, + "revokePermission": { + "message": "Revoke permission" + }, "revokeSpendingCap": { "message": "Revoke spending cap for your $1", "description": "$1 is a token symbol" @@ -3681,6 +3687,9 @@ "selectAccounts": { "message": "Select the account(s) to use on this site" }, + "selectAccountsForSnap": { + "message": "Select the account(s) to use with this snap" + }, "selectAll": { "message": "Select all" }, @@ -3770,10 +3779,6 @@ "settingsSearchMatchingNotFound": { "message": "No matching results found." }, - "shortVersion": { - "message": "v$1", - "description": "$1 is the version number to show" - }, "show": { "message": "Show" }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bfdbe7730..a33ee2754 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -2480,6 +2480,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:handleRequest', ), + revokeDynamicSnapPermissions: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:revokeDynamicPermissions', + ), dismissNotifications: this.dismissNotifications.bind(this), markNotificationsAsRead: this.markNotificationsAsRead.bind(this), ///: END:ONLY_INCLUDE_IN diff --git a/shared/constants/snaps/permissions.ts b/shared/constants/snaps/permissions.ts index 360c4a379..be3590ef4 100644 --- a/shared/constants/snaps/permissions.ts +++ b/shared/constants/snaps/permissions.ts @@ -29,3 +29,5 @@ export const ExcludedSnapEndowments = Object.freeze({ 'endowment:long-running is deprecated. For more information please see https://github.com/MetaMask/snaps-monorepo/issues/945.', ///: END:ONLY_INCLUDE_IN }); + +export const DynamicSnapPermissions = Object.freeze(['eth_accounts']); diff --git a/ui/components/app/permission-cell/permission-cell-options.js b/ui/components/app/permission-cell/permission-cell-options.js new file mode 100644 index 000000000..ec7dda4cd --- /dev/null +++ b/ui/components/app/permission-cell/permission-cell-options.js @@ -0,0 +1,101 @@ +import React, { useState, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch } from 'react-redux'; +import Box from '../../ui/box'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { IconName, ButtonIcon, Text } from '../../component-library'; +import { Menu, MenuItem } from '../../ui/menu'; +import { + TextColor, + TextVariant, +} from '../../../helpers/constants/design-system'; +import Popover from '../../ui/popover/popover.component'; +import { DynamicSnapPermissions } from '../../../../shared/constants/snaps/permissions'; +import { revokeDynamicSnapPermissions } from '../../../store/actions'; + +export const PermissionCellOptions = ({ + snapId, + permissionName, + description, +}) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const ref = useRef(false); + const [showOptions, setShowOptions] = useState(false); + const [showDetails, setShowDetails] = useState(false); + + const isRevokable = DynamicSnapPermissions.includes(permissionName); + + const handleOpen = () => { + setShowOptions(true); + }; + + const handleClose = () => { + setShowOptions(false); + }; + + const handleDetailsOpen = () => { + setShowOptions(false); + setShowDetails(true); + }; + + const handleDetailsClose = () => { + setShowOptions(false); + setShowDetails(false); + }; + + const handleRevokePermission = () => { + setShowOptions(false); + dispatch(revokeDynamicSnapPermissions(snapId, [permissionName])); + }; + + return ( + + + {showOptions && ( + + + + {t('details')} + + + {isRevokable && ( + + + {t('revokePermission')} + + + )} + + )} + {showDetails && ( + + + {description} + + + )} + + ); +}; + +PermissionCellOptions.propTypes = { + snapId: PropTypes.string.isRequired, + permissionName: PropTypes.string.isRequired, + description: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), +}; diff --git a/ui/components/app/permission-cell/permission-cell.js b/ui/components/app/permission-cell/permission-cell.js index e325e3000..c5fca8fa8 100644 --- a/ui/components/app/permission-cell/permission-cell.js +++ b/ui/components/app/permission-cell/permission-cell.js @@ -21,14 +21,18 @@ import { import { formatDate } from '../../../helpers/utils/util'; import { useI18nContext } from '../../../hooks/useI18nContext'; import Tooltip from '../../ui/tooltip'; +import { PermissionCellOptions } from './permission-cell-options'; const PermissionCell = ({ + snapId, + permissionName, title, description, weight, avatarIcon, dateApproved, revoked, + showOptions, }) => { const t = useI18nContext(); @@ -107,15 +111,25 @@ const PermissionCell = ({ - {description}} position="bottom"> - - + {showOptions && snapId ? ( + + ) : ( + {description}} position="bottom"> + + + )} ); }; PermissionCell.propTypes = { + snapId: PropTypes.string, + permissionName: PropTypes.string.isRequired, title: PropTypes.oneOfType([ PropTypes.string.isRequired, PropTypes.object.isRequired, @@ -125,6 +139,7 @@ PermissionCell.propTypes = { avatarIcon: PropTypes.any.isRequired, dateApproved: PropTypes.number, revoked: PropTypes.bool, + showOptions: PropTypes.bool, }; export default PermissionCell; diff --git a/ui/components/app/permission-cell/permission-cell.test.js b/ui/components/app/permission-cell/permission-cell.test.js index f4567827f..1d7dd2ab5 100644 --- a/ui/components/app/permission-cell/permission-cell.test.js +++ b/ui/components/app/permission-cell/permission-cell.test.js @@ -19,6 +19,7 @@ describe('Permission Cell', () => { it('renders approved permission cell', () => { renderWithProvider( { it('renders revoked permission cell', () => { renderWithProvider( { it('renders requested permission cell', () => { renderWithProvider(
@@ -109,12 +128,9 @@ export default class PermissionPageContainerContent extends PureComponent { iconUrl={subjectMetadata.iconUrl} iconName={subjectMetadata.name} headerTitle={title} - headerText={ - subjectMetadata.extensionId - ? t('allowExternalExtensionTo', [subjectMetadata.extensionId]) - : t('allowThisSiteTo') - } + headerText={headerText} siteOrigin={subjectMetadata.origin} + subjectType={subjectMetadata.subjectType} />
{this.renderRequestedPermissions()} diff --git a/ui/components/app/permission-page-container/permission-page-container.component.js b/ui/components/app/permission-page-container/permission-page-container.component.js index a352d5b36..1f817edf8 100644 --- a/ui/components/app/permission-page-container/permission-page-container.component.js +++ b/ui/components/app/permission-page-container/permission-page-container.component.js @@ -7,6 +7,7 @@ import { WALLET_SNAP_PERMISSION_KEY, } from '@metamask/rpc-methods'; ///: END:ONLY_INCLUDE_IN +import { SubjectType } from '@metamask/permission-controller'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { PageContainerFooter } from '../../ui/page-container'; import PermissionsConnectFooter from '../permissions-connect-footer'; @@ -189,7 +190,7 @@ export default class PermissionPageContainer extends Component { ///: END:ONLY_INCLUDE_IN return ( -
+ <> { ///: BEGIN:ONLY_INCLUDE_IN(snaps) <> @@ -210,7 +211,9 @@ export default class PermissionPageContainer extends Component { allIdentitiesSelected={allIdentitiesSelected} />
- + {targetSubjectMetadata?.subjectType !== SubjectType.Snap && ( + + )} this.onCancel()} @@ -220,7 +223,7 @@ export default class PermissionPageContainer extends Component { buttonSizeLarge={false} />
-
+ ); } } diff --git a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js index 5f3fc5366..725db0fc4 100644 --- a/ui/components/app/permissions-connect-header/permissions-connect-header.component.js +++ b/ui/components/app/permissions-connect-header/permissions-connect-header.component.js @@ -1,6 +1,9 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classnames from 'classnames'; +///: BEGIN:ONLY_INCLUDE_IN(snaps) +import { SubjectType } from '@metamask/subject-metadata-controller'; +///: END:ONLY_INCLUDE_IN import SiteOrigin from '../../ui/site-origin'; import Box from '../../ui/box'; import { @@ -19,6 +22,7 @@ export default class PermissionsConnectHeader extends Component { headerText: PropTypes.string, leftIcon: PropTypes.node, rightIcon: PropTypes.node, + subjectType: PropTypes.string, }; static defaultProps = { @@ -29,7 +33,23 @@ export default class PermissionsConnectHeader extends Component { }; renderHeaderIcon() { - const { iconUrl, iconName, siteOrigin, leftIcon, rightIcon } = this.props; + const { + iconUrl, + iconName, + siteOrigin, + leftIcon, + rightIcon, + ///: BEGIN:ONLY_INCLUDE_IN(snaps) + subjectType, + ///: END:ONLY_INCLUDE_IN + } = this.props; + + ///: BEGIN:ONLY_INCLUDE_IN(snaps) + + if (subjectType === SubjectType.Snap) { + return null; + } + ///: END:ONLY_INCLUDE_IN return (
diff --git a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js index 74bf2adab..4f99c2162 100644 --- a/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js +++ b/ui/components/app/snaps/snap-authorship-header/snap-authorship-header.js @@ -24,7 +24,11 @@ import { getTargetSubjectMetadata } from '../../../../selectors'; import SnapAvatar from '../snap-avatar'; import SnapVersion from '../snap-version/snap-version'; -const SnapAuthorshipHeader = ({ snapId, className }) => { +const SnapAuthorshipHeader = ({ + snapId, + className, + boxShadow = 'var(--shadow-size-lg) var(--color-shadow-default)', +}) => { // We're using optional chaining with snapId, because with the current implementation // of snap update in the snap controller, we do not have reference to snapId when an // update request is rejected because the reference comes from the request itself and not subject metadata @@ -51,7 +55,7 @@ const SnapAuthorshipHeader = ({ snapId, className }) => { display={DISPLAY.FLEX} padding={4} style={{ - boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)', + boxShadow, }} > @@ -91,6 +95,7 @@ SnapAuthorshipHeader.propTypes = { * The className of the SnapAuthorship */ className: PropTypes.string, + boxShadow: PropTypes.string, }; export default SnapAuthorshipHeader; diff --git a/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js b/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js index 6dda11d04..27ab6e3d2 100644 --- a/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js +++ b/ui/components/app/snaps/snap-permissions-list/snap-permissions-list.js @@ -6,8 +6,10 @@ import PermissionCell from '../../permission-cell'; import Box from '../../../ui/box'; export default function SnapPermissionsList({ + snapId, permissions, targetSubjectMetadata, + showOptions, }) { const t = useI18nContext(); @@ -17,12 +19,15 @@ export default function SnapPermissionsList({ (permission, index) => { return ( ); }, @@ -32,6 +37,8 @@ export default function SnapPermissionsList({ } SnapPermissionsList.propTypes = { + snapId: PropTypes.string.isRequired, permissions: PropTypes.object.isRequired, targetSubjectMetadata: PropTypes.object.isRequired, + showOptions: PropTypes.bool, }; diff --git a/ui/components/app/snaps/snap-version/snap-version.js b/ui/components/app/snaps/snap-version/snap-version.js index a04e9449f..f70653540 100644 --- a/ui/components/app/snaps/snap-version/snap-version.js +++ b/ui/components/app/snaps/snap-version/snap-version.js @@ -18,10 +18,8 @@ import { Text, } from '../../../component-library'; import Preloader from '../../../ui/icon/preloader/preloader-icon.component'; -import { useI18nContext } from '../../../../hooks/useI18nContext'; const SnapVersion = ({ version, url }) => { - const t = useI18nContext(); return (