mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
[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 <frederik.bolding@gmail.com> Co-authored-by: Guillaume Roux <guillaumeroux123@gmail.com> Co-authored-by: Erik Nilsson <eriks@mail.se>
This commit is contained in:
parent
95d57b254f
commit
ff36e32fb0
22
app/_locales/en/messages.json
generated
22
app/_locales/en/messages.json
generated
@ -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."
|
||||
|
@ -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;
|
||||
}, {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -40,16 +40,18 @@ const InstallError = ({ title, error, description, iconName }) => {
|
||||
{title}
|
||||
</Text>
|
||||
{description && <Text textAlign={TextAlign.Center}>{description}</Text>}
|
||||
<Box padding={2}>
|
||||
<ActionableMessage type="danger" message={error} />
|
||||
</Box>
|
||||
{error && (
|
||||
<Box padding={2}>
|
||||
<ActionableMessage type="danger" message={error} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
InstallError.propTypes = {
|
||||
title: PropTypes.node.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
iconName: PropTypes.string,
|
||||
};
|
||||
|
@ -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={
|
||||
<AvatarIcon
|
||||
iconName={IconName.Snaps}
|
||||
size={IconSize.Sm}
|
||||
size={badgeSize}
|
||||
backgroundColor={IconColor.infoDefault}
|
||||
borderColor={BackgroundColor.backgroundDefault}
|
||||
borderWidth={2}
|
||||
borderWidth={borderWidth}
|
||||
iconProps={{
|
||||
size: IconSize.Sm,
|
||||
size: badgeSize,
|
||||
color: IconColor.infoInverse,
|
||||
}}
|
||||
/>
|
||||
@ -53,10 +58,10 @@ const SnapAvatar = ({ snapId, className }) => {
|
||||
position={BadgeWrapperPosition.bottomRight}
|
||||
>
|
||||
{iconUrl ? (
|
||||
<AvatarFavicon size={Size.LG} src={iconUrl} name={friendlyName} />
|
||||
<AvatarFavicon size={avatarSize} src={iconUrl} name={friendlyName} />
|
||||
) : (
|
||||
<AvatarBase
|
||||
size={Size.LG}
|
||||
size={avatarSize}
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={AlignItems.center}
|
||||
justifyContent={JustifyContent.center}
|
||||
@ -75,6 +80,9 @@ SnapAvatar.propTypes = {
|
||||
* The id of the snap
|
||||
*/
|
||||
snapId: PropTypes.string,
|
||||
badgeSize: PropTypes.string,
|
||||
avatarSize: PropTypes.string,
|
||||
borderWidth: PropTypes.number,
|
||||
/**
|
||||
* The className of the SnapAvatar
|
||||
*/
|
||||
|
1
ui/components/app/snaps/snap-connect-cell/index.js
Normal file
1
ui/components/app/snaps/snap-connect-cell/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './snap-connect-cell';
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Box from '../../../ui/box';
|
||||
import {
|
||||
IconColor,
|
||||
AlignItems,
|
||||
Display,
|
||||
FontWeight,
|
||||
} from '../../../../helpers/constants/design-system';
|
||||
import { getSnapName } from '../../../../helpers/utils/util';
|
||||
import {
|
||||
Icon,
|
||||
IconName,
|
||||
IconSize,
|
||||
Text,
|
||||
ValidTag,
|
||||
} from '../../../component-library';
|
||||
import Tooltip from '../../../ui/tooltip/tooltip';
|
||||
import { useI18nContext } from '../../../../hooks/useI18nContext';
|
||||
import SnapAvatar from '../snap-avatar/snap-avatar';
|
||||
import { getTargetSubjectMetadata } from '../../../../selectors';
|
||||
|
||||
export default function SnapConnectCell({ origin, snapId }) {
|
||||
const t = useI18nContext();
|
||||
const snapMetadata = useSelector((state) =>
|
||||
getTargetSubjectMetadata(state, snapId),
|
||||
);
|
||||
const friendlyName = getSnapName(snapId, snapMetadata);
|
||||
|
||||
return (
|
||||
<Box
|
||||
alignItems={AlignItems.center}
|
||||
paddingTop={2}
|
||||
paddingBottom={2}
|
||||
display={Display.Flex}
|
||||
>
|
||||
<SnapAvatar snapId={snapId} />
|
||||
<Box width="full" marginLeft={4} marginRight={4}>
|
||||
<Text>
|
||||
{t('connectSnap', [
|
||||
<Text as={ValidTag.Span} key="1" fontWeight={FontWeight.Bold}>
|
||||
{friendlyName}
|
||||
</Text>,
|
||||
])}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Tooltip
|
||||
html={
|
||||
<div>
|
||||
{t('snapConnectionWarning', [
|
||||
<b key="0">{origin}</b>,
|
||||
<b key="1">{friendlyName}</b>,
|
||||
])}
|
||||
</div>
|
||||
}
|
||||
position="bottom"
|
||||
>
|
||||
<Icon
|
||||
color={IconColor.iconMuted}
|
||||
name={IconName.Info}
|
||||
size={IconSize.Sm}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
SnapConnectCell.propTypes = {
|
||||
origin: PropTypes.string.isRequired,
|
||||
snapId: PropTypes.string.isRequired,
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import SnapConnectCell from '.';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/Snaps/SnapConnectCell',
|
||||
component: SnapConnectCell,
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <SnapConnectCell {...args} />;
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
origin: 'aave.com',
|
||||
snapId: 'npm:@metamask/example-snap',
|
||||
};
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
<Route
|
||||
path={snapsConnectPath}
|
||||
exact
|
||||
render={() => (
|
||||
<SnapsConnect
|
||||
request={permissionsRequest || {}}
|
||||
approveConnection={(...args) => {
|
||||
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)
|
||||
}
|
||||
<Route
|
||||
path={snapInstallPath}
|
||||
exact
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { SubjectType } from '@metamask/subject-metadata-controller';
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
import { WALLET_SNAP_PERMISSION_KEY } from '@metamask/rpc-methods';
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
@ -32,6 +35,7 @@ import {
|
||||
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,
|
||||
@ -75,10 +79,21 @@ const mapStateToProps = (state, ownProps) => {
|
||||
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,
|
||||
|
@ -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 (
|
||||
<Box
|
||||
className="page-container snap-install"
|
||||
@ -86,14 +98,31 @@ export default function SnapInstall({
|
||||
borderStyle={BorderStyle.none}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
|
||||
{isLoading || hasError ? (
|
||||
<Box
|
||||
width="full"
|
||||
alignItems={AlignItems.center}
|
||||
justifyContent={JustifyContent.center}
|
||||
paddingTop={4}
|
||||
>
|
||||
<SiteOrigin
|
||||
chip
|
||||
siteOrigin={origin}
|
||||
title={origin}
|
||||
iconSrc={iconUrl}
|
||||
iconName={name}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
|
||||
)}
|
||||
<Box
|
||||
ref={ref}
|
||||
onScroll={onScroll}
|
||||
className="snap-install__content"
|
||||
style={{
|
||||
overflowY: 'auto',
|
||||
flex: !isLoading && '1',
|
||||
flex: !isLoading && !hasError && '1',
|
||||
}}
|
||||
>
|
||||
{isLoading && (
|
||||
@ -107,7 +136,16 @@ export default function SnapInstall({
|
||||
</Box>
|
||||
)}
|
||||
{hasError && (
|
||||
<InstallError error={requestState.error} title={t('requestFailed')} />
|
||||
<InstallError
|
||||
iconName={IconName.Warning}
|
||||
title={t('connectionFailed')}
|
||||
description={t('connectionFailedDescription', [
|
||||
<Text as={ValidTag.Span} key="1" fontWeight={FontWeight.Medium}>
|
||||
{snapName}
|
||||
</Text>,
|
||||
])}
|
||||
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())}
|
||||
/>
|
||||
</Box>
|
||||
{isShowingWarning && (
|
||||
|
@ -0,0 +1 @@
|
||||
export { default } from './snaps-connect';
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<Box
|
||||
className="snap-connect__loader-container"
|
||||
flexDirection={FlexDirection.Column}
|
||||
alignItems={AlignItems.center}
|
||||
justifyContent={JustifyContent.center}
|
||||
>
|
||||
<PulseLoader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
if (snaps?.length > 1) {
|
||||
return (
|
||||
<Box
|
||||
className="snaps-connect__content"
|
||||
flexDirection={FlexDirection.Column}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
paddingLeft={4}
|
||||
paddingRight={4}
|
||||
paddingTop={8}
|
||||
width={BlockSize.Full}
|
||||
style={{ overflowY: 'hidden' }}
|
||||
>
|
||||
<Text paddingBottom={2} variant={TextVariant.headingLg}>
|
||||
{t('connectionRequest')}
|
||||
</Text>
|
||||
<Text variant={TextVariant.bodyMd} textAlign={TextAlign.Center}>
|
||||
{t('multipleSnapConnectionWarning', [
|
||||
<Text
|
||||
as={ValidTag.Span}
|
||||
key="1"
|
||||
variant={TextVariant.bodyMd}
|
||||
fontWeight={FontWeight.Medium}
|
||||
>
|
||||
{trimmedOrigin}
|
||||
</Text>,
|
||||
<Text
|
||||
as={ValidTag.Span}
|
||||
key="2"
|
||||
variant={TextVariant.bodyMd}
|
||||
fontWeight={FontWeight.Medium}
|
||||
>
|
||||
{snaps?.length}
|
||||
</Text>,
|
||||
])}
|
||||
</Text>
|
||||
<Box
|
||||
className="snaps-connect__content__snaps-list"
|
||||
flexDirection={FlexDirection.Column}
|
||||
display={Display.Flex}
|
||||
marginTop={4}
|
||||
width={BlockSize.Full}
|
||||
style={{ overflowY: 'auto', flex: 1 }}
|
||||
>
|
||||
{snaps.map((snap) => (
|
||||
// TODO(hbmalik88): add in the iconUrl prop when we have access to a snap's icons pre-installation
|
||||
<SnapConnectCell
|
||||
key={`snaps-connect-${snap}`}
|
||||
snapId={snap}
|
||||
origin={trimmedOrigin}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
} else if (snaps?.length === 1) {
|
||||
const snapId = snaps[0];
|
||||
const snapName = getSnapName(snapId, singularConnectSnapMetadata);
|
||||
return (
|
||||
<Box
|
||||
className="snaps-connect__content"
|
||||
flexDirection={FlexDirection.Column}
|
||||
justifyContent={JustifyContent.center}
|
||||
alignItems={AlignItems.center}
|
||||
height={BlockSize.Full}
|
||||
paddingLeft={4}
|
||||
paddingRight={4}
|
||||
>
|
||||
<Box paddingBottom={2}>
|
||||
<SnapAvatar
|
||||
snapId={snaps[0]}
|
||||
badgeSize={IconSize.Md}
|
||||
avatarSize={IconSize.Xl}
|
||||
borderWidth={3}
|
||||
/>
|
||||
</Box>
|
||||
<Text paddingBottom={2} variant={TextVariant.headingLg}>
|
||||
{t('connectionRequest')}
|
||||
</Text>
|
||||
<Text
|
||||
variant={TextVariant.bodyMd}
|
||||
textAlign={TextAlign.Center}
|
||||
padding={[0, 4]}
|
||||
>
|
||||
{t('snapConnectionWarning', [
|
||||
<Text
|
||||
as={ValidTag.Span}
|
||||
key="1"
|
||||
variant={TextVariant.bodyMd}
|
||||
fontWeight={FontWeight.Medium}
|
||||
>
|
||||
{trimmedOrigin}
|
||||
</Text>,
|
||||
<Text
|
||||
as={ValidTag.Span}
|
||||
key="2"
|
||||
variant={TextVariant.bodyMd}
|
||||
fontWeight={FontWeight.Medium}
|
||||
>
|
||||
{snapName}
|
||||
</Text>,
|
||||
])}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="page-container snaps-connect"
|
||||
flexDirection={FlexDirection.Column}
|
||||
alignItems={AlignItems.center}
|
||||
>
|
||||
{isShowingSnapsPrivacyWarning && (
|
||||
<SnapPrivacyWarning
|
||||
onAccepted={() => {
|
||||
setIsShowingSnapsPrivacyWarning(false);
|
||||
setSnapsInstallPrivacyWarningShownStatus(true);
|
||||
}}
|
||||
onCanceled={onCancel}
|
||||
/>
|
||||
)}
|
||||
<Box
|
||||
className="snaps-connect__header"
|
||||
flexDirection={FlexDirection.Column}
|
||||
alignItems={AlignItems.center}
|
||||
paddingLeft={4}
|
||||
paddingRight={4}
|
||||
>
|
||||
<SiteOrigin
|
||||
chip
|
||||
siteOrigin={origin}
|
||||
title={origin}
|
||||
iconSrc={iconUrl}
|
||||
iconName={name}
|
||||
/>
|
||||
</Box>
|
||||
<SnapsConnectContent />
|
||||
<PageContainerFooter
|
||||
footerClassName="snaps-connect__footer"
|
||||
cancelButtonType="default"
|
||||
hideCancel={false}
|
||||
disabled={isLoading}
|
||||
onCancel={onCancel}
|
||||
cancelText={t('cancel')}
|
||||
onSubmit={onConnect}
|
||||
submitText={t('connect')}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
@ -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) => <Provider store={store}>{story()}</Provider>],
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => <SnapsConnect {...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) => <SnapsConnect {...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) => <SnapsConnect {...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',
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user