1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

[FLASK] Add snap alerts and prompts via snap_dialog RPC method (#16048)

Co-authored-by: Guillaume Roux <guillaumeroux123@gmail.com>
Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com>
This commit is contained in:
Erik Marks 2022-12-01 07:46:06 -08:00 committed by GitHub
parent 91e275e0d1
commit a861cc6dae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 654 additions and 166 deletions

View File

@ -751,6 +751,10 @@
"contacts": {
"message": "Contacts"
},
"contentFromSnap": {
"message": "Content from $1",
"description": "$1 represents the name of the snap"
},
"continue": {
"message": "Continue"
},
@ -2723,6 +2727,10 @@
"message": "Display a confirmation in MetaMask.",
"description": "The description for the `snap_confirm` permission"
},
"permission_dialog": {
"message": "Display dialog windows in MetaMask.",
"description": "The description for the `snap_dialog` permission"
},
"permission_ethereumAccounts": {
"message": "See address, account balance, activity and suggest transactions to approve",
"description": "The description for the `eth_accounts` permission"

View File

@ -91,6 +91,7 @@ import {
ORIGIN_METAMASK,
///: BEGIN:ONLY_INCLUDE_IN(flask)
MESSAGE_TYPE,
SNAP_DIALOG_TYPES,
///: END:ONLY_INCLUDE_IN
POLLING_TOKEN_ENVIRONMENT_TYPES,
SUBJECT_TYPES,
@ -1232,9 +1233,15 @@ export default class MetamaskController extends EventEmitter {
showConfirmation: (origin, confirmationData) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: MESSAGE_TYPE.SNAP_CONFIRM,
type: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
requestData: confirmationData,
}),
showDialog: (origin, type, requestData) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: SNAP_DIALOG_TYPES[type],
requestData,
}),
showNativeNotification: (origin, args) =>
this.controllerMessenger.call(
'RateLimitController:call',
@ -1399,7 +1406,7 @@ export default class MetamaskController extends EventEmitter {
).filter(
(approval) =>
approval.origin === truncatedSnap.id &&
approval.type === MESSAGE_TYPE.SNAP_CONFIRM,
approval.type.startsWith(RestrictedMethods.snap_dialog),
);
for (const approval of approvals) {
this.approvalController.reject(

View File

@ -1,3 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { DialogType } from '@metamask/rpc-methods';
///: END:ONLY_INCLUDE_IN
import { RestrictedMethods } from './permissions';
/**
@ -53,10 +56,20 @@ export const MESSAGE_TYPE = {
WATCH_ASSET: 'wallet_watchAsset',
WATCH_ASSET_LEGACY: 'metamask_watchAsset',
///: BEGIN:ONLY_INCLUDE_IN(flask)
SNAP_CONFIRM: RestrictedMethods.snap_confirm,
SNAP_DIALOG_ALERT: `${RestrictedMethods.snap_dialog}:alert`,
SNAP_DIALOG_CONFIRMATION: `${RestrictedMethods.snap_dialog}:confirmation`,
SNAP_DIALOG_PROMPT: `${RestrictedMethods.snap_dialog}:prompt`,
///: END:ONLY_INCLUDE_IN
} as const;
///: BEGIN:ONLY_INCLUDE_IN(flask)
export const SNAP_DIALOG_TYPES = {
[DialogType.Alert]: MESSAGE_TYPE.SNAP_DIALOG_ALERT,
[DialogType.Confirmation]: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
[DialogType.Prompt]: MESSAGE_TYPE.SNAP_DIALOG_PROMPT,
};
///: END:ONLY_INCLUDE_IN
/**
* Custom messages to send and be received by the extension
*/

View File

@ -6,6 +6,7 @@ export const RestrictedMethods = Object.freeze({
eth_accounts: 'eth_accounts',
///: BEGIN:ONLY_INCLUDE_IN(flask)
snap_confirm: 'snap_confirm',
snap_dialog: 'snap_dialog',
snap_notify: 'snap_notify',
snap_manageState: 'snap_manageState',
snap_getBip32PublicKey: 'snap_getBip32PublicKey',
@ -31,6 +32,6 @@ export const EndowmentPermissions = Object.freeze({
} as const);
// Methods / permissions in external packages that we are temporarily excluding.
export const ExcludedSnapPermissions = new Set(['snap_dialog']);
export const ExcludedSnapPermissions = new Set([]);
export const ExcludedSnapEndowments = new Set(['endowment:keyring']);
///: END:ONLY_INCLUDE_IN

View File

@ -39,8 +39,10 @@
@import 'flask/snap-content-footer/index';
@import 'flask/snap-install-warning/index';
@import 'flask/snap-remove-warning/index';
@import 'flask/snap-delineator/index';
@import 'flask/snap-settings-card/index';
@import 'flask/update-snap-permission-list/index';
@import 'flask/copyable/index';
@import 'gas-details-item/index';
@import 'gas-details-item/gas-details-item-title/index';
@import 'gas-timing/index';

View File

@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
import Box from '../../../ui/box';
import IconCopied from '../../../ui/icon/icon-copied';
import IconCopy from '../../../ui/icon/icon-copy';
import Typography from '../../../ui/typography';
import {
ALIGN_ITEMS,
BORDER_RADIUS,
COLORS,
JUSTIFY_CONTENT,
OVERFLOW_WRAP,
FLEX_DIRECTION,
TYPOGRAPHY,
} from '../../../../helpers/constants/design-system';
import { useCopyToClipboard } from '../../../../hooks/useCopyToClipboard';
export const Copyable = ({ text }) => {
const [copied, handleCopy] = useCopyToClipboard();
return (
<Box
className="copyable"
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
alignItems={ALIGN_ITEMS.STRETCH}
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
borderRadius={BORDER_RADIUS.SM}
paddingLeft={4}
paddingRight={4}
paddingTop={2}
paddingBottom={2}
>
<Typography
variant={TYPOGRAPHY.H6}
color={COLORS.TEXT_ALTERNATIVE}
marginRight={2}
overflowWrap={OVERFLOW_WRAP.ANYWHERE}
>
{text}
</Typography>
<Box
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={ALIGN_ITEMS.CENTER}
justifyContent={JUSTIFY_CONTENT.FLEX_START}
marginTop={2}
marginBottom={1}
>
{copied ? (
<IconCopied
color="var(--color-icon-alternative)"
className="copyable__icon"
size={18}
/>
) : (
<IconCopy
className="copyable__icon"
color="var(--color-icon-alternative)"
onClick={() => handleCopy(text)}
size={18}
/>
)}
</Box>
</Box>
);
};
Copyable.propTypes = {
text: PropTypes.string,
};

View File

@ -0,0 +1 @@
export { Copyable } from './copyable';

View File

@ -0,0 +1,5 @@
.copyable {
&__icon {
cursor: pointer;
}
}

View File

@ -0,0 +1 @@
export { SnapDelineator } from './snap-delineator';

View File

@ -0,0 +1,14 @@
.snap-delineator {
&__header {
border-bottom-width: 1px;
border-color: var(--color-border-muted);
border-style: solid;
border-radius: 8px 8px 0 0;
&__text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
}

View File

@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
ALIGN_ITEMS,
BORDER_RADIUS,
BORDER_STYLE,
COLORS,
SIZES,
TEXT,
} from '../../../../helpers/constants/design-system';
import Box from '../../../ui/box';
import { Icon, Text } from '../../../component-library';
export const SnapDelineator = ({ snapName, children }) => {
const t = useI18nContext();
return (
<Box
className="snap-delineator__wrapper"
borderStyle={BORDER_STYLE.SOLID}
borderColor={COLORS.BORDER_MUTED}
borderRadius={BORDER_RADIUS.LG}
>
<Box
className="snap-delineator__header"
alignItems={ALIGN_ITEMS.CENTER}
backgroundColor={COLORS.INFO_MUTED}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
paddingBottom={1}
>
<Icon name="snaps-filled" color={COLORS.INFO_DEFAULT} size={SIZES.SM} />
<Text
variant={TEXT.BODY_SM}
color={COLORS.INFO_DEFAULT}
className="snap-delineator__header__text"
marginLeft={1}
marginTop={0}
marginBottom={0}
>
{t('contentFromSnap', [snapName])}
</Text>
</Box>
<Box className="snap-delineator__content" padding={4}>
{children}
</Box>
</Box>
);
};
SnapDelineator.propTypes = {
snapName: PropTypes.string,
children: PropTypes.ReactNode,
};

View File

@ -0,0 +1,11 @@
import React from 'react';
import { SnapDelineator } from '.';
export default {
title: 'Components/App/SnapDelineator',
id: __filename,
};
export const DefaultStory = () => (
<SnapDelineator snapId="foo">Children</SnapDelineator>
);

View File

@ -8,28 +8,38 @@ import Box from '../../ui/box';
import MetaMaskTranslation from '../metamask-translation';
import NetworkDisplay from '../network-display';
import TextArea from '../../ui/textarea/textarea';
import TextField from '../../ui/text-field';
import ConfirmationNetworkSwitch from '../../../pages/confirmation/components/confirmation-network-switch';
import UrlIcon from '../../ui/url-icon';
import Tooltip from '../../ui/tooltip/tooltip';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { SnapDelineator } from '../flask/snap-delineator';
import { Copyable } from '../flask/copyable';
///: END:ONLY_INCLUDE_IN
export const safeComponentList = {
MetaMaskTranslation,
a: 'a',
b: 'b',
i: 'i',
p: 'p',
div: 'div',
span: 'span',
Typography,
Chip,
DefinitionList,
TruncatedDefinitionList,
Button,
Popover,
Box,
NetworkDisplay,
TextArea,
Button,
Chip,
ConfirmationNetworkSwitch,
UrlIcon,
DefinitionList,
MetaMaskTranslation,
NetworkDisplay,
Popover,
TextArea,
TextField,
Tooltip,
i: 'i',
TruncatedDefinitionList,
Typography,
UrlIcon,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapDelineator,
Copyable,
///: END:ONLY_INCLUDE_IN
};

View File

@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
const IconCopied = ({
size = 24,
color = 'currentColor',
ariaLabel,
className,
onClick,
}) => (
<svg
width={size}
height={size}
fill={color}
className={className}
aria-label={ariaLabel}
onClick={onClick}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
>
<path d="M16.59 3H12.81C10.0352 3 8.73388 3.98468 8.46277 6.36511C8.40605 6.86311 8.81849 7.275 9.31971 7.275H11.19C14.97 7.275 16.725 9.03 16.725 12.81V14.6803C16.725 15.1815 17.1369 15.594 17.6349 15.5372C20.0153 15.2661 21 13.9648 21 11.19V7.41C21 4.26 19.74 3 16.59 3Z" />
<path d="M11.19 8.4H7.41C4.26 8.4 3 9.66 3 12.81V16.59C3 19.74 4.26 21 7.41 21H11.19C14.34 21 15.6 19.74 15.6 16.59V12.81C15.6 9.66 14.34 8.4 11.19 8.4ZM12.261 13.485L8.922 16.824C8.796 16.95 8.634 17.013 8.463 17.013C8.292 17.013 8.13 16.95 8.004 16.824L6.33 15.15C6.078 14.898 6.078 14.493 6.33 14.241C6.582 13.989 6.987 13.989 7.239 14.241L8.454 15.456L11.343 12.567C11.595 12.315 12 12.315 12.252 12.567C12.504 12.819 12.513 13.233 12.261 13.485Z" />
</svg>
);
IconCopied.propTypes = {
/**
* The size of the Icon follows an 8px grid 2 = 16px, 3 = 24px etc
*/
size: PropTypes.number,
/**
* The color of the icon accepts design token css variables
*/
color: PropTypes.string,
/**
* An additional className to assign the Icon
*/
className: PropTypes.string,
/**
* The onClick handler
*/
onClick: PropTypes.func,
/**
* The aria-label of the icon for accessibility purposes
*/
ariaLabel: PropTypes.string,
};
export default IconCopied;

View File

@ -41,6 +41,7 @@ import IconTokenSearch from './icon-token-search';
import SearchIcon from './search-icon';
import IconCopy from './icon-copy';
import IconBlockExplorer from './icon-block-explorer';
import IconCopied from './icon-copied';
const validColors = [
'var(--color-icon-default)',
@ -133,6 +134,7 @@ export const DefaultStory = (args) => (
<IconItem Component={<SearchIcon {...args} />} />
<IconItem Component={<IconCopy {...args} />} />
<IconItem Component={<IconBlockExplorer {...args} />} />
<IconItem Component={<IconCopied {...args} />} />
</div>
</Box>
<Typography

View File

@ -44,7 +44,7 @@ $border-style: solid, double, none, dashed, dotted;
$directions: top, right, bottom, left;
$display: block, flex, grid, inline-block, inline-grid, inline-flex, list-item, none;
$text-align: left, right, center, justify, end;
$overflow-wrap: normal, break-word;
$overflow-wrap: normal, break-word, anywhere;
$font-weight: bold, medium, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900;
$font-style: normal, italic, oblique;
$font-size: 10px, 12px;

View File

@ -301,6 +301,7 @@ export const FONT_WEIGHT = {
export const OVERFLOW_WRAP = {
BREAK_WORD: 'break-word',
ANYWHERE: 'anywhere',
NORMAL: 'normal',
};

View File

@ -30,6 +30,11 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
leftIcon: 'fas fa-user-check',
rightIcon: null,
}),
[RestrictedMethods.snap_dialog]: (t) => ({
label: t('permission_dialog'),
leftIcon: 'fas fa-user-check',
rightIcon: null,
}),
[RestrictedMethods.snap_notify]: (t) => ({
leftIcon: 'fas fa-bell',
label: t('permission_notifications'),

View File

@ -1,11 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Button from '../../../../components/ui/button';
export default function ConfirmationFooter({
onApprove,
onSubmit,
onCancel,
approveText,
submitText,
cancelText,
alerts,
}) {
@ -13,11 +14,19 @@ export default function ConfirmationFooter({
<div className="confirmation-footer">
{alerts}
<div className="confirmation-footer__actions">
<Button type="secondary" onClick={onCancel}>
{cancelText}
</Button>
<Button type="primary" onClick={onApprove}>
{approveText}
{onCancel ? (
<Button type="secondary" onClick={onCancel}>
{cancelText}
</Button>
) : null}
<Button
type="primary"
onClick={onSubmit}
className={classnames({
centered: !onCancel,
})}
>
{submitText}
</Button>
</div>
</div>
@ -26,8 +35,8 @@ export default function ConfirmationFooter({
ConfirmationFooter.propTypes = {
alerts: PropTypes.node,
onApprove: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
approveText: PropTypes.string.isRequired,
cancelText: PropTypes.string.isRequired,
onCancel: PropTypes.func,
cancelText: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
submitText: PropTypes.string.isRequired,
};

View File

@ -7,8 +7,14 @@
background-color: var(--color-background-default);
padding: 16px;
& .button:first-child {
margin-right: 16px;
& .button {
&:first-child {
margin-right: 16px;
}
&.centered {
margin-right: 0;
}
}
}
}

View File

@ -6,11 +6,12 @@ import React, {
useState,
} from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { isEqual } from 'lodash';
import { produce } from 'immer';
import { MESSAGE_TYPE } from '../../../shared/constants/app';
import Box from '../../components/ui/box';
import MetaMaskTemplateRenderer from '../../components/app/metamask-template-renderer';
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
@ -21,13 +22,21 @@ import {
} from '../../helpers/constants/design-system';
import { useI18nContext } from '../../hooks/useI18nContext';
import { useOriginMetadata } from '../../hooks/useOriginMetadata';
import { getUnapprovedTemplatedConfirmations } from '../../selectors';
import {
///: BEGIN:ONLY_INCLUDE_IN(flask)
getSnap,
///: END:ONLY_INCLUDE_IN
getUnapprovedTemplatedConfirmations,
} from '../../selectors';
import NetworkDisplay from '../../components/app/network-display/network-display';
import Callout from '../../components/ui/callout';
import SiteOrigin from '../../components/ui/site-origin';
import ConfirmationFooter from './components/confirmation-footer';
import { getTemplateValues, getTemplateAlerts } from './templates';
// TODO(rekmarks): This component and all of its sub-components should probably
// be renamed to "Dialog", now that we are using it in that manner.
/**
* a very simple reducer using produce from Immer to keep state manipulation
* immutable and painless. This state is not stored in redux state because it
@ -132,14 +141,70 @@ export default function ConfirmationPage({
const originMetadata = useOriginMetadata(pendingConfirmation?.origin) || {};
const [alertState, dismissAlert] = useAlertState(pendingConfirmation);
const [inputStates, setInputStates] = useState({});
const setInputState = (key, value) => {
setInputStates((currentState) => ({ ...currentState, [key]: value }));
};
///: BEGIN:ONLY_INCLUDE_IN(flask)
const {
manifest: { proposedName },
} = useSelector((state) => getSnap(state, pendingConfirmation?.origin));
const SNAP_DIALOG_TYPE = [
MESSAGE_TYPE.SNAP_DIALOG_ALERT,
MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
MESSAGE_TYPE.SNAP_DIALOG_PROMPT,
];
const isSnapDialog = SNAP_DIALOG_TYPE.includes(pendingConfirmation?.type);
///: END:ONLY_INCLUDE_IN
const INPUT_STATE_CONFIRMATIONS = [
///: BEGIN:ONLY_INCLUDE_IN(flask)
MESSAGE_TYPE.SNAP_DIALOG_PROMPT,
///: END:ONLY_INCLUDE_IN
];
// Generating templatedValues is potentially expensive, and if done on every render
// will result in a new object. Avoiding calling this generation unnecessarily will
// improve performance and prevent unnecessary draws.
const templatedValues = useMemo(() => {
return pendingConfirmation
? getTemplateValues(pendingConfirmation, t, dispatch, history)
? getTemplateValues(
{
///: BEGIN:ONLY_INCLUDE_IN(flask)
snapName: isSnapDialog && proposedName,
///: END:ONLY_INCLUDE_IN
...pendingConfirmation,
},
t,
dispatch,
history,
setInputState,
)
: {};
}, [pendingConfirmation, t, dispatch, history]);
}, [
pendingConfirmation,
t,
dispatch,
history,
///: BEGIN:ONLY_INCLUDE_IN(flask)
isSnapDialog,
proposedName,
///: END:ONLY_INCLUDE_IN
]);
const hasInputState = (type) => {
return INPUT_STATE_CONFIRMATIONS.includes(type);
};
const handleSubmit = () =>
templatedValues.onSubmit(
hasInputState(pendingConfirmation.type)
? inputStates[MESSAGE_TYPE.SNAP_DIALOG_PROMPT]
: null,
);
useEffect(() => {
// If the number of pending confirmations reduces to zero when the user
@ -245,9 +310,9 @@ export default function ConfirmationPage({
</Callout>
))
}
onApprove={templatedValues.onApprove}
onSubmit={handleSubmit}
onCancel={templatedValues.onCancel}
approveText={templatedValues.approvalText}
submitText={templatedValues.submitText}
cancelText={templatedValues.cancelText}
/>
</div>

View File

@ -1,6 +1,6 @@
@import 'components/confirmation-footer/confirmation-footer';
@import 'components/confirmation-network-switch/index';
@import 'templates/flask/snap-confirm/index';
@import 'templates/flask/snap-prompt/index';
.confirmation-page {
width: 100%;

View File

@ -322,9 +322,9 @@ function getValues(pendingApproval, t, actions, history) {
},
},
],
approvalText: t('approveButtonText'),
cancelText: t('cancel'),
onApprove: async () => {
submitText: t('approveButtonText'),
onSubmit: async () => {
await actions.resolvePendingApproval(
pendingApproval.id,
pendingApproval.requestData,

View File

@ -0,0 +1,74 @@
import { TYPOGRAPHY } from '../../../../../helpers/constants/design-system';
function getValues(pendingApproval, t, actions) {
const { snapName, requestData } = pendingApproval;
const { title, description, textAreaContent } = requestData;
return {
content: [
{
element: 'Box',
key: 'snap-dialog-content-wrapper',
props: {
marginLeft: 4,
marginRight: 4,
},
children: {
element: 'SnapDelineator',
key: 'snap-delineator',
props: {
snapName,
},
children: [
{
element: 'Typography',
key: 'title',
children: title,
props: {
variant: TYPOGRAPHY.H3,
fontWeight: 'bold',
boxProps: {
marginBottom: 4,
},
},
},
...(description
? [
{
element: 'Typography',
key: 'subtitle',
children: description,
props: {
variant: TYPOGRAPHY.H6,
boxProps: {
marginBottom: 4,
},
},
},
]
: []),
...(textAreaContent
? [
{
element: 'Copyable',
key: 'snap-dialog-content-text',
props: {
text: textAreaContent,
},
},
]
: []),
],
},
},
],
submitText: t('ok'),
onSubmit: () => actions.resolvePendingApproval(pendingApproval.id, null),
};
}
const snapAlert = {
getValues,
};
export default snapAlert;

View File

@ -1,10 +0,0 @@
.snap-confirm {
padding: 16px 32px;
border-top: 1px solid var(--color-border-muted);
border-bottom: 1px solid var(--color-border-muted);
margin: 24px 0 16px 0;
.text {
font-family: monospace;
}
}

View File

@ -1,105 +0,0 @@
import {
RESIZE,
TYPOGRAPHY,
} from '../../../../../helpers/constants/design-system';
import ZENDESK_URLS from '../../../../../helpers/constants/zendesk-url';
function getValues(pendingApproval, t, actions) {
const { title, description, textAreaContent } = pendingApproval.requestData;
return {
content: [
{
element: 'Typography',
key: 'title',
children: title,
props: {
variant: TYPOGRAPHY.H3,
align: 'center',
fontWeight: 'bold',
boxProps: {
margin: [0, 0, 4],
},
},
},
...(description
? [
{
element: 'Typography',
key: 'subtitle',
children: description,
props: {
variant: TYPOGRAPHY.H6,
align: 'center',
boxProps: {
margin: [0, 0, 4],
},
},
},
]
: []),
...(textAreaContent
? [
{
element: 'div',
key: 'text-area',
children: {
element: 'TextArea',
props: {
// TODO(ritave): Terrible hard-coded height hack. Fixing this to adjust automatically to current window height would
// mean allowing template compoments to change global css, and since the intended use of the template
// renderer was to allow users to build their own UIs, this would be a big no-no.
height: '238px',
value: textAreaContent,
readOnly: true,
resize: RESIZE.VERTICAL,
scrollable: true,
className: 'text',
},
},
props: {
className: 'snap-confirm',
},
},
]
: []),
{
element: 'Typography',
key: 'only-interact-with-entities-you-trust',
children: [
{
element: 'span',
key: 'only-connect-trust',
children: `${t('onlyConnectTrust')} `,
},
{
element: 'a',
children: t('learnMoreUpperCase'),
key: 'learnMore-a-href',
props: {
href: ZENDESK_URLS.USER_GUIDE_DAPPS,
target: '__blank',
},
},
],
props: {
variant: TYPOGRAPHY.H7,
align: 'center',
boxProps: {
margin: 0,
},
},
},
],
approvalText: t('approveButtonText'),
cancelText: t('reject'),
onApprove: () => actions.resolvePendingApproval(pendingApproval.id, true),
onCancel: () => actions.resolvePendingApproval(pendingApproval.id, false),
};
}
const snapConfirm = {
getValues,
};
export default snapConfirm;

View File

@ -0,0 +1,76 @@
import { TYPOGRAPHY } from '../../../../../helpers/constants/design-system';
function getValues(pendingApproval, t, actions) {
const { snapName, requestData } = pendingApproval;
const { title, description, textAreaContent } = requestData;
return {
content: [
{
element: 'Box',
key: 'snap-dialog-content-wrapper',
props: {
marginLeft: 4,
marginRight: 4,
},
children: {
element: 'SnapDelineator',
key: 'snap-delineator',
props: {
snapName,
},
children: [
{
element: 'Typography',
key: 'title',
children: title,
props: {
variant: TYPOGRAPHY.H3,
fontWeight: 'bold',
boxProps: {
marginBottom: 4,
},
},
},
...(description
? [
{
element: 'Typography',
key: 'subtitle',
children: description,
props: {
variant: TYPOGRAPHY.H6,
boxProps: {
marginBottom: 4,
},
},
},
]
: []),
...(textAreaContent
? [
{
element: 'Copyable',
key: 'snap-dialog-content-text',
props: {
text: textAreaContent,
},
},
]
: []),
],
},
},
],
cancelText: t('reject'),
submitText: t('approveButtonText'),
onSubmit: () => actions.resolvePendingApproval(pendingApproval.id, true),
onCancel: () => actions.resolvePendingApproval(pendingApproval.id, false),
};
}
const snapConfirmation = {
getValues,
};
export default snapConfirmation;

View File

@ -0,0 +1,11 @@
.snap-prompt {
margin-top: 24px;
}
.snap-prompt-input {
& input {
@include H6;
}
width: 100%;
}

View File

@ -0,0 +1,88 @@
import { TYPOGRAPHY } from '../../../../../helpers/constants/design-system';
import { MESSAGE_TYPE } from '../../../../../../shared/constants/app';
function getValues(pendingApproval, t, actions, _history, setInputState) {
const { snapName, requestData } = pendingApproval;
const { title, description, placeholder } = requestData;
return {
content: [
{
element: 'Box',
key: 'snap-dialog-content-wrapper',
props: {
marginLeft: 4,
marginRight: 4,
},
children: {
element: 'SnapDelineator',
key: 'snap-delineator',
props: {
snapName,
},
children: [
{
element: 'Typography',
key: 'title',
children: title,
props: {
variant: TYPOGRAPHY.H3,
fontWeight: 'bold',
boxProps: {
marginBottom: 4,
},
},
},
...(description
? [
{
element: 'Typography',
key: 'subtitle',
children: description,
props: {
variant: TYPOGRAPHY.H6,
boxProps: {
marginBottom: 4,
},
},
},
]
: []),
{
element: 'div',
key: 'snap-prompt-container',
children: {
element: 'TextField',
key: 'snap-prompt-input',
props: {
className: 'snap-prompt-input',
placeholder,
max: 300,
onChange: (event) => {
const inputValue = event.target.value ?? '';
setInputState(MESSAGE_TYPE.SNAP_DIALOG_PROMPT, inputValue);
},
theme: 'bordered',
},
},
props: {
className: 'snap-prompt',
},
},
],
},
},
],
cancelText: t('cancel'),
submitText: t('submit'),
onSubmit: (inputValue) =>
actions.resolvePendingApproval(pendingApproval.id, inputValue),
onCancel: () => actions.resolvePendingApproval(pendingApproval.id, null),
};
}
const snapConfirm = {
getValues,
};
export default snapConfirm;

View File

@ -8,14 +8,18 @@ import {
import addEthereumChain from './add-ethereum-chain';
import switchEthereumChain from './switch-ethereum-chain';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import snapConfirm from './flask/snap-confirm/snap-confirm';
import snapAlert from './flask/snap-alert/snap-alert';
import snapConfirmation from './flask/snap-confirmation/snap-confirmation';
import snapPrompt from './flask/snap-prompt/snap-prompt';
///: END:ONLY_INCLUDE_IN
const APPROVAL_TEMPLATES = {
[MESSAGE_TYPE.ADD_ETHEREUM_CHAIN]: addEthereumChain,
[MESSAGE_TYPE.SWITCH_ETHEREUM_CHAIN]: switchEthereumChain,
///: BEGIN:ONLY_INCLUDE_IN(flask)
[MESSAGE_TYPE.SNAP_CONFIRM]: snapConfirm,
[MESSAGE_TYPE.SNAP_DIALOG_ALERT]: snapAlert,
[MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION]: snapConfirmation,
[MESSAGE_TYPE.SNAP_DIALOG_PROMPT]: snapPrompt,
///: END:ONLY_INCLUDE_IN
};
@ -23,12 +27,12 @@ export const TEMPLATED_CONFIRMATION_MESSAGE_TYPES =
Object.keys(APPROVAL_TEMPLATES);
const ALLOWED_TEMPLATE_KEYS = [
'content',
'approvalText',
'cancelText',
'onApprove',
'content',
'onCancel',
'onSubmit',
'networkDisplay',
'submitText',
];
/**
@ -113,12 +117,20 @@ function getAttenuatedDispatch(dispatch) {
/**
* Returns the templated values to be consumed in the confirmation page
*
* @param {object} pendingApproval - The pending confirmation object
* @param {Function} t - Translation function
* @param {Function} dispatch - Redux dispatch function
* @param history
* @param {object} pendingApproval - The pending confirmation object.
* @param {Function} t - Translation function.
* @param {Function} dispatch - Redux dispatch function.
* @param {object} history - The application's history object.
* @param {Function} setInputState - A function that can be used to record the
* state of input fields in the templated component.
*/
export function getTemplateValues(pendingApproval, t, dispatch, history) {
export function getTemplateValues(
pendingApproval,
t,
dispatch,
history,
setInputState,
) {
const fn = APPROVAL_TEMPLATES[pendingApproval.type]?.getValues;
if (!fn) {
throw new Error(
@ -127,7 +139,7 @@ export function getTemplateValues(pendingApproval, t, dispatch, history) {
}
const safeActions = getAttenuatedDispatch(dispatch);
const values = fn(pendingApproval, t, safeActions, history);
const values = fn(pendingApproval, t, safeActions, history, setInputState);
const extraneousKeys = omit(values, ALLOWED_TEMPLATE_KEYS);
const safeValues = pick(values, ALLOWED_TEMPLATE_KEYS);
if (extraneousKeys.length > 0) {

View File

@ -72,9 +72,9 @@ function getValues(pendingApproval, t, actions) {
},
},
],
approvalText: t('switchNetwork'),
cancelText: t('cancel'),
onApprove: () =>
submitText: t('switchNetwork'),
onSubmit: () =>
actions.resolvePendingApproval(
pendingApproval.id,
pendingApproval.requestData,

View File

@ -874,6 +874,14 @@ export function getSnaps(state) {
return state.metamask.snaps;
}
export const getSnap = createSelector(
getSnaps,
(_, snapId) => snapId,
(snaps, snapId) => {
return snaps[snapId];
},
);
export function getInsightSnaps(state) {
const snaps = Object.values(state.metamask.snaps);
const subjects = getPermissionSubjects(state);