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:
parent
91e275e0d1
commit
a861cc6dae
8
app/_locales/en/messages.json
generated
8
app/_locales/en/messages.json
generated
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
68
ui/components/app/flask/copyable/copyable.js
Normal file
68
ui/components/app/flask/copyable/copyable.js
Normal 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,
|
||||
};
|
1
ui/components/app/flask/copyable/index.js
Normal file
1
ui/components/app/flask/copyable/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { Copyable } from './copyable';
|
5
ui/components/app/flask/copyable/index.scss
Normal file
5
ui/components/app/flask/copyable/index.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.copyable {
|
||||
&__icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
1
ui/components/app/flask/snap-delineator/index.js
Normal file
1
ui/components/app/flask/snap-delineator/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { SnapDelineator } from './snap-delineator';
|
14
ui/components/app/flask/snap-delineator/index.scss
Normal file
14
ui/components/app/flask/snap-delineator/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
56
ui/components/app/flask/snap-delineator/snap-delineator.js
Normal file
56
ui/components/app/flask/snap-delineator/snap-delineator.js
Normal 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,
|
||||
};
|
@ -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>
|
||||
);
|
@ -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
|
||||
};
|
||||
|
49
ui/components/ui/icon/icon-copied.js
Normal file
49
ui/components/ui/icon/icon-copied.js
Normal 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;
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -301,6 +301,7 @@ export const FONT_WEIGHT = {
|
||||
|
||||
export const OVERFLOW_WRAP = {
|
||||
BREAK_WORD: 'break-word',
|
||||
ANYWHERE: 'anywhere',
|
||||
NORMAL: 'normal',
|
||||
};
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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%;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
@ -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;
|
11
ui/pages/confirmation/templates/flask/snap-prompt/index.scss
Normal file
11
ui/pages/confirmation/templates/flask/snap-prompt/index.scss
Normal file
@ -0,0 +1,11 @@
|
||||
.snap-prompt {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.snap-prompt-input {
|
||||
& input {
|
||||
@include H6;
|
||||
}
|
||||
|
||||
width: 100%;
|
||||
}
|
@ -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;
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user