From ff36e32fb053828c5917af72e3fb3ea3ac028e84 Mon Sep 17 00:00:00 2001 From: Hassan Malik <41640681+hmalik88@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:36:38 -0400 Subject: [PATCH] [FLASK] Improve snaps connect flow (#19461) * add todo comments * add snaps-connect component * added new messages * added component scss files to main scss files * remove dead code and add snap-connect-cell * update snaps connect * updated messages and styling * update messages and css * update css * moved snaps privacy warning into snaps connect, moved snaps connect error into snap install * added story and removed unused import * fix style linting and move snaps connect error css * removed unused message * ran lavamoat policy generation * fix fencing * some more css changes * Fix scrolling and box shadow * added comment, fixed quote * Align more with Figma * Regen LavaMoat policies * bring back privacy logic to permission page container * Revert scrolling changes + fix snaps icon * fix linting, reintroduced dedupe logic and additionally addressed a corner case * made some fixes * Fix scrolling with multiple snaps * add dedupe logic to snaps connect and fix spacing issue * policy regen * lint fix * fix fencing * replaced with new icon design, trimmed origin urls in certain places * remove unused imports * badge icon size * Revert LM policy changes * Use SnapAvatar for snaps-connect * Use InstallError for connection failed * Delete unused CSS file * Remove unused CSS * Use useOriginMetadata * addressed PR comments * fix linting errors * add explicit condition * fix fencing * fix some more fencing * fix util fencing issue * fix storybook file, prevent null destructuring * Fix storybook origin URLs * Fix wrong prop name --------- Co-authored-by: Frederik Bolding Co-authored-by: Guillaume Roux Co-authored-by: Erik Nilsson --- app/_locales/en/messages.json | 22 ++ .../permission-page-container.component.js | 35 +-- .../app/snaps/install-error/install-error.js | 10 +- .../app/snaps/snap-avatar/snap-avatar.js | 22 +- .../app/snaps/snap-connect-cell/index.js | 1 + .../snap-connect-cell/snap-connect-cell.js | 74 ++++++ .../snap-connect-cell.stories.js | 16 ++ ui/helpers/constants/routes.ts | 2 + ui/helpers/utils/util.js | 21 ++ ui/pages/permissions-connect/index.scss | 1 + .../permissions-connect.component.js | 40 +++ .../permissions-connect.container.js | 23 +- .../snaps/snap-install/snap-install.js | 50 +++- .../snaps/snaps-connect/index.js | 1 + .../snaps/snaps-connect/index.scss | 9 + .../snaps/snaps-connect/snaps-connect.js | 251 ++++++++++++++++++ .../snaps-connect/snaps-connect.stories.js | 102 +++++++ 17 files changed, 639 insertions(+), 41 deletions(-) create mode 100644 ui/components/app/snaps/snap-connect-cell/index.js create mode 100644 ui/components/app/snaps/snap-connect-cell/snap-connect-cell.js create mode 100644 ui/components/app/snaps/snap-connect-cell/snap-connect-cell.stories.js create mode 100644 ui/pages/permissions-connect/snaps/snaps-connect/index.js create mode 100644 ui/pages/permissions-connect/snaps/snaps-connect/index.scss create mode 100644 ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js create mode 100644 ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.stories.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6f50c0a49..fdbc6875e 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -739,6 +739,10 @@ "connectManually": { "message": "Manually connect to current site" }, + "connectSnap": { + "message": "Connect $1", + "description": "$1 is the snap for which a connection is being requested." + }, "connectTo": { "message": "Connect to $1", "description": "$1 is the name/origin of a web3 site/application that the user can connect to metamask" @@ -804,6 +808,16 @@ "connectionError": { "message": "Connection error" }, + "connectionFailed": { + "message": "Connection failed" + }, + "connectionFailedDescription": { + "message": "Fetching of $1 failed, check your network and try again.", + "description": "$1 is the name of the snap being fetched." + }, + "connectionRequest": { + "message": "Connection request" + }, "contactUs": { "message": "Contact us" }, @@ -2265,6 +2279,10 @@ "moreComingSoon": { "message": "More coming soon..." }, + "multipleSnapConnectionWarning": { + "message": "$1 wants to connect with $2 snaps. Only proceed if you trust this website.", + "description": "$1 is the dapp and $2 is the number of snaps it wants to connect to." + }, "mustSelectOne": { "message": "Must select at least 1 token." }, @@ -3764,6 +3782,10 @@ "smartTransaction": { "message": "Smart transaction" }, + "snapConnectionWarning": { + "message": "$1 wants to connect to $2. Only continue if you trust this website.", + "description": "$2 is the snap and $1 is the dapp requesting connection to the snap." + }, "snapContent": { "message": "This content is coming from $1", "description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap." 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 a30825c09..a352d5b36 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 @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { isEqual } from 'lodash'; ///: BEGIN:ONLY_INCLUDE_IN(snaps) -import { isObject } from '@metamask/utils'; import { SnapCaveatType, WALLET_SNAP_PERMISSION_KEY, @@ -14,6 +13,7 @@ import PermissionsConnectFooter from '../permissions-connect-footer'; ///: BEGIN:ONLY_INCLUDE_IN(snaps) import { RestrictedMethods } from '../../../../shared/constants/permissions'; import SnapPrivacyWarning from '../snaps/snap-privacy-warning'; +import { getDedupedSnaps } from '../../../helpers/utils/util'; ///: END:ONLY_INCLUDE_IN import { PermissionPageContainerContent } from '.'; @@ -86,29 +86,20 @@ export default class PermissionPageContainer extends Component { ///: BEGIN:ONLY_INCLUDE_IN(snaps) getDedupedSnapPermissions() { - const permission = - this.props.request.permissions[WALLET_SNAP_PERMISSION_KEY]; - const requestedSnaps = permission?.caveats[0].value; - const currentSnaps = - this.props.currentPermissions[WALLET_SNAP_PERMISSION_KEY]?.caveats[0] - .value; - - if (!isObject(currentSnaps)) { - return permission; - } - - const requestedSnapKeys = requestedSnaps ? Object.keys(requestedSnaps) : []; - const currentSnapKeys = currentSnaps ? Object.keys(currentSnaps) : []; - const dedupedCaveats = requestedSnapKeys.reduce((acc, snapId) => { - if (!currentSnapKeys.includes(snapId)) { - acc[snapId] = {}; - } - return acc; - }, {}); - + const { request, currentPermissions } = this.props; + const snapKeys = getDedupedSnaps(request, currentPermissions); + const permission = request?.permissions?.[WALLET_SNAP_PERMISSION_KEY] || {}; return { ...permission, - caveats: [{ type: SnapCaveatType.SnapIds, value: dedupedCaveats }], + caveats: [ + { + type: SnapCaveatType.SnapIds, + value: snapKeys.reduce((caveatValue, snapId) => { + caveatValue[snapId] = {}; + return caveatValue; + }, {}), + }, + ], }; } diff --git a/ui/components/app/snaps/install-error/install-error.js b/ui/components/app/snaps/install-error/install-error.js index 02b051ea7..dad100bda 100644 --- a/ui/components/app/snaps/install-error/install-error.js +++ b/ui/components/app/snaps/install-error/install-error.js @@ -40,16 +40,18 @@ const InstallError = ({ title, error, description, iconName }) => { {title} {description && {description}} - - - + {error && ( + + + + )} ); }; InstallError.propTypes = { title: PropTypes.node.isRequired, - error: PropTypes.string.isRequired, + error: PropTypes.string, description: PropTypes.string, iconName: PropTypes.string, }; diff --git a/ui/components/app/snaps/snap-avatar/snap-avatar.js b/ui/components/app/snaps/snap-avatar/snap-avatar.js index f59d4bbcb..f0e85d62e 100644 --- a/ui/components/app/snaps/snap-avatar/snap-avatar.js +++ b/ui/components/app/snaps/snap-avatar/snap-avatar.js @@ -8,7 +8,6 @@ import { AlignItems, DISPLAY, JustifyContent, - Size, BackgroundColor, } from '../../../../helpers/constants/design-system'; import { getSnapName } from '../../../../helpers/utils/util'; @@ -23,7 +22,13 @@ import { } from '../../../component-library'; import { getTargetSubjectMetadata } from '../../../../selectors'; -const SnapAvatar = ({ snapId, className }) => { +const SnapAvatar = ({ + snapId, + badgeSize = IconSize.Sm, + avatarSize = IconSize.Lg, + borderWidth = 2, + className, +}) => { const subjectMetadata = useSelector((state) => getTargetSubjectMetadata(state, snapId), ); @@ -40,12 +45,12 @@ const SnapAvatar = ({ snapId, className }) => { badge={ @@ -53,10 +58,10 @@ const SnapAvatar = ({ snapId, className }) => { position={BadgeWrapperPosition.bottomRight} > {iconUrl ? ( - + ) : ( + getTargetSubjectMetadata(state, snapId), + ); + const friendlyName = getSnapName(snapId, snapMetadata); + + return ( + + + + + {t('connectSnap', [ + + {friendlyName} + , + ])} + + + + + {t('snapConnectionWarning', [ + {origin}, + {friendlyName}, + ])} + + } + position="bottom" + > + + + + + ); +} + +SnapConnectCell.propTypes = { + origin: PropTypes.string.isRequired, + snapId: PropTypes.string.isRequired, +}; diff --git a/ui/components/app/snaps/snap-connect-cell/snap-connect-cell.stories.js b/ui/components/app/snaps/snap-connect-cell/snap-connect-cell.stories.js new file mode 100644 index 000000000..b1007f91f --- /dev/null +++ b/ui/components/app/snaps/snap-connect-cell/snap-connect-cell.stories.js @@ -0,0 +1,16 @@ +import React from 'react'; +import SnapConnectCell from '.'; + +export default { + title: 'Components/App/Snaps/SnapConnectCell', + component: SnapConnectCell, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + origin: 'aave.com', + snapId: 'npm:@metamask/example-snap', +}; diff --git a/ui/helpers/constants/routes.ts b/ui/helpers/constants/routes.ts index 59eba786e..5d0f1c2f3 100644 --- a/ui/helpers/constants/routes.ts +++ b/ui/helpers/constants/routes.ts @@ -44,6 +44,7 @@ const TOKEN_DETAILS = '/token-details'; const CONNECT_ROUTE = '/connect'; const CONNECT_CONFIRM_PERMISSIONS_ROUTE = '/confirm-permissions'; ///: BEGIN:ONLY_INCLUDE_IN(snaps) +const CONNECT_SNAPS_CONNECT_ROUTE = '/snaps-connect'; const CONNECT_SNAP_INSTALL_ROUTE = '/snap-install'; const CONNECT_SNAP_UPDATE_ROUTE = '/snap-update'; const CONNECT_SNAP_RESULT_ROUTE = '/snap-install-result'; @@ -238,6 +239,7 @@ export { CONNECT_ROUTE, CONNECT_CONFIRM_PERMISSIONS_ROUTE, ///: BEGIN:ONLY_INCLUDE_IN(snaps) + CONNECT_SNAPS_CONNECT_ROUTE, CONNECT_SNAP_INSTALL_ROUTE, CONNECT_SNAP_UPDATE_ROUTE, CONNECT_SNAP_RESULT_ROUTE, diff --git a/ui/helpers/utils/util.js b/ui/helpers/utils/util.js index 00bb738b5..58e52cef4 100644 --- a/ui/helpers/utils/util.js +++ b/ui/helpers/utils/util.js @@ -9,6 +9,8 @@ import * as lodash from 'lodash'; import bowser from 'bowser'; ///: BEGIN:ONLY_INCLUDE_IN(snaps) import { getSnapPrefix } from '@metamask/snaps-utils'; +import { WALLET_SNAP_PERMISSION_KEY } from '@metamask/rpc-methods'; +import { isObject } from '@metamask/utils'; ///: END:ONLY_INCLUDE_IN import { CHAIN_IDS, NETWORK_TYPES } from '../../../shared/constants/network'; import { @@ -566,6 +568,25 @@ export const getSnapName = (snapId, subjectMetadata) => { return subjectMetadata?.name ?? removeSnapIdPrefix(snapId); }; +export const getDedupedSnaps = (request, permissions) => { + const permission = request?.permissions?.[WALLET_SNAP_PERMISSION_KEY]; + const requestedSnaps = permission?.caveats[0].value; + const currentSnaps = + permissions?.[WALLET_SNAP_PERMISSION_KEY]?.caveats[0].value; + + if (!isObject(currentSnaps) && requestedSnaps) { + return Object.keys(requestedSnaps); + } + + const requestedSnapKeys = requestedSnaps ? Object.keys(requestedSnaps) : []; + const currentSnapKeys = currentSnaps ? Object.keys(currentSnaps) : []; + const dedupedSnaps = requestedSnapKeys.filter( + (snapId) => !currentSnapKeys.includes(snapId), + ); + + return dedupedSnaps.length > 0 ? dedupedSnaps : requestedSnapKeys; +}; + ///: END:ONLY_INCLUDE_IN /** diff --git a/ui/pages/permissions-connect/index.scss b/ui/pages/permissions-connect/index.scss index 1d31864d0..d8762d80f 100644 --- a/ui/pages/permissions-connect/index.scss +++ b/ui/pages/permissions-connect/index.scss @@ -2,6 +2,7 @@ @import 'snaps/snap-install/index'; @import 'snaps/snap-update/index'; @import 'snaps/snap-result/index'; +@import 'snaps/snaps-connect/index'; @import 'redirect/index'; .permissions-connect { diff --git a/ui/pages/permissions-connect/permissions-connect.component.js b/ui/pages/permissions-connect/permissions-connect.component.js index 0c7fe45b5..da06d002b 100644 --- a/ui/pages/permissions-connect/permissions-connect.component.js +++ b/ui/pages/permissions-connect/permissions-connect.component.js @@ -13,6 +13,7 @@ import { Icon, IconName, IconSize } from '../../components/component-library'; import ChooseAccount from './choose-account'; import PermissionsRedirect from './redirect'; ///: BEGIN:ONLY_INCLUDE_IN(snaps) +import SnapsConnect from './snaps/snaps-connect'; import SnapInstall from './snaps/snap-install'; import SnapUpdate from './snaps/snap-update'; import SnapResult from './snaps/snap-result'; @@ -40,6 +41,7 @@ export default class PermissionConnect extends Component { confirmPermissionPath: PropTypes.string.isRequired, requestType: PropTypes.string.isRequired, ///: BEGIN:ONLY_INCLUDE_IN(snaps) + snapsConnectPath: PropTypes.string.isRequired, snapInstallPath: PropTypes.string.isRequired, snapUpdatePath: PropTypes.string.isRequired, snapResultPath: PropTypes.string.isRequired, @@ -105,6 +107,7 @@ export default class PermissionConnect extends Component { connectPath, confirmPermissionPath, ///: BEGIN:ONLY_INCLUDE_IN(snaps) + snapsConnectPath, snapInstallPath, snapUpdatePath, snapResultPath, @@ -140,6 +143,9 @@ export default class PermissionConnect extends Component { case 'wallet_installSnapResult': history.replace(snapResultPath); break; + case 'wallet_connectSnaps': + history.replace(snapsConnectPath); + break; default: ///: END:ONLY_INCLUDE_IN history.replace(confirmPermissionPath); @@ -183,6 +189,7 @@ export default class PermissionConnect extends Component { confirmPermissionPath, requestType, ///: BEGIN:ONLY_INCLUDE_IN(snaps) + snapsConnectPath, snapInstallPath, snapUpdatePath, snapResultPath, @@ -204,6 +211,9 @@ export default class PermissionConnect extends Component { case 'wallet_installSnapResult': this.props.history.push(snapResultPath); break; + case 'wallet_connectSnaps': + this.props.history.replace(snapsConnectPath); + break; ///: END:ONLY_INCLUDE_IN default: this.props.history.push(confirmPermissionPath); @@ -299,6 +309,7 @@ export default class PermissionConnect extends Component { confirmPermissionPath, hideTopBar, ///: BEGIN:ONLY_INCLUDE_IN(snaps) + snapsConnectPath, snapInstallPath, snapUpdatePath, snapResultPath, @@ -381,6 +392,35 @@ export default class PermissionConnect extends Component { { ///: BEGIN:ONLY_INCLUDE_IN(snaps) } + ( + { + approvePermissionsRequest(...args); + this.redirect(true); + }} + rejectConnection={(requestId) => + this.cancelPermissionsRequest(requestId) + } + targetSubjectMetadata={targetSubjectMetadata} + snapsInstallPrivacyWarningShown={ + snapsInstallPrivacyWarningShown + } + setSnapsInstallPrivacyWarningShownStatus={ + setSnapsInstallPrivacyWarningShownStatus + } + /> + )} + /> + { + ///: END:ONLY_INCLUDE_IN + } + { + ///: BEGIN:ONLY_INCLUDE_IN(snaps) + } { subjectType: SubjectType.Unknown, }; - const requestType = getRequestType(state, permissionsRequestId); + let requestType = getRequestType(state, permissionsRequestId); ///: BEGIN:ONLY_INCLUDE_IN(snaps) - const requestState = getRequestState(state, permissionsRequestId); + // We want to only assign the wallet_connectSnaps request type (i.e. only show + // SnapsConnect) if and only if we get a singular wallet_snap permission request. + // Any other request gets pushed to the normal permission connect flow. + if ( + permissionsRequest && + Object.keys(permissionsRequest.permissions || {}).length === 1 && + permissionsRequest.permissions?.[WALLET_SNAP_PERMISSION_KEY] + ) { + requestType = 'wallet_connectSnaps'; + } + + const requestState = getRequestState(state, permissionsRequestId) || {}; ///: END:ONLY_INCLUDE_IN const accountsWithLabels = getAccountsWithLabels(state); @@ -96,6 +111,7 @@ const mapStateToProps = (state, ownProps) => { const connectPath = `${CONNECT_ROUTE}/${permissionsRequestId}`; const confirmPermissionPath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_CONFIRM_PERMISSIONS_ROUTE}`; ///: BEGIN:ONLY_INCLUDE_IN(snaps) + const snapsConnectPath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_SNAPS_CONNECT_ROUTE}`; const snapInstallPath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_SNAP_INSTALL_ROUTE}`; const snapUpdatePath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_SNAP_UPDATE_ROUTE}`; const snapResultPath = `${CONNECT_ROUTE}/${permissionsRequestId}${CONNECT_SNAP_RESULT_ROUTE}`; @@ -119,6 +135,8 @@ const mapStateToProps = (state, ownProps) => { ///: BEGIN:ONLY_INCLUDE_IN(snaps) } else if (isSnapInstallOrUpdateOrResult) { page = isRequestingAccounts ? '3' : '2'; + } else if (pathname === snapsConnectPath) { + page = 1; ///: END:ONLY_INCLUDE_IN } else { throw new Error('Incorrect path for permissions-connect component'); @@ -128,6 +146,7 @@ const mapStateToProps = (state, ownProps) => { isRequestingAccounts, requestType, ///: BEGIN:ONLY_INCLUDE_IN(snaps) + snapsConnectPath, snapInstallPath, snapUpdatePath, snapResultPath, diff --git a/ui/pages/permissions-connect/snaps/snap-install/snap-install.js b/ui/pages/permissions-connect/snaps/snap-install/snap-install.js index e9135c745..cdf48bf00 100644 --- a/ui/pages/permissions-connect/snaps/snap-install/snap-install.js +++ b/ui/pages/permissions-connect/snaps/snap-install/snap-install.js @@ -18,7 +18,6 @@ import { } from '../../../../helpers/constants/design-system'; import { getSnapInstallWarnings } from '../util'; import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader'; -import InstallError from '../../../../components/app/snaps/install-error/install-error'; import SnapAuthorshipHeader from '../../../../components/app/snaps/snap-authorship-header'; import { AvatarIcon, @@ -29,6 +28,9 @@ import { import { getSnapName } from '../../../../helpers/utils/util'; import SnapPermissionsList from '../../../../components/app/snaps/snap-permissions-list'; import { useScrollRequired } from '../../../../hooks/useScrollRequired'; +import SiteOrigin from '../../../../components/ui/site-origin/site-origin'; +import InstallError from '../../../../components/app/snaps/install-error/install-error'; +import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; export default function SnapInstall({ request, @@ -38,7 +40,8 @@ export default function SnapInstall({ targetSubjectMetadata, }) { const t = useI18nContext(); - + const siteMetadata = useOriginMetadata(request?.metadata?.dappOrigin) || {}; + const { origin, iconUrl, name } = siteMetadata; const [isShowingWarning, setIsShowingWarning] = useState(false); const { isScrollable, isScrolledToBottom, scrollToBottom, ref, onScroll } = @@ -78,6 +81,15 @@ export default function SnapInstall({ } }; + const getFooterMessage = () => { + if (hasError) { + return 'ok'; + } else if (isLoading) { + return 'connect'; + } + return 'install'; + }; + return ( - + {isLoading || hasError ? ( + + + + ) : ( + + )} {isLoading && ( @@ -107,7 +136,16 @@ export default function SnapInstall({ )} {hasError && ( - + + {snapName} + , + ])} + error={requestState.error} + /> )} {!hasError && !isLoading && ( <> @@ -172,7 +210,7 @@ export default function SnapInstall({ onCancel={onCancel} cancelText={t('cancel')} onSubmit={handleSubmit} - submitText={t(hasError ? 'ok' : 'install')} + submitText={t(getFooterMessage())} /> {isShowingWarning && ( diff --git a/ui/pages/permissions-connect/snaps/snaps-connect/index.js b/ui/pages/permissions-connect/snaps/snaps-connect/index.js new file mode 100644 index 000000000..1cf40e395 --- /dev/null +++ b/ui/pages/permissions-connect/snaps/snaps-connect/index.js @@ -0,0 +1 @@ +export { default } from './snaps-connect'; diff --git a/ui/pages/permissions-connect/snaps/snaps-connect/index.scss b/ui/pages/permissions-connect/snaps/snaps-connect/index.scss new file mode 100644 index 000000000..d632aedc6 --- /dev/null +++ b/ui/pages/permissions-connect/snaps/snaps-connect/index.scss @@ -0,0 +1,9 @@ +.snaps-connect { + overflow-y: hidden; + + .page-container__footer { + border-top: 0; + margin-top: auto; + box-shadow: var(--shadow-size-lg) var(--color-shadow-default); + } +} diff --git a/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js b/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js new file mode 100644 index 000000000..7a7329be9 --- /dev/null +++ b/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.js @@ -0,0 +1,251 @@ +import React, { useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import PropTypes from 'prop-types'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import Box from '../../../../components/ui/box'; +import SiteOrigin from '../../../../components/ui/site-origin'; +import { + IconSize, + Text, + ValidTag, +} from '../../../../components/component-library'; +import { + FlexDirection, + TextVariant, + JustifyContent, + AlignItems, + TextAlign, + Display, + FontWeight, + BlockSize, +} from '../../../../helpers/constants/design-system'; +import { PageContainerFooter } from '../../../../components/ui/page-container'; +import SnapConnectCell from '../../../../components/app/snaps/snap-connect-cell/snap-connect-cell'; +import { getDedupedSnaps, getSnapName } from '../../../../helpers/utils/util'; +import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader'; +import SnapPrivacyWarning from '../../../../components/app/snaps/snap-privacy-warning/snap-privacy-warning'; +import { + getPermissions, + getTargetSubjectMetadata, +} from '../../../../selectors'; +import SnapAvatar from '../../../../components/app/snaps/snap-avatar/snap-avatar'; +import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; + +export default function SnapsConnect({ + request, + approveConnection, + rejectConnection, + targetSubjectMetadata, + snapsInstallPrivacyWarningShown, + setSnapsInstallPrivacyWarningShownStatus, +}) { + const t = useI18nContext(); + const { origin, iconUrl, name } = targetSubjectMetadata; + const [isLoading, setIsLoading] = useState(false); + const [isShowingSnapsPrivacyWarning, setIsShowingSnapsPrivacyWarning] = + useState(!snapsInstallPrivacyWarningShown); + const currentPermissions = useSelector((state) => + getPermissions(state, request?.metadata?.origin), + ); + + const onCancel = useCallback(() => { + rejectConnection(request.metadata.id); + }, [request, rejectConnection]); + + const onConnect = useCallback(() => { + try { + setIsLoading(true); + approveConnection(request); + } finally { + setIsLoading(false); + } + }, [request, approveConnection]); + + const snaps = getDedupedSnaps(request, currentPermissions); + + const singularConnectSnapMetadata = useSelector((state) => + getTargetSubjectMetadata(state, snaps?.[0]), + ); + + const SnapsConnectContent = () => { + const { hostname: trimmedOrigin } = useOriginMetadata(origin) || {}; + if (isLoading) { + return ( + + + + ); + } + if (snaps?.length > 1) { + return ( + + + {t('connectionRequest')} + + + {t('multipleSnapConnectionWarning', [ + + {trimmedOrigin} + , + + {snaps?.length} + , + ])} + + + {snaps.map((snap) => ( + // TODO(hbmalik88): add in the iconUrl prop when we have access to a snap's icons pre-installation + + ))} + + + ); + } else if (snaps?.length === 1) { + const snapId = snaps[0]; + const snapName = getSnapName(snapId, singularConnectSnapMetadata); + return ( + + + + + + {t('connectionRequest')} + + + {t('snapConnectionWarning', [ + + {trimmedOrigin} + , + + {snapName} + , + ])} + + + ); + } + return null; + }; + + return ( + + {isShowingSnapsPrivacyWarning && ( + { + setIsShowingSnapsPrivacyWarning(false); + setSnapsInstallPrivacyWarningShownStatus(true); + }} + onCanceled={onCancel} + /> + )} + + + + + + + ); +} + +SnapsConnect.propTypes = { + request: PropTypes.object.isRequired, + approveConnection: PropTypes.func.isRequired, + rejectConnection: PropTypes.func.isRequired, + targetSubjectMetadata: PropTypes.shape({ + extensionId: PropTypes.string, + iconUrl: PropTypes.string, + name: PropTypes.string, + origin: PropTypes.string, + subjectType: PropTypes.string, + }), + snapsInstallPrivacyWarningShown: PropTypes.bool.isRequired, + setSnapsInstallPrivacyWarningShownStatus: PropTypes.func, +}; diff --git a/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.stories.js b/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.stories.js new file mode 100644 index 000000000..bad657abb --- /dev/null +++ b/ui/pages/permissions-connect/snaps/snaps-connect/snaps-connect.stories.js @@ -0,0 +1,102 @@ +import React from 'react'; +import { Provider } from 'react-redux'; + +import configureStore from '../../../../store/store'; +import mockState from '../../../../../test/data/mock-state.json'; +import SnapsConnect from '.'; + +const store = configureStore(mockState); + +export default { + title: 'Pages/Snaps/SnapConnect', + + component: SnapsConnect, + argTypes: {}, + decorators: [(story) => {story()}], +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; + +DefaultStory.args = { + request: { + metadata: { + id: 'foo', + }, + permissions: { + wallet_snap: { + caveats: [ + { + value: { + 'npm:@metamask/test-snap-bip44': {}, + }, + }, + ], + }, + }, + }, + targetSubjectMetadata: { + origin: 'https://metamask.io', + }, +}; + +export const MultiStory = (args) => ; + +MultiStory.storyName = 'Multi'; + +MultiStory.args = { + request: { + metadata: { + id: 'foo', + }, + permissions: { + wallet_snap: { + caveats: [ + { + value: { + 'npm:@metamask/test-snap-bip44': {}, + 'npm:@metamask/test-snap-bip32': {}, + 'npm:@metamask/test-snap-getEntropy': {}, + }, + }, + ], + }, + }, + }, + targetSubjectMetadata: { + origin: 'https://metamask.io', + }, +}; + +export const ScrollingStory = (args) => ; + +ScrollingStory.storyName = 'Scrolling'; + +ScrollingStory.args = { + request: { + metadata: { + id: 'foo', + }, + permissions: { + wallet_snap: { + caveats: [ + { + value: { + 'npm:@metamask/test-snap-bip44': {}, + 'npm:@metamask/test-snap-bip32': {}, + 'npm:@metamask/test-snap-getEntropy': {}, + 'npm:@metamask/test-snap-networkAccess': {}, + 'npm:@metamask/test-snap-wasm': {}, + 'npm:@metamask/test-snap-notify': {}, + 'npm:@metamask/test-snap-dialog': {}, + }, + }, + ], + }, + }, + }, + targetSubjectMetadata: { + origin: 'https://metamask.io', + }, +};