1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +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", "message": "enable $1",
"description": "$1 is a token symbol, e.g. ETH" "description": "$1 is a token symbol, e.g. ETH"
}, },
"enabled": {
"message": "Enabled"
},
"encryptionPublicKeyNotice": { "encryptionPublicKeyNotice": {
"message": "$1 would like your public encryption key. By consenting, this site will be able to compose encrypted messages to you.", "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" "description": "$1 is the web3 site name"
@ -3777,10 +3780,6 @@
"message": "Install snap" "message": "Install snap"
}, },
"snapInstallRequest": { "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.", "message": "Installing $1 gives it the following permissions. Only continue if you trust $1.",
"description": "$1 is the snap name." "description": "$1 is the snap name."
}, },

View File

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

View File

@ -54,6 +54,8 @@ describe('Test Snap bip-32', function () {
await driver.waitForSelector({ text: 'Install' }); await driver.waitForSelector({ text: 'Install' });
await driver.clickElement('[data-testid="snap-install-scroll"]');
await driver.clickElement({ await driver.clickElement({
text: 'Install', text: 'Install',
tag: 'button', 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 // try to click on the Ok button and pass test if it works
await driver.clickElement({ await driver.clickElement({
text: 'Ok', text: 'OK',
tag: 'button', tag: 'button',
}); });
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import {
BorderStyle, BorderStyle,
Color, Color,
BorderRadius, BorderRadius,
FontWeight,
} from '../../../../helpers/constants/design-system'; } from '../../../../helpers/constants/design-system';
import { import {
formatDate, formatDate,
@ -34,7 +35,7 @@ import { disableSnap, enableSnap } from '../../../../store/actions';
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; import { useOriginMetadata } from '../../../../hooks/useOriginMetadata';
import SnapVersion from '../snap-version/snap-version'; import SnapVersion from '../snap-version/snap-version';
const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => { const SnapAuthorshipExpanded = ({ snapId, className, snap }) => {
const t = useI18nContext(); const t = useI18nContext();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -55,7 +56,6 @@ const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
const friendlyName = snapId && getSnapName(snapId, subjectMetadata); const friendlyName = snapId && getSnapName(snapId, subjectMetadata);
// Expanded data
const versionHistory = snap?.versionHistory ?? []; const versionHistory = snap?.versionHistory ?? [];
const installInfo = versionHistory.length const installInfo = versionHistory.length
? versionHistory[versionHistory.length - 1] ? versionHistory[versionHistory.length - 1]
@ -72,33 +72,35 @@ const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
return ( return (
<Box <Box
className={classnames('snaps-authorship', className)} className={classnames('snaps-authorship-expanded', className)}
backgroundColor={BackgroundColor.backgroundDefault} backgroundColor={BackgroundColor.backgroundDefault}
borderColor={BorderColor.borderDefault} borderColor={BorderColor.borderDefault}
borderWidth={1} borderWidth={1}
width={BLOCK_SIZES.FULL} width={BLOCK_SIZES.FULL}
borderRadius={expanded ? BorderRadius.LG : BorderRadius.pill} borderRadius={BorderRadius.LG}
> >
<Box <Box
alignItems={AlignItems.center} alignItems={AlignItems.center}
display={DISPLAY.FLEX} display={DISPLAY.FLEX}
width={BLOCK_SIZES.FULL} width={BLOCK_SIZES.FULL}
paddingLeft={expanded ? 4 : 2} paddingLeft={4}
paddingRight={expanded ? 4 : 2} paddingRight={4}
paddingTop={expanded ? 3 : 1} paddingTop={3}
paddingBottom={expanded ? 3 : 1} paddingBottom={3}
> >
<Box> <Box>
<SnapAvatar snapId={snapId} /> <SnapAvatar snapId={snapId} />
</Box> </Box>
<Box <Box
marginLeft={2} marginLeft={4}
marginRight={expanded ? 0 : 2} marginRight={0}
display={DISPLAY.FLEX} display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
style={{ overflow: 'hidden' }} style={{ overflow: 'hidden' }}
> >
<Text ellipsis>{friendlyName}</Text> <Text ellipsis fontWeight={FontWeight.Medium}>
{friendlyName}
</Text>
<Text <Text
ellipsis ellipsis
variant={TextVariant.bodySm} variant={TextVariant.bodySm}
@ -107,80 +109,77 @@ const SnapAuthorship = ({ snapId, className, expanded = false, snap }) => {
{packageName} {packageName}
</Text> </Text>
</Box> </Box>
{!expanded && (
<Box marginLeft="auto">
<SnapVersion version={subjectMetadata?.version} url={url} />
</Box>
)}
</Box> </Box>
{expanded && ( <Box flexDirection={FLEX_DIRECTION.COLUMN} width={BLOCK_SIZES.FULL}>
<Box flexDirection={FLEX_DIRECTION.COLUMN} width={BLOCK_SIZES.FULL}> <Box
<Box flexDirection={FLEX_DIRECTION.ROW}
flexDirection={FLEX_DIRECTION.ROW} justifyContent={JustifyContent.spaceBetween}
justifyContent={JustifyContent.spaceBetween} paddingLeft={4}
paddingLeft={4} paddingTop={4}
paddingTop={4} paddingBottom={4}
paddingBottom={4} borderColor={BorderColor.borderDefault}
borderColor={BorderColor.borderDefault} width={BLOCK_SIZES.FULL}
width={BLOCK_SIZES.FULL} style={{
style={{ borderLeft: BorderStyle.none,
borderLeft: BorderStyle.none, borderRight: BorderStyle.none,
borderRight: BorderStyle.none, }}
}} >
> <Text variant={TextVariant.bodyMd} fontWeight={FontWeight.Medium}>
<Text variant={TextVariant.bodyMdBold}>{t('enableSnap')}</Text> {t('enabled')}
<Box style={{ maxWidth: '52px' }}> </Text>
<Tooltip interactive position="left" html={t('snapsToggle')}> <Box style={{ maxWidth: '52px' }}>
<ToggleButton value={snap?.enabled} onToggle={onToggle} /> <Tooltip interactive position="left" html={t('snapsToggle')}>
</Tooltip> <ToggleButton value={snap?.enabled} onToggle={onToggle} />
</Box> </Tooltip>
</Box> </Box>
<Box </Box>
flexDirection={FLEX_DIRECTION.COLUMN} <Box
padding={4} flexDirection={FLEX_DIRECTION.COLUMN}
width={BLOCK_SIZES.FULL} padding={4}
> width={BLOCK_SIZES.FULL}
{installOrigin && installInfo && ( >
<Box {installOrigin && installInfo && (
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.ROW} flexDirection={FLEX_DIRECTION.ROW}
justifyContent={JustifyContent.spaceBetween} justifyContent={JustifyContent.spaceBetween}
alignItems={AlignItems.center} width={BLOCK_SIZES.FULL}
marginTop={4}
> >
<Text variant={TextVariant.bodyMdBold}>{t('version')}</Text> <Text variant={TextVariant.bodyMd} fontWeight={FontWeight.Medium}>
<SnapVersion version={snap?.version} url={url} /> {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.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>
)} </Box>
</Box> </Box>
); );
}; };
SnapAuthorship.propTypes = { SnapAuthorshipExpanded.propTypes = {
/** /**
* The id of the snap * The id of the snap
*/ */
@ -190,13 +189,9 @@ SnapAuthorship.propTypes = {
*/ */
className: PropTypes.string, className: PropTypes.string,
/** /**
* If the authorship component should be expanded * The snap object.
*/
expanded: PropTypes.bool,
/**
* The snap object. Can be undefined if the component is not expanded
*/ */
snap: PropTypes.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, DISPLAY,
JustifyContent, JustifyContent,
Size, Size,
BackgroundColor,
} from '../../../../helpers/constants/design-system'; } from '../../../../helpers/constants/design-system';
import { getSnapName } from '../../../../helpers/utils/util'; import { getSnapName } from '../../../../helpers/utils/util';
import { import {
@ -39,10 +40,12 @@ const SnapAvatar = ({ snapId, className }) => {
badge={ badge={
<AvatarIcon <AvatarIcon
iconName={IconName.Snaps} iconName={IconName.Snaps}
size={IconSize.Xs} size={IconSize.Sm}
backgroundColor={IconColor.infoDefault} backgroundColor={IconColor.infoDefault}
borderColor={BackgroundColor.backgroundDefault}
borderWidth={2}
iconProps={{ iconProps={{
size: IconSize.Xs, size: IconSize.Sm,
color: IconColor.infoInverse, color: IconColor.infoInverse,
}} }}
/> />
@ -50,10 +53,10 @@ const SnapAvatar = ({ snapId, className }) => {
position={BadgeWrapperPosition.bottomRight} position={BadgeWrapperPosition.bottomRight}
> >
{iconUrl ? ( {iconUrl ? (
<AvatarFavicon size={Size.MD} src={iconUrl} name={friendlyName} /> <AvatarFavicon size={Size.LG} src={iconUrl} name={friendlyName} />
) : ( ) : (
<AvatarBase <AvatarBase
size={Size.MD} size={Size.LG}
display={DISPLAY.FLEX} display={DISPLAY.FLEX}
alignItems={AlignItems.center} alignItems={AlignItems.center}
justifyContent={JustifyContent.center} justifyContent={JustifyContent.center}

View File

@ -47,19 +47,20 @@ export const SnapDelineator = ({
> >
<AvatarIcon <AvatarIcon
iconName={IconName.Snaps} iconName={IconName.Snaps}
size={IconSize.Xs} size={IconSize.Sm}
backgroundColor={ backgroundColor={
isError ? IconColor.errorDefault : IconColor.infoDefault isError ? IconColor.errorDefault : IconColor.infoDefault
} }
margin={1} borderColor={BackgroundColor.backgroundDefault}
borderWidth={2}
iconProps={{ iconProps={{
size: IconSize.Xs, size: IconSize.Sm,
color: IconColor.infoInverse, color: IconColor.infoInverse,
}} }}
/> />
<Text <Text
variant={TextVariant.bodySm} variant={TextVariant.bodySm}
color={isError ? TextColor.errorDefault : TextColor.default} color={isError ? TextColor.errorDefault : TextColor.textAlternative}
className="snap-delineator__header__text" className="snap-delineator__header__text"
marginLeft={1} marginLeft={1}
marginTop={0} 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, alerts,
loading, loading,
submitAlerts, submitAlerts,
actionsStyle,
style,
}) { }) {
return ( return (
<div className="confirmation-footer"> <div className="confirmation-footer" style={style}>
{alerts} {alerts}
{submitAlerts} {submitAlerts}
<div className="confirmation-footer__actions"> <div className="confirmation-footer__actions" style={actionsStyle}>
{onCancel ? ( {onCancel ? (
<Button type="secondary" onClick={onCancel}> <Button type="secondary" onClick={onCancel}>
{cancelText} {cancelText}
@ -47,4 +49,6 @@ ConfirmationFooter.propTypes = {
loadingText: PropTypes.string, loadingText: PropTypes.string,
loading: PropTypes.bool, loading: PropTypes.bool,
submitAlerts: PropTypes.node, 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 SiteOrigin from '../../components/ui/site-origin';
import { Icon, IconName } from '../../components/component-library'; import { Icon, IconName } from '../../components/component-library';
///: BEGIN:ONLY_INCLUDE_IN(snaps) ///: 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'; import { getSnapName } from '../../helpers/utils/util';
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
import ConfirmationFooter from './components/confirmation-footer'; import ConfirmationFooter from './components/confirmation-footer';
@ -372,13 +372,7 @@ export default function ConfirmationPage({
{ {
///: BEGIN:ONLY_INCLUDE_IN(snaps) ///: BEGIN:ONLY_INCLUDE_IN(snaps)
isSnapDialog && ( isSnapDialog && (
<Box <SnapAuthorshipHeader snapId={pendingConfirmation?.origin} />
alignItems="center"
margin={4}
flexDirection={FLEX_DIRECTION.COLUMN}
>
<SnapAuthorship snapId={pendingConfirmation?.origin} />
</Box>
) )
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
} }
@ -412,6 +406,22 @@ export default function ConfirmationPage({
</Callout> </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} onSubmit={handleSubmit}
onCancel={templatedValues.onCancel} onCancel={templatedValues.onCancel}
submitText={templatedValues.submitText} submitText={templatedValues.submitText}

View File

@ -14,6 +14,7 @@ function getValues(pendingApproval, t, actions) {
element: 'Box', element: 'Box',
key: 'snap-dialog-content-wrapper', key: 'snap-dialog-content-wrapper',
props: { props: {
marginTop: 4,
marginLeft: 4, marginLeft: 4,
marginRight: 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), onSubmit: () => actions.resolvePendingApproval(pendingApproval.id, null),
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,15 +2,22 @@
box-shadow: none; box-shadow: none;
&__content { &__content {
flex: 1;
&__loader-container { &__loader-container {
height: 100%; height: 100%;
} }
} }
&__scroll-button {
position: absolute;
margin-left: auto;
margin-right: auto;
right: 0;
left: 0;
bottom: 90px;
}
.page-container__footer { .page-container__footer {
width: 100%; 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 Box from '../../../../components/ui/box/box';
import { import {
AlignItems, AlignItems,
BackgroundColor,
BLOCK_SIZES, BLOCK_SIZES,
BorderStyle, BorderStyle,
FLEX_DIRECTION, FLEX_DIRECTION,
@ -17,11 +18,16 @@ import {
import { getSnapInstallWarnings } from '../util'; import { getSnapInstallWarnings } from '../util';
import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader'; import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader';
import InstallError from '../../../../components/app/snaps/install-error/install-error'; 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 { Text, ValidTag } from '../../../../components/component-library'; import {
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; AvatarIcon,
IconName,
Text,
ValidTag,
} from '../../../../components/component-library';
import { getSnapName } from '../../../../helpers/utils/util'; import { getSnapName } from '../../../../helpers/utils/util';
import SnapPermissionsList from '../../../../components/app/snaps/snap-permissions-list'; import SnapPermissionsList from '../../../../components/app/snaps/snap-permissions-list';
import { useScrollRequired } from '../../../../hooks/useScrollRequired';
export default function SnapInstall({ export default function SnapInstall({
request, request,
@ -33,7 +39,9 @@ export default function SnapInstall({
const t = useI18nContext(); const t = useI18nContext();
const [isShowingWarning, setIsShowingWarning] = useState(false); const [isShowingWarning, setIsShowingWarning] = useState(false);
const originMetadata = useOriginMetadata(request.metadata?.dappOrigin) || {};
const { isScrollable, isScrolledToBottom, scrollToBottom, ref, onScroll } =
useScrollRequired([requestState]);
const onCancel = useCallback( const onCancel = useCallback(
() => rejectSnapInstall(request.metadata.id), () => rejectSnapInstall(request.metadata.id),
@ -49,13 +57,6 @@ export default function SnapInstall({
const isLoading = requestState.loading; const isLoading = requestState.loading;
const hasPermissions =
!hasError &&
requestState?.permissions &&
Object.keys(requestState.permissions).length > 0;
const isEmpty = !isLoading && !hasError && !hasPermissions;
const warnings = getSnapInstallWarnings( const warnings = getSnapInstallWarnings(
requestState?.permissions ?? {}, requestState?.permissions ?? {},
targetSubjectMetadata, targetSubjectMetadata,
@ -84,25 +85,16 @@ export default function SnapInstall({
borderStyle={BorderStyle.none} borderStyle={BorderStyle.none}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
> >
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
<Box <Box
className="snap-install__header" ref={ref}
alignItems={AlignItems.center} onScroll={onScroll}
paddingLeft={4} className="snap-install__content"
paddingRight={4} style={{
flexDirection={FLEX_DIRECTION.COLUMN} 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 && ( {isLoading && (
<Box <Box
className="snap-install__content__loader-container" className="snap-install__content__loader-container"
@ -116,8 +108,16 @@ export default function SnapInstall({
{hasError && ( {hasError && (
<InstallError error={requestState.error} title={t('requestFailed')} /> <InstallError error={requestState.error} title={t('requestFailed')} />
)} )}
{hasPermissions && ( {!hasError && !isLoading && (
<> <>
<Text
variant={TextVariant.headingLg}
paddingTop={4}
paddingBottom={2}
textAlign="center"
>
{t('snapInstall')}
</Text>
<Text <Text
className="snap-install__content__permission-description" className="snap-install__content__permission-description"
paddingBottom={4} paddingBottom={4}
@ -125,7 +125,7 @@ export default function SnapInstall({
paddingRight={4} paddingRight={4}
textAlign={TEXT_ALIGN.CENTER} textAlign={TEXT_ALIGN.CENTER}
> >
{t('snapInstallRequestsPermission', [ {t('snapInstallRequest', [
<Text <Text
as={ValidTag.Span} as={ValidTag.Span}
key="2" key="2"
@ -140,40 +140,38 @@ export default function SnapInstall({
permissions={requestState.permissions || {}} permissions={requestState.permissions || {}}
targetSubjectMetadata={targetSubjectMetadata} 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>
<Box <Box
className="snap-install__footer" className="snap-install__footer"
alignItems={AlignItems.center} alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
> >
<PageContainerFooter <PageContainerFooter
cancelButtonType="default" cancelButtonType="default"
hideCancel={hasError} hideCancel={hasError}
disabled={isLoading} disabled={
isLoading || (!hasError && isScrollable && !isScrolledToBottom)
}
onCancel={onCancel} onCancel={onCancel}
cancelText={t('cancel')} cancelText={t('cancel')}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitText={t( submitText={t(hasError ? 'ok' : 'install')}
// eslint-disable-next-line no-nested-ternary
hasError ? 'ok' : 'install',
)}
/> />
</Box> </Box>
{isShowingWarning && ( {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 { .snap-result {
box-shadow: none; box-shadow: none;
&__header {
flex: 1;
&__loader-container {
height: 100%;
}
}
.page-container__footer { .page-container__footer {
width: 100%; width: 100%;
margin-top: 12px; border-top: 0;
} }
} }

View File

@ -26,7 +26,7 @@ import {
} from '../../../../components/component-library'; } from '../../../../components/component-library';
import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader'; import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader';
import InstallError from '../../../../components/app/snaps/install-error/install-error'; 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'; import { getSnapName } from '../../../../helpers/utils/util';
export default function SnapResult({ export default function SnapResult({
@ -140,17 +140,20 @@ export default function SnapResult({
borderStyle={BorderStyle.none} borderStyle={BorderStyle.none}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
> >
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
<Box <Box
className="snap-result__header" className="snap-result__content"
paddingLeft={4} paddingLeft={4}
paddingRight={4} paddingRight={4}
alignItems={AlignItems.center} alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
style={{
overflowY: 'auto',
}}
> >
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
{isLoading && ( {isLoading && (
<Box <Box
className="snap-result__header__loader-container" className="snap-result__content__loader-container"
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.center} alignItems={AlignItems.center}
justifyContent={JustifyContent.center} justifyContent={JustifyContent.center}
@ -167,6 +170,9 @@ export default function SnapResult({
className="snap-result__footer" className="snap-result__footer"
alignItems={AlignItems.center} alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
> >
<PageContainerFooter <PageContainerFooter
hideCancel 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; box-shadow: none;
&__content { &__content {
flex: 1;
&__loader-container { &__loader-container {
height: 100%; height: 100%;
} }
}
&__permission-description { &__scroll-button {
border-bottom: 1px solid var(--color-border-default); position: absolute;
} margin-left: auto;
margin-right: auto;
right: 0;
left: 0;
bottom: 90px;
} }
.page-container__footer { .page-container__footer {
width: 100%; width: 100%;
margin-top: 12px; border-top: 0;
button { button {
padding: 0.75rem; 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 Box from '../../../../components/ui/box/box';
import { import {
AlignItems, AlignItems,
BackgroundColor,
BLOCK_SIZES, BLOCK_SIZES,
BorderStyle, BorderStyle,
FLEX_DIRECTION, FLEX_DIRECTION,
FontWeight,
JustifyContent, JustifyContent,
TextVariant, TextVariant,
TEXT_ALIGN, TEXT_ALIGN,
@ -18,10 +20,16 @@ import UpdateSnapPermissionList from '../../../../components/app/snaps/update-sn
import { getSnapInstallWarnings } from '../util'; import { getSnapInstallWarnings } from '../util';
import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader'; import PulseLoader from '../../../../components/ui/pulse-loader/pulse-loader';
import InstallError from '../../../../components/app/snaps/install-error/install-error'; 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 { Text } from '../../../../components/component-library'; import {
AvatarIcon,
IconName,
Text,
ValidTag,
} from '../../../../components/component-library';
import { useOriginMetadata } from '../../../../hooks/useOriginMetadata'; import { useOriginMetadata } from '../../../../hooks/useOriginMetadata';
import { getSnapName } from '../../../../helpers/utils/util'; import { getSnapName } from '../../../../helpers/utils/util';
import { useScrollRequired } from '../../../../hooks/useScrollRequired';
export default function SnapUpdate({ export default function SnapUpdate({
request, request,
@ -35,6 +43,9 @@ export default function SnapUpdate({
const [isShowingWarning, setIsShowingWarning] = useState(false); const [isShowingWarning, setIsShowingWarning] = useState(false);
const originMetadata = useOriginMetadata(request.metadata?.dappOrigin) || {}; const originMetadata = useOriginMetadata(request.metadata?.dappOrigin) || {};
const { isScrollable, isScrolledToBottom, scrollToBottom, ref, onScroll } =
useScrollRequired([requestState]);
const onCancel = useCallback( const onCancel = useCallback(
() => rejectSnapUpdate(request.metadata.id), () => rejectSnapUpdate(request.metadata.id),
[request, rejectSnapUpdate], [request, rejectSnapUpdate],
@ -81,25 +92,26 @@ export default function SnapUpdate({
borderStyle={BorderStyle.none} borderStyle={BorderStyle.none}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
> >
<SnapAuthorshipHeader snapId={targetSubjectMetadata.origin} />
<Box <Box
className="snap-update__header" ref={ref}
paddingLeft={4} onScroll={onScroll}
paddingRight={4} className="snap-update__content"
alignItems={AlignItems.center} style={{
flexDirection={FLEX_DIRECTION.COLUMN} overflowY: 'auto',
flex: !isLoading && '1',
}}
> >
<SnapAuthorship snapId={targetSubjectMetadata.origin} />
{!isLoading && !hasError && ( {!isLoading && !hasError && (
<Text <Text
paddingBottom={4} paddingBottom={4}
paddingTop={4} paddingTop={4}
variant={TextVariant.headingLg} variant={TextVariant.headingLg}
textAlign="center"
> >
{t('snapUpdate')} {t('snapUpdate')}
</Text> </Text>
)} )}
</Box>
<Box className="snap-update__content">
{isLoading && ( {isLoading && (
<Box <Box
className="snap-update__content__loader-container" className="snap-update__content__loader-container"
@ -123,9 +135,30 @@ export default function SnapUpdate({
textAlign={TEXT_ALIGN.CENTER} textAlign={TEXT_ALIGN.CENTER}
> >
{t('snapUpdateRequest', [ {t('snapUpdateRequest', [
<b key="1">{originMetadata?.hostname}</b>, <Text
<b key="2">{snapName}</b>, as={ValidTag.Span}
<b key="3">v{newVersion}</b>, 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> </Text>
<UpdateSnapPermissionList <UpdateSnapPermissionList
@ -134,6 +167,17 @@ export default function SnapUpdate({
newPermissions={newPermissions} newPermissions={newPermissions}
targetSubjectMetadata={targetSubjectMetadata} 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> </Box>
@ -141,11 +185,16 @@ export default function SnapUpdate({
className="snap-update__footer" className="snap-update__footer"
alignItems={AlignItems.center} alignItems={AlignItems.center}
flexDirection={FLEX_DIRECTION.COLUMN} flexDirection={FLEX_DIRECTION.COLUMN}
style={{
boxShadow: 'var(--shadow-size-lg) var(--color-shadow-default)',
}}
> >
<PageContainerFooter <PageContainerFooter
cancelButtonType="default" cancelButtonType="default"
hideCancel={hasError} hideCancel={hasError}
disabled={isLoading} disabled={
isLoading || (!hasError && isScrollable && !isScrolledToBottom)
}
onCancel={onCancel} onCancel={onCancel}
cancelText={t('cancel')} cancelText={t('cancel')}
onSubmit={handleSubmit} 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, TextColor,
TextVariant, TextVariant,
} from '../../../../helpers/constants/design-system'; } 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 Box from '../../../../components/ui/box';
import SnapRemoveWarning from '../../../../components/app/snaps/snap-remove-warning'; import SnapRemoveWarning from '../../../../components/app/snaps/snap-remove-warning';
import ConnectedSitesList from '../../../../components/app/connected-sites-list'; import ConnectedSitesList from '../../../../components/app/connected-sites-list';
@ -124,7 +124,7 @@ function ViewSnap() {
paddingLeft={4} paddingLeft={4}
paddingRight={4} paddingRight={4}
> >
<SnapAuthorship snapId={snap.id} snap={snap} expanded /> <SnapAuthorshipExpanded snapId={snap.id} snap={snap} />
<Box className="view-snap__description" marginTop={[4, 7]}> <Box className="view-snap__description" marginTop={[4, 7]}>
<SnapDelineator type={DelineatorType.Description} snapName={snapName}> <SnapDelineator type={DelineatorType.Description} snapName={snapName}>
<Box <Box

View File

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