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

[FLASK] Rework Snaps headers and footers (#19442)

* Add new snap header and footer to snap install

* Add new snap header and footer to snap result and snap update

* Fix loading state

* Fix lint

* Add required scrolling

* Adjust avatar component

* Apply new headers and footers to snaps confirmations

* Rename previous SnapAuthorship component to SnapAuthorshipExpanded

* Fix lint

* Fix font weight

* Fix fencing

* Fix a test

* Fix lint after rebase

* Fix E2E

* Fix locale lint

* Fix another E2E

* Fix test ID

* Address PR comments

* Better scroll button centering

* Address design comments

* Fix unit test

* Fix E2Es
This commit is contained in:
Frederik Bolding 2023-06-07 15:18:49 +02:00 committed by GitHub
parent 546d8349e7
commit 789779f4d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 766 additions and 273 deletions

View File

@ -1371,6 +1371,9 @@
"message": "enable $1",
"description": "$1 is a token symbol, e.g. ETH"
},
"enabled": {
"message": "Enabled"
},
"encryptionPublicKeyNotice": {
"message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.",
"description": "$1 is the web3 site name"
@ -3777,10 +3780,6 @@
"message": "Install snap"
},
"snapInstallRequest": {
"message": "$1 wants to install $2. Make sure you trust the authors before you proceed.",
"description": "$1 is the dApp origin requesting the snap and $2 is the snap name"
},
"snapInstallRequestsPermission": {
"message": "Installing $1 gives it the following permissions. Only continue if you trust $1.",
"description": "$1 is the snap name."
},

View File

@ -165,7 +165,13 @@
}
},
"frequentRpcListDetail": [],
"subjectMetadata": {},
"subjectMetadata": {
"npm:@metamask/test-snap-bip44": {
"name": "@metamask/test-snap-bip44",
"version": "1.2.3",
"subjectType": "snap"
}
},
"notifications": {
"test": {
"id": "test",

View File

@ -54,6 +54,8 @@ describe('Test Snap bip-32', function () {
await driver.waitForSelector({ text: 'Install' });
await driver.clickElement('[data-testid="snap-install-scroll"]');
await driver.clickElement({
text: 'Install',
tag: 'button',

View File

@ -90,7 +90,7 @@ describe('Test Snap Cronjob', function () {
// try to click on the Ok button and pass test if it works
await driver.clickElement({
text: 'Ok',
text: 'OK',
tag: 'button',
});
},

View File

@ -95,7 +95,7 @@ describe('Test Snap Dialog', function () {
// click ok button
await driver.clickElement({
text: 'Ok',
text: 'OK',
tag: 'button',
});

View File

@ -97,7 +97,7 @@ describe('Test Snap networkAccess', function () {
// click ok button
await driver.clickElement({
text: 'Ok',
text: 'OK',
tag: 'button',
});
},

View File

@ -55,6 +55,8 @@ describe('Test Snap RPC', function () {
await driver.waitForSelector({ text: 'Install' });
await driver.clickElement('[data-testid="snap-install-scroll"]');
await driver.clickElement({
text: 'Install',
tag: 'button',

View File

@ -55,6 +55,8 @@ describe('Test Snap update', function () {
await driver.waitForSelector({ text: 'Install' });
await driver.clickElement('[data-testid="snap-install-scroll"]');
await driver.clickElement({
text: 'Install',
tag: 'button',
@ -105,6 +107,8 @@ describe('Test Snap update', function () {
await driver.waitForSelector({ text: 'Update' });
await driver.clickElement('[data-testid="snap-update-scroll"]');
await driver.clickElement({
text: 'Update',
tag: 'button',

View File

@ -7,17 +7,8 @@ import {
FLEX_DIRECTION,
JustifyContent,
} from '../../../helpers/constants/design-system';
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import SnapAuthorship from '../snaps/snap-authorship';
///: END:ONLY_INCLUDE_IN
export default class PermissionsConnectHeader extends Component {
///: BEGIN:ONLY_INCLUDE_IN(snaps)
static contextTypes = {
t: PropTypes.func,
};
///: END:ONLY_INCLUDE_IN
static propTypes = {
className: PropTypes.string,
iconUrl: PropTypes.string,
@ -28,10 +19,6 @@ export default class PermissionsConnectHeader extends Component {
headerText: PropTypes.string,
leftIcon: PropTypes.node,
rightIcon: PropTypes.node,
///: BEGIN:ONLY_INCLUDE_IN(snaps)
snapVersion: PropTypes.string,
isSnapInstallOrUpdate: PropTypes.bool,
///: END:ONLY_INCLUDE_IN
};
static defaultProps = {
@ -42,22 +29,7 @@ export default class PermissionsConnectHeader extends Component {
};
renderHeaderIcon() {
const {
iconUrl,
iconName,
siteOrigin,
leftIcon,
rightIcon,
///: BEGIN:ONLY_INCLUDE_IN(snaps)
isSnapInstallOrUpdate,
///: END:ONLY_INCLUDE_IN
} = this.props;
///: BEGIN:ONLY_INCLUDE_IN(snaps)
if (isSnapInstallOrUpdate) {
return null;
}
///: END:ONLY_INCLUDE_IN
const { iconUrl, iconName, siteOrigin, leftIcon, rightIcon } = this.props;
return (
<div className="permissions-connect-header__icon">
@ -75,16 +47,7 @@ export default class PermissionsConnectHeader extends Component {
}
render() {
const {
boxProps,
className,
headerTitle,
headerText,
///: BEGIN:ONLY_INCLUDE_IN(snaps)
siteOrigin,
isSnapInstallOrUpdate,
///: END:ONLY_INCLUDE_IN
} = this.props;
const { boxProps, className, headerTitle, headerText } = this.props;
return (
<Box
className={classnames('permissions-connect-header', className)}
@ -94,11 +57,6 @@ export default class PermissionsConnectHeader extends Component {
>
{this.renderHeaderIcon()}
<div className="permissions-connect-header__title">{headerTitle}</div>
{
///: BEGIN:ONLY_INCLUDE_IN(snaps)
isSnapInstallOrUpdate && <SnapAuthorship snapId={siteOrigin} />
///: END:ONLY_INCLUDE_IN
}
<div className="permissions-connect-header__subtitle">{headerText}</div>
</Box>
);

View File

@ -0,0 +1 @@
export { default } from './snap-authorship-expanded';

View File

@ -17,6 +17,7 @@ import {
BorderStyle,
Color,
BorderRadius,
FontWeight,
} from '../../../../helpers/constants/design-system';
import {
formatDate,
@ -34,7 +35,7 @@ import { disableSnap, enableSnap } from '../../../../store/actions';
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata';
import SnapVersion from '../snap-version/snap-version';
const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
const SnapAuthorshipExpanded = ({ snapId, className, snap }) => {
const t = useI18nContext();
const dispatch = useDispatch();
@ -55,7 +56,6 @@ const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
const friendlyName = snapId && getSnapName(snapId, subjectMetadata);
// Expanded data
const versionHistory = snap?.versionHistory ?? [];
const installInfo = versionHistory.length
? versionHistory[versionHistory.length - 1]
@ -72,33 +72,35 @@ const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
return (
<Box
className={classnames('snaps-authorship', className)}
className={classnames('snaps-authorship-expanded', className)}
backgroundColor={BackgroundColor.backgroundDefault}
borderColor={BorderColor.borderDefault}
borderWidth={1}
width={BLOCK_SIZES.FULL}
borderRadius={expanded ? BorderRadius.LG : BorderRadius.pill}
borderRadius={BorderRadius.LG}
>
<Box
alignItems={AlignItems.center}
display={DISPLAY.FLEX}
width={BLOCK_SIZES.FULL}
paddingLeft={expanded ? 4 : 2}
paddingRight={expanded ? 4 : 2}
paddingTop={expanded ? 3 : 1}
paddingBottom={expanded ? 3 : 1}
paddingLeft={4}
paddingRight={4}
paddingTop={3}
paddingBottom={3}
>
<Box>
<SnapAvatar snapId={snapId} />
</Box>
<Box
marginLeft={2}
marginRight={expanded ? 0 : 2}
marginLeft={4}
marginRight={0}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
style={{ overflow: 'hidden' }}
>
<Text ellipsis>{friendlyName}</Text>
<Text ellipsis fontWeight={FontWeight.Medium}>
{friendlyName}
</Text>
<Text
ellipsis
variant={TextVariant.bodySm}
@ -107,80 +109,77 @@ const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
{packageName}
</Text>
</Box>
{!expanded && (
<Box marginLeft="auto">
<SnapVersion version={subjectMetadata?.version} url={url} />
</Box>
)}
</Box>
{expanded && (
<Box flexDirection={FLEX_DIRECTION.COLUMN} width={BLOCK_SIZES.FULL}>
<Box
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.spaceBetween}
paddingLeft={4}
paddingTop={4}
paddingBottom={4}
borderColor={BorderColor.borderDefault}
width={BLOCK_SIZES.FULL}
style={{
borderLeft: BorderStyle.none,
borderRight: BorderStyle.none,
}}
>
<Text variant={TextVariant.bodyMdBold}>{t('enableSnap')}</Text>
<Box style={{ maxWidth: '52px' }}>
<Tooltip interactive position="left" html={t('snapsToggle')}>
<ToggleButton value={snap?.enabled} onToggle={onToggle} />
</Tooltip>
</Box>
<Box flexDirection={FLEX_DIRECTION.COLUMN} width={BLOCK_SIZES.FULL}>
<Box
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.spaceBetween}
paddingLeft={4}
paddingTop={4}
paddingBottom={4}
borderColor={BorderColor.borderDefault}
width={BLOCK_SIZES.FULL}
style={{
borderLeft: BorderStyle.none,
borderRight: BorderStyle.none,
}}
>
<Text variant={TextVariant.bodyMd} fontWeight={FontWeight.Medium}>
{t('enabled')}
</Text>
<Box style={{ maxWidth: '52px' }}>
<Tooltip interactive position="left" html={t('snapsToggle')}>
<ToggleButton value={snap?.enabled} onToggle={onToggle} />
</Tooltip>
</Box>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
padding={4}
width={BLOCK_SIZES.FULL}
>
{installOrigin && installInfo && (
<Box
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.spaceBetween}
width={BLOCK_SIZES.FULL}
>
<Text variant={TextVariant.bodyMdBold}>
{t('installOrigin')}
</Text>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.flexEnd}
>
<ButtonLink href={installOrigin.origin} target="_blank">
{installOrigin.host}
</ButtonLink>
<Text color={Color.textMuted}>
{t('installedOn', [
formatDate(installInfo.date, 'dd MMM yyyy'),
])}
</Text>
</Box>
</Box>
)}
</Box>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
padding={4}
width={BLOCK_SIZES.FULL}
>
{installOrigin && installInfo && (
<Box
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.center}
marginTop={4}
width={BLOCK_SIZES.FULL}
>
<Text variant={TextVariant.bodyMdBold}>{t('version')}</Text>
<SnapVersion version={snap?.version} url={url} />
<Text variant={TextVariant.bodyMd} fontWeight={FontWeight.Medium}>
{t('installOrigin')}
</Text>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.flexEnd}
>
<ButtonLink href={installOrigin.origin} target="_blank">
{installOrigin.host}
</ButtonLink>
<Text color={Color.textMuted}>
{t('installedOn', [
formatDate(installInfo.date, 'dd MMM yyyy'),
])}
</Text>
</Box>
</Box>
)}
<Box
flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.center}
marginTop={4}
>
<Text variant={TextVariant.bodyMd} fontWeight={FontWeight.Medium}>
{t('version')}
</Text>
<SnapVersion version={snap?.version} url={url} />
</Box>
</Box>
)}
</Box>
</Box>
);
};
SnapAuthorship.propTypes = {
SnapAuthorshipExpanded.propTypes = {
/**
* The id of the snap
*/
@ -190,13 +189,9 @@ SnapAuthorship.propTypes = {
*/
className: PropTypes.string,
/**
* If the authorship component should be expanded
*/
expanded: PropTypes.bool,
/**
* The snap object. Can be undefined if the component is not expanded
* The snap object.
*/
snap: PropTypes.object,
};
export default SnapAuthorship;
export default SnapAuthorshipExpanded;

View File

@ -0,0 +1 @@
export { default } from './snap-authorship-header';

View File

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { getSnapPrefix } from '@metamask/snaps-utils';
import { useSelector } from 'react-redux';
import Box from '../../../ui/box';
import {
BackgroundColor,
TextColor,
FLEX_DIRECTION,
TextVariant,
AlignItems,
DISPLAY,
BLOCK_SIZES,
FontWeight,
} from '../../../../helpers/constants/design-system';
import {
getSnapName,
removeSnapIdPrefix,
} from '../../../../helpers/utils/util';
import { Text } from '../../../component-library';
import { getTargetSubjectMetadata } from '../../../../selectors';
import SnapAvatar from '../snap-avatar';
import SnapVersion from '../snap-version/snap-version';
const SnapAuthorshipHeader = ({ snapId, className }) => {
// 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
// like it is done with snap install
const snapPrefix = snapId && getSnapPrefix(snapId);
const packageName = snapId && removeSnapIdPrefix(snapId);
const isNPM = snapPrefix === 'npm:';
const url = isNPM
? `https://www.npmjs.com/package/${packageName}`
: packageName;
const subjectMetadata = useSelector((state) =>
getTargetSubjectMetadata(state, snapId),
);
const friendlyName = snapId && getSnapName(snapId, subjectMetadata);
return (
<Box
className={classnames('snaps-authorship-header', className)}
backgroundColor={BackgroundColor.backgroundDefault}
width={BLOCK_SIZES.FULL}
alignItems={AlignItems.center}
display={DISPLAY.FLEX}
padding={4}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
>
<Box>
<SnapAvatar snapId={snapId} />
</Box>
<Box
marginLeft={4}
marginRight={4}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
style={{ overflow: 'hidden' }}
>
<Text ellipsis fontWeight={FontWeight.Medium}>
{friendlyName}
</Text>
<Text
ellipsis
variant={TextVariant.bodySm}
color={TextColor.textAlternative}
>
{packageName}
</Text>
</Box>
<Box marginLeft="auto">
<SnapVersion version={subjectMetadata?.version} url={url} />
</Box>
</Box>
);
};
SnapAuthorshipHeader.propTypes = {
/**
* The id of the snap
*/
snapId: PropTypes.string,
/**
* The className of the SnapAuthorship
*/
className: PropTypes.string,
};
export default SnapAuthorshipHeader;

View File

@ -0,0 +1,21 @@
import React from 'react';
import SnapAuthorshipHeader from './snap-authorship-header';
export default {
title: 'Components/App/Snaps/SnapAuthorshipHeader',
component: SnapAuthorshipHeader,
argTypes: {
snapId: {
control: 'text',
},
},
};
export const DefaultStory = (args) => <SnapAuthorshipHeader {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
snapId: 'npm:@metamask/test-snap-bip44',
};

View File

@ -1 +0,0 @@
export { default } from './snap-authorship';

View File

@ -1,21 +0,0 @@
import React from 'react';
import SnapAuthorship from '.';
export default {
title: 'Components/App/Snaps/SnapAuthorship',
component: SnapAuthorship,
argTypes: {
snapId: {
control: 'text',
},
},
};
export const DefaultStory = (args) => <SnapAuthorship {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
snapId: 'npm:@metamask/test-snap-bip44',
};

View File

@ -9,6 +9,7 @@ import {
DISPLAY,
JustifyContent,
Size,
BackgroundColor,
} from '../../../../helpers/constants/design-system';
import { getSnapName } from '../../../../helpers/utils/util';
import {
@ -39,10 +40,12 @@ const SnapAvatar = ({ snapId, className }) => {
badge={
<AvatarIcon
iconName={IconName.Snaps}
size={IconSize.Xs}
size={IconSize.Sm}
backgroundColor={IconColor.infoDefault}
borderColor={BackgroundColor.backgroundDefault}
borderWidth={2}
iconProps={{
size: IconSize.Xs,
size: IconSize.Sm,
color: IconColor.infoInverse,
}}
/>
@ -50,10 +53,10 @@ const SnapAvatar = ({ snapId, className }) => {
position={BadgeWrapperPosition.bottomRight}
>
{iconUrl ? (
<AvatarFavicon size={Size.MD} src={iconUrl} name={friendlyName} />
<AvatarFavicon size={Size.LG} src={iconUrl} name={friendlyName} />
) : (
<AvatarBase
size={Size.MD}
size={Size.LG}
display={DISPLAY.FLEX}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}

View File

@ -47,19 +47,20 @@ export const SnapDelineator = ({
>
<AvatarIcon
iconName={IconName.Snaps}
size={IconSize.Xs}
size={IconSize.Sm}
backgroundColor={
isError ? IconColor.errorDefault : IconColor.infoDefault
}
margin={1}
borderColor={BackgroundColor.backgroundDefault}
borderWidth={2}
iconProps={{
size: IconSize.Xs,
size: IconSize.Sm,
color: IconColor.infoInverse,
}}
/>
<Text
variant={TextVariant.bodySm}
color={isError ? TextColor.errorDefault : TextColor.default}
color={isError ? TextColor.errorDefault : TextColor.textAlternative}
className="snap-delineator__header__text"
marginLeft={1}
marginTop={0}

View File

@ -0,0 +1,44 @@
import { useEffect, useRef, useState } from 'react';
import { debounce } from 'lodash';
/**
* Utility hook for requiring users to scroll through content.
* Returns an object containing state and helpers to accomplish this.
*
* The hook expects both the `ref` and the `onScroll` handler to be passed to the scrolling element.
*
* @param dependencies - Any optional hook dependencies for updating the scroll state.
* @returns Flags for isScrollable and isScrollToBottom, a ref to use for the scrolling content, a scrollToBottom function and a onScroll handler.
*/
export const useScrollRequired = (dependencies = []) => {
const ref = useRef();
const [isScrollableState, setIsScrollable] = useState(false);
const [isScrolledToBottomState, setIsScrolledToBottom] = useState(false);
const update = () => {
const isScrollable =
ref.current && ref.current.scrollHeight > ref.current.clientHeight;
const isScrolledToBottom = isScrollable
? Math.round(ref.current.scrollTop) + ref.current.offsetHeight >=
ref.current.scrollHeight
: true;
setIsScrollable(isScrollable);
setIsScrolledToBottom(isScrolledToBottom);
};
useEffect(update, [ref, ...dependencies]);
const scrollToBottom = () => {
if (ref.current) {
ref.current.scrollTo(0, ref.current.scrollHeight);
}
};
return {
isScrollable: isScrollableState,
isScrolledToBottom: isScrolledToBottomState,
scrollToBottom,
ref,
onScroll: debounce(update, 25),
};
};

View File

@ -12,12 +12,14 @@ export default function ConfirmationFooter({
alerts,
loading,
submitAlerts,
actionsStyle,
style,
}) {
return (
<div className="confirmation-footer">
<div className="confirmation-footer" style={style}>
{alerts}
{submitAlerts}
<div className="confirmation-footer__actions">
<div className="confirmation-footer__actions" style={actionsStyle}>
{onCancel ? (
<Button type="secondary" onClick={onCancel}>
{cancelText}
@ -47,4 +49,6 @@ ConfirmationFooter.propTypes = {
loadingText: PropTypes.string,
loading: PropTypes.bool,
submitAlerts: PropTypes.node,
style: PropTypes.object,
actionsStyle: PropTypes.object,
};

View File

@ -38,7 +38,7 @@ import Callout from '../../components/ui/callout';
import SiteOrigin from '../../components/ui/site-origin';
import { Icon, IconName } from '../../components/component-library';
///: BEGIN:ONLY_INCLUDE_IN(snaps)
import SnapAuthorship from '../../components/app/snaps/snap-authorship';
import SnapAuthorshipHeader from '../../components/app/snaps/snap-authorship-header';
import { getSnapName } from '../../helpers/utils/util';
///: END:ONLY_INCLUDE_IN
import ConfirmationFooter from './components/confirmation-footer';
@ -372,13 +372,7 @@ export default function ConfirmationPage({
{
///: BEGIN:ONLY_INCLUDE_IN(snaps)
isSnapDialog && (
<Box
alignItems="center"
margin={4}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<SnapAuthorship snapId={pendingConfirmation?.origin} />
</Box>
<SnapAuthorshipHeader snapId={pendingConfirmation?.origin} />
)
///: END:ONLY_INCLUDE_IN
}
@ -412,6 +406,22 @@ export default function ConfirmationPage({
</Callout>
))
}
///: BEGIN:ONLY_INCLUDE_IN(snaps)
style={
isSnapDialog
? {
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}
: {}
}
actionsStyle={
isSnapDialog
? {
borderTop: 0,
}
: {}
}
///: END:ONLY_INCLUDE_IN
onSubmit={handleSubmit}
onCancel={templatedValues.onCancel}
submitText={templatedValues.submitText}

View File

@ -14,6 +14,7 @@ function getValues(pendingApproval, t, actions) {
element: 'Box',
key: 'snap-dialog-content-wrapper',
props: {
marginTop: 4,
marginLeft: 4,
marginRight: 4,
},
@ -29,7 +30,7 @@ function getValues(pendingApproval, t, actions) {
},
},
],
submitText: t('ok'),
submitText: t('ok').toUpperCase(),
onSubmit: () => actions.resolvePendingApproval(pendingApproval.id, null),
};
}

View File

@ -14,6 +14,7 @@ function getValues(pendingApproval, t, actions) {
element: 'Box',
key: 'snap-dialog-content-wrapper',
props: {
marginTop: 4,
marginLeft: 4,
marginRight: 4,
},

View File

@ -15,6 +15,7 @@ function getValues(pendingApproval, t, actions, _history, setInputState) {
element: 'Box',
key: 'snap-dialog-content-wrapper',
props: {
marginTop: 4,
marginLeft: 4,
marginRight: 4,
},

View File

@ -49,6 +49,7 @@ export default class PermissionConnect extends Component {
setSnapsInstallPrivacyWarningShownStatus: PropTypes.func.isRequired,
snapsInstallPrivacyWarningShown: PropTypes.bool.isRequired,
///: END:ONLY_INCLUDE_IN
hideTopBar: PropTypes.bool,
totalPages: PropTypes.string.isRequired,
page: PropTypes.string.isRequired,
targetSubjectMetadata: PropTypes.shape({
@ -296,6 +297,7 @@ export default class PermissionConnect extends Component {
permissionsRequestId,
connectPath,
confirmPermissionPath,
hideTopBar,
///: BEGIN:ONLY_INCLUDE_IN(snaps)
snapInstallPath,
snapUpdatePath,
@ -318,7 +320,7 @@ export default class PermissionConnect extends Component {
return (
<div className="permissions-connect">
{this.renderTopBar()}
{!hideTopBar && this.renderTopBar()}
{redirecting && permissionsApproved ? (
<PermissionsRedirect subjectMetadata={targetSubjectMetadata} />
) : (

View File

@ -78,8 +78,6 @@ const mapStateToProps = (state, ownProps) => {
const requestType = getRequestType(state, permissionsRequestId);
///: BEGIN:ONLY_INCLUDE_IN(snaps)
const isSnap = targetSubjectMetadata.subjectType === SubjectType.Snap;
const requestState = getRequestState(state, permissionsRequestId);
///: END:ONLY_INCLUDE_IN
@ -101,11 +99,15 @@ const mapStateToProps = (state, ownProps) => {
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}`;
const isSnapInstallOrUpdateOrResult =
pathname === snapInstallPath ||
pathname === snapUpdatePath ||
pathname === snapResultPath;
///: END:ONLY_INCLUDE_IN
let totalPages = 1 + isRequestingAccounts;
///: BEGIN:ONLY_INCLUDE_IN(snaps)
totalPages += isSnap;
totalPages += isSnapInstallOrUpdateOrResult;
///: END:ONLY_INCLUDE_IN
totalPages = totalPages.toString();
@ -115,11 +117,7 @@ const mapStateToProps = (state, ownProps) => {
} else if (pathname === confirmPermissionPath) {
page = isRequestingAccounts ? '2' : '1';
///: BEGIN:ONLY_INCLUDE_IN(snaps)
} else if (
pathname === snapInstallPath ||
pathname === snapUpdatePath ||
pathname === snapResultPath
) {
} else if (isSnapInstallOrUpdateOrResult) {
page = isRequestingAccounts ? '3' : '2';
///: END:ONLY_INCLUDE_IN
} else {
@ -134,7 +132,7 @@ const mapStateToProps = (state, ownProps) => {
snapUpdatePath,
snapResultPath,
requestState,
isSnap,
hideTopBar: isSnapInstallOrUpdateOrResult,
snapsInstallPrivacyWarningShown: getSnapsInstallPrivacyWarningShown(state),
///: END:ONLY_INCLUDE_IN
permissionsRequest,

View File

@ -2,15 +2,22 @@
box-shadow: none;
&__content {
flex: 1;
&__loader-container {
height: 100%;
}
}
&__scroll-button {
position: absolute;
margin-left: auto;
margin-right: auto;
right: 0;
left: 0;
bottom: 90px;
}
.page-container__footer {
width: 100%;
margin-top: 12px;
border-top: 0;
}
}

View File

@ -6,6 +6,7 @@ import SnapInstallWarning from '../../../../components/app/snaps/snap-install-wa
import Box from '../../../../components/ui/box/box';
import {
AlignItems,
BackgroundColor,
BLOCK_SIZES,
BorderStyle,
FLEX_DIRECTION,
@ -17,11 +18,16 @@ import {
import { getSnapInstallWarnings } from '../util';
import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader';
import InstallError from '../../../../components/app/snaps/install-error/install-error';
import SnapAuthorship from '../../../../components/app/snaps/snap-authorship';
import { Text, ValidTag } from '../../../../components/component-library';
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata';
import SnapAuthorshipHeader from '../../../../components/app/snaps/snap-authorship-header';
import {
AvatarIcon,
IconName,
Text,
ValidTag,
} from '../../../../components/component-library';
import { getSnapName } from '../../../../helpers/utils/util';
import SnapPermissionsList from '../../../../components/app/snaps/snap-permissions-list';
import { useScrollRequired } from '../../../../hooks/useScrollRequired';
export default function SnapInstall({
request,
@ -33,7 +39,9 @@ export default function SnapInstall({
const t = useI18nContext();
const [isShowingWarning, setIsShowingWarning] = useState(false);
const originMetadata = useOriginMetadata(request.metadata?.dappOrigin) || {};
const { isScrollable, isScrolledToBottom, scrollToBottom, ref, onScroll } =
useScrollRequired([requestState]);
const onCancel = useCallback(
() => rejectSnapInstall(request.metadata.id),
@ -49,13 +57,6 @@ export default function SnapInstall({
const isLoading = requestState.loading;
const hasPermissions =
!hasError &&
requestState?.permissions &&
Object.keys(requestState.permissions).length > 0;
const isEmpty = !isLoading && !hasError && !hasPermissions;
const warnings = getSnapInstallWarnings(
requestState?.permissions ?? {},
targetSubjectMetadata,
@ -84,25 +85,16 @@ export default function SnapInstall({
borderStyle={BorderStyle.none}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
<Box
className="snap-install__header"
alignItems={AlignItems.center}
paddingLeft={4}
paddingRight={4}
flexDirection={FLEX_DIRECTION.COLUMN}
ref={ref}
onScroll={onScroll}
className="snap-install__content"
style={{
overflowY: 'auto',
flex: !isLoading && '1',
}}
>
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
{!isLoading && !hasError && (
<Text
variant={TextVariant.headingLg}
paddingTop={4}
paddingBottom={2}
>
{t('snapInstall')}
</Text>
)}
</Box>
<Box className="snap-install__content">
{isLoading && (
<Box
className="snap-install__content__loader-container"
@ -116,8 +108,16 @@ export default function SnapInstall({
{hasError && (
<InstallError error={requestState.error} title={t('requestFailed')} />
)}
{hasPermissions && (
{!hasError && !isLoading && (
<>
<Text
variant={TextVariant.headingLg}
paddingTop={4}
paddingBottom={2}
textAlign="center"
>
{t('snapInstall')}
</Text>
<Text
className="snap-install__content__permission-description"
paddingBottom={4}
@ -125,7 +125,7 @@ export default function SnapInstall({
paddingRight={4}
textAlign={TEXT_ALIGN.CENTER}
>
{t('snapInstallRequestsPermission', [
{t('snapInstallRequest', [
<Text
as={ValidTag.Span}
key="2"
@ -140,40 +140,38 @@ export default function SnapInstall({
permissions={requestState.permissions || {}}
targetSubjectMetadata={targetSubjectMetadata}
/>
{isScrollable && !isScrolledToBottom ? (
<AvatarIcon
className="snap-install__scroll-button"
data-testid="snap-install-scroll"
iconName={IconName.Arrow2Down}
backgroundColor={BackgroundColor.infoDefault}
color={BackgroundColor.backgroundDefault}
onClick={scrollToBottom}
style={{ cursor: 'pointer' }}
/>
) : null}
</>
)}
{isEmpty && (
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
height={BLOCK_SIZES.FULL}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
>
<Text textAlign={TEXT_ALIGN.CENTER}>
{t('snapInstallRequest', [
<b key="1">{originMetadata?.hostname}</b>,
<b key="2">{snapName}</b>,
])}
</Text>
</Box>
)}
</Box>
<Box
className="snap-install__footer"
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
>
<PageContainerFooter
cancelButtonType="default"
hideCancel={hasError}
disabled={isLoading}
disabled={
isLoading || (!hasError && isScrollable && !isScrolledToBottom)
}
onCancel={onCancel}
cancelText={t('cancel')}
onSubmit={handleSubmit}
submitText={t(
// eslint-disable-next-line no-nested-ternary
hasError ? 'ok' : 'install',
)}
submitText={t(hasError ? 'ok' : 'install')}
/>
</Box>
{isShowingWarning && (

View File

@ -0,0 +1,122 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../../store/store';
import mockState from '../../../../../test/data/mock-state.json';
import SnapInstall from '.';
const store = configureStore(mockState);
export default {
title: 'Pages/Snaps/SnapInstall',
component: SnapInstall,
argTypes: {},
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};
export const DefaultStory = (args) => <SnapInstall {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: false,
permissions: {
'endowment:rpc': {
caveats: [
{
type: 'rpcOrigin',
value: {
dapps: true,
},
},
],
},
snap_dialog: {},
snap_getBip44Entropy: {
caveats: [
{
type: 'permittedCoinTypes',
value: [
{
coinType: 1,
},
],
},
],
},
},
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};
export const LoadingStory = (args) => <SnapInstall {...args} />;
LoadingStory.storyName = 'Loading';
LoadingStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: true,
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};
export const ScrollingStory = (args) => <SnapInstall {...args} />;
ScrollingStory.storyName = 'Scrolling';
ScrollingStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: false,
permissions: {
'endowment:rpc': {
caveats: [
{
type: 'rpcOrigin',
value: {
dapps: true,
},
},
],
},
'endowment:network-access': {},
snap_notify: {},
snap_dialog: {},
snap_getBip44Entropy: {
caveats: [
{
type: 'permittedCoinTypes',
value: [
{
coinType: 1,
},
],
},
],
},
},
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};

View File

@ -1,16 +1,8 @@
.snap-result {
box-shadow: none;
&__header {
flex: 1;
&__loader-container {
height: 100%;
}
}
.page-container__footer {
width: 100%;
margin-top: 12px;
border-top: 0;
}
}

View File

@ -26,7 +26,7 @@ import {
} from '../../../../components/component-library';
import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader';
import InstallError from '../../../../components/app/snaps/install-error/install-error';
import SnapAuthorship from '../../../../components/app/snaps/snap-authorship';
import SnapAuthorshipHeader from '../../../../components/app/snaps/snap-authorship-header';
import { getSnapName } from '../../../../helpers/utils/util';
export default function SnapResult({
@ -140,17 +140,20 @@ export default function SnapResult({
borderStyle={BorderStyle.none}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
<Box
className="snap-result__header"
className="snap-result__content"
paddingLeft={4}
paddingRight={4}
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
style={{
overflowY: 'auto',
}}
>
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
{isLoading && (
<Box
className="snap-result__header__loader-container"
className="snap-result__content__loader-container"
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.center}
justifyContent={JustifyContent.center}
@ -167,6 +170,9 @@ export default function SnapResult({
className="snap-result__footer"
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
>
<PageContainerFooter
hideCancel

View File

@ -0,0 +1,64 @@
import React from 'react';
import SnapResult from '.';
export default {
title: 'Pages/Snaps/SnapResult',
component: SnapResult,
argTypes: {},
};
export const DefaultStory = (args) => <SnapResult {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: false,
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};
export const LoadingStory = (args) => <SnapResult {...args} />;
LoadingStory.storyName = 'Loading';
LoadingStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: true,
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};
export const ErrorStory = (args) => <SnapResult {...args} />;
ErrorStory.storyName = 'Error';
ErrorStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: false,
error: 'foo',
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};

View File

@ -2,20 +2,23 @@
box-shadow: none;
&__content {
flex: 1;
&__loader-container {
height: 100%;
}
}
&__permission-description {
border-bottom: 1px solid var(--color-border-default);
}
&__scroll-button {
position: absolute;
margin-left: auto;
margin-right: auto;
right: 0;
left: 0;
bottom: 90px;
}
.page-container__footer {
width: 100%;
margin-top: 12px;
border-top: 0;
button {
padding: 0.75rem;

View File

@ -6,9 +6,11 @@ import SnapInstallWarning from '../../../../components/app/snaps/snap-install-wa
import Box from '../../../../components/ui/box/box';
import {
AlignItems,
BackgroundColor,
BLOCK_SIZES,
BorderStyle,
FLEX_DIRECTION,
FontWeight,
JustifyContent,
TextVariant,
TEXT_ALIGN,
@ -18,10 +20,16 @@ import UpdateSnapPermissionList from '../../../../components/app/snaps/update-sn
import { getSnapInstallWarnings } from '../util';
import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader';
import InstallError from '../../../../components/app/snaps/install-error/install-error';
import SnapAuthorship from '../../../../components/app/snaps/snap-authorship';
import { Text } from '../../../../components/component-library';
import SnapAuthorshipHeader from '../../../../components/app/snaps/snap-authorship-header';
import {
AvatarIcon,
IconName,
Text,
ValidTag,
} from '../../../../components/component-library';
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata';
import { getSnapName } from '../../../../helpers/utils/util';
import { useScrollRequired } from '../../../../hooks/useScrollRequired';
export default function SnapUpdate({
request,
@ -35,6 +43,9 @@ export default function SnapUpdate({
const [isShowingWarning, setIsShowingWarning] = useState(false);
const originMetadata = useOriginMetadata(request.metadata?.dappOrigin) || {};
const { isScrollable, isScrolledToBottom, scrollToBottom, ref, onScroll } =
useScrollRequired([requestState]);
const onCancel = useCallback(
() => rejectSnapUpdate(request.metadata.id),
[request, rejectSnapUpdate],
@ -81,25 +92,26 @@ export default function SnapUpdate({
borderStyle={BorderStyle.none}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
<Box
className="snap-update__header"
paddingLeft={4}
paddingRight={4}
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
ref={ref}
onScroll={onScroll}
className="snap-update__content"
style={{
overflowY: 'auto',
flex: !isLoading && '1',
}}
>
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
{!isLoading && !hasError && (
<Text
paddingBottom={4}
paddingTop={4}
variant={TextVariant.headingLg}
textAlign="center"
>
{t('snapUpdate')}
</Text>
)}
</Box>
<Box className="snap-update__content">
{isLoading && (
<Box
className="snap-update__content__loader-container"
@ -123,9 +135,30 @@ export default function SnapUpdate({
textAlign={TEXT_ALIGN.CENTER}
>
{t('snapUpdateRequest', [
<b key="1">{originMetadata?.hostname}</b>,
<b key="2">{snapName}</b>,
<b key="3">v{newVersion}</b>,
<Text
as={ValidTag.Span}
key="1"
variant={TextVariant.bodyMd}
fontWeight={FontWeight.Medium}
>
{originMetadata?.hostname}
</Text>,
<Text
as={ValidTag.Span}
key="2"
variant={TextVariant.bodyMd}
fontWeight={FontWeight.Medium}
>
{snapName}
</Text>,
<Text
as={ValidTag.Span}
key="3"
variant={TextVariant.bodyMd}
fontWeight={FontWeight.Medium}
>
{newVersion}
</Text>,
])}
</Text>
<UpdateSnapPermissionList
@ -134,6 +167,17 @@ export default function SnapUpdate({
newPermissions={newPermissions}
targetSubjectMetadata={targetSubjectMetadata}
/>
{isScrollable && !isScrolledToBottom ? (
<AvatarIcon
className="snap-install__scroll-button"
data-testid="snap-update-scroll"
iconName={IconName.Arrow2Down}
backgroundColor={BackgroundColor.infoDefault}
color={BackgroundColor.backgroundDefault}
onClick={scrollToBottom}
style={{ cursor: 'pointer' }}
/>
) : null}
</>
)}
</Box>
@ -141,11 +185,16 @@ export default function SnapUpdate({
className="snap-update__footer"
alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
>
<PageContainerFooter
cancelButtonType="default"
hideCancel={hasError}
disabled={isLoading}
disabled={
isLoading || (!hasError && isScrollable && !isScrolledToBottom)
}
onCancel={onCancel}
cancelText={t('cancel')}
onSubmit={handleSubmit}

View File

@ -0,0 +1,124 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../../store/store';
import mockState from '../../../../../test/data/mock-state.json';
import SnapUpdate from '.';
const store = configureStore(mockState);
export default {
title: 'Pages/Snaps/SnapUpdate',
component: SnapUpdate,
argTypes: {},
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};
export const DefaultStory = (args) => <SnapUpdate {...args} />;
DefaultStory.storyName = 'Default';
DefaultStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: false,
newVersion: '2.0.0',
approvedPermissions: {
'endowment:rpc': {
caveats: [
{
type: 'rpcOrigin',
value: {
dapps: true,
},
},
],
},
snap_dialog: {},
snap_getBip44Entropy: {
caveats: [
{
type: 'permittedCoinTypes',
value: [
{
coinType: 1,
},
],
},
],
},
},
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};
export const LoadingStory = (args) => <SnapUpdate {...args} />;
LoadingStory.storyName = 'Loading';
LoadingStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: true,
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};
export const ScrollingStory = (args) => <SnapUpdate {...args} />;
ScrollingStory.storyName = 'Scrolling';
ScrollingStory.args = {
request: {
metadata: {
id: 'foo',
},
},
requestState: {
loading: false,
newVersion: '2.0.0',
approvedPermissions: {
'endowment:rpc': {
caveats: [
{
type: 'rpcOrigin',
value: {
dapps: true,
},
},
],
},
'endowment:network-access': {},
snap_notify: {},
snap_dialog: {},
snap_getBip44Entropy: {
caveats: [
{
type: 'permittedCoinTypes',
value: [
{
coinType: 1,
},
],
},
],
},
},
},
targetSubjectMetadata: {
origin: 'npm:@metamask/test-snap-bip44',
},
};

View File

@ -14,7 +14,7 @@ import {
TextColor,
TextVariant,
} from '../../../../helpers/constants/design-system';
import SnapAuthorship from '../../../../components/app/snaps/snap-authorship';
import SnapAuthorshipExpanded from '../../../../components/app/snaps/snap-authorship-expanded';
import Box from '../../../../components/ui/box';
import SnapRemoveWarning from '../../../../components/app/snaps/snap-remove-warning';
import ConnectedSitesList from '../../../../components/app/connected-sites-list';
@ -124,7 +124,7 @@ function ViewSnap() {
paddingLeft={4}
paddingRight={4}
>
<SnapAuthorship snapId={snap.id} snap={snap} expanded />
<SnapAuthorshipExpanded snapId={snap.id} snap={snap} />
<Box className="view-snap__description" marginTop={[4, 7]}>
<SnapDelineator type={DelineatorType.Description} snapName={snapName}>
<Box

View File

@ -38,9 +38,9 @@ describe('ViewSnap', () => {
// Snap name & Snap authorship component
expect(getByText('BIP-44 Test Snap')).toBeDefined();
expect(container.getElementsByClassName('snaps-authorship')?.length).toBe(
1,
);
expect(
container.getElementsByClassName('snaps-authorship-expanded')?.length,
).toBe(1);
// Snap description
expect(
getByText('An example Snap that signs messages using BLS.'),
@ -48,7 +48,7 @@ describe('ViewSnap', () => {
// Snap version info
expect(getByText('v5.1.2')).toBeDefined();
// Enable Snap
expect(getByText('Enable')).toBeDefined();
expect(getByText('Enabled')).toBeDefined();
expect(container.getElementsByClassName('toggle-button')?.length).toBe(1);
// Permissions
expect(getByText('Permissions')).toBeDefined();