diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7833b25f3..b8d64b675 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2684,6 +2684,10 @@ "message": "Show notifications.", "description": "The description for the `snap_notify` permission" }, + "permission_transactionInsight": { + "message": "Fetch and display transaction insights.", + "description": "The description for the `endowment:transaction-insight` permission" + }, "permission_unknown": { "message": "Unknown permission: $1", "description": "$1 is the name of a requested permission that is not recognized." @@ -3239,6 +3243,10 @@ "message": "Added on $1 from $2", "description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap." }, + "snapContent": { + "message": "This content is coming from $1", + "description": "This is shown when a snap shows transaction insight information in the confirmation UI. $1 is a link to the snap's settings page with the link text being the name of the snap." + }, "snapError": { "message": "Snap Error: '$1'. Error Code: '$2'", "description": "This is shown when a snap encounters an error. $1 is the error message from the snap, and $2 is the error code." @@ -3269,6 +3277,12 @@ "snaps": { "message": "Snaps" }, + "snapsInsightLoading": { + "message": "Loading transaction insight..." + }, + "snapsNoInsight": { + "message": "The snap didn't return any insight" + }, "snapsSettingsDescription": { "message": "Manage your Snaps" }, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 6022e6927..1befeb496 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1867,6 +1867,10 @@ export default class MetamaskController extends EventEmitter { this.controllerMessenger, 'SnapController:remove', ), + handleSnapRequest: this.controllerMessenger.call.bind( + this.controllerMessenger, + 'SnapController:handleRequest', + ), dismissNotifications: this.dismissNotifications.bind(this), markNotificationsAsRead: this.markNotificationsAsRead.bind(this), ///: END:ONLY_INCLUDE_IN diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss index 77574fb3e..dc85fc6a1 100644 --- a/ui/components/app/app-components.scss +++ b/ui/components/app/app-components.scss @@ -32,6 +32,7 @@ @import 'edit-gas-fee-popover/edit-gas-tooltip/index'; @import 'flask/experimental-area/index'; @import 'flask/snaps-authorship-pill/index'; +@import 'flask/snap-content-footer/index'; @import 'flask/snap-install-warning/index'; @import 'flask/snap-remove-warning/index'; @import 'flask/snap-settings-card/index'; diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index b09b534ed..be48015a5 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -23,6 +23,9 @@ export default class ConfirmPageContainerContent extends Component { dataComponent: PropTypes.node, dataHexComponent: PropTypes.node, detailsComponent: PropTypes.node, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent: PropTypes.node, + ///: END:ONLY_INCLUDE_IN errorKey: PropTypes.string, errorMessage: PropTypes.string, hideSubtitle: PropTypes.bool, @@ -59,15 +62,37 @@ export default class ConfirmPageContainerContent extends Component { renderContent() { const { detailsComponent, dataComponent } = this.props; + ///: BEGIN:ONLY_INCLUDE_IN(flask) + const { insightComponent } = this.props; + + if (insightComponent && (detailsComponent || dataComponent)) { + return this.renderTabs(); + } + ///: END:ONLY_INCLUDE_IN + if (detailsComponent && dataComponent) { return this.renderTabs(); } - return detailsComponent || dataComponent; + + return ( + detailsComponent || + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent || + ///: END:ONLY_INCLUDE_IN + dataComponent + ); } renderTabs() { const { t } = this.context; - const { detailsComponent, dataComponent, dataHexComponent } = this.props; + const { + detailsComponent, + dataComponent, + dataHexComponent, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent, + ///: END:ONLY_INCLUDE_IN + } = this.props; return ( @@ -88,6 +113,12 @@ export default class ConfirmPageContainerContent extends Component { {dataHexComponent} )} + + { + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent + ///: END:ONLY_INCLUDE_IN + } ); } diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss b/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss index 37d104c80..92395fbb1 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/index.scss @@ -82,12 +82,28 @@ color: var(--color-text-alternative); text-transform: uppercase; - margin: 0 8px; & button { font-size: unset; color: var(--color-text-alternative); text-transform: uppercase; + max-width: 170px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + & .dropdown__select { + color: var(--color-text-alternative); + text-transform: uppercase; + + option { + text-transform: none; + } + } + + & .dropdown__icon-caret-down { + top: 40%; } } diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 027d771ac..60912159a 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -64,6 +64,9 @@ export default class ConfirmPageContainer extends Component { dataComponent: PropTypes.node, dataHexComponent: PropTypes.node, detailsComponent: PropTypes.node, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent: PropTypes.node, + ///: END:ONLY_INCLUDE_IN tokenAddress: PropTypes.string, nonce: PropTypes.string, warning: PropTypes.string, @@ -155,6 +158,9 @@ export default class ConfirmPageContainer extends Component { isBuyableChain, networkIdentifier, setApproveForAllArg, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent, + ///: END:ONLY_INCLUDE_IN } = this.props; const showAddToAddressDialog = @@ -242,6 +248,9 @@ export default class ConfirmPageContainer extends Component { detailsComponent={detailsComponent} dataComponent={dataComponent} dataHexComponent={dataHexComponent} + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent={insightComponent} + ///: END:ONLY_INCLUDE_IN errorMessage={errorMessage} errorKey={errorKey} tokenAddress={tokenAddress} diff --git a/ui/components/app/confirm-page-container/flask/index.js b/ui/components/app/confirm-page-container/flask/index.js new file mode 100644 index 000000000..7fa665cf7 --- /dev/null +++ b/ui/components/app/confirm-page-container/flask/index.js @@ -0,0 +1 @@ +export { SnapInsight } from './snap-insight'; diff --git a/ui/components/app/confirm-page-container/flask/index.scss b/ui/components/app/confirm-page-container/flask/index.scss new file mode 100644 index 000000000..cd027f79c --- /dev/null +++ b/ui/components/app/confirm-page-container/flask/index.scss @@ -0,0 +1,3 @@ +.snap-insight { + word-wrap: break-word; +} diff --git a/ui/components/app/confirm-page-container/flask/snap-insight.js b/ui/components/app/confirm-page-container/flask/snap-insight.js new file mode 100644 index 000000000..1567763dd --- /dev/null +++ b/ui/components/app/confirm-page-container/flask/snap-insight.js @@ -0,0 +1,111 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import Preloader from '../../../ui/icon/preloader/preloader-icon.component'; +import Typography from '../../../ui/typography/typography'; +import { + ALIGN_ITEMS, + COLORS, + FLEX_DIRECTION, + JUSTIFY_CONTENT, + TEXT_ALIGN, + TYPOGRAPHY, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { useTransactionInsightSnap } from '../../../../hooks/flask/useTransactionInsightSnap'; +import SnapContentFooter from '../../flask/snap-content-footer/snap-content-footer'; +import Box from '../../../ui/box/box'; + +export const SnapInsight = ({ transaction, chainId, selectedSnap }) => { + const t = useI18nContext(); + const response = useTransactionInsightSnap({ + transaction, + chainId, + snapId: selectedSnap.id, + }); + + const data = response?.insights; + + const hasNoData = !data || !Object.keys(data).length; + + return ( + + {data ? ( + + {Object.keys(data).length ? ( + <> + + {Object.keys(data).map((key, i) => ( +
+ + {key} + + {data[key]} +
+ ))} +
+ + + ) : ( + + {t('snapsNoInsight')} + + )} +
+ ) : ( + <> + + + {t('snapsInsightLoading')} + + + )} +
+ ); +}; + +SnapInsight.propTypes = { + /* + * The transaction data object + */ + transaction: PropTypes.object, + /* + * CAIP2 Chain ID + */ + chainId: PropTypes.string, + /* + * The insight snap selected + */ + selectedSnap: PropTypes.object, +}; diff --git a/ui/components/app/confirm-page-container/index.js b/ui/components/app/confirm-page-container/index.js index 955ef1bb8..b216aba05 100644 --- a/ui/components/app/confirm-page-container/index.js +++ b/ui/components/app/confirm-page-container/index.js @@ -7,3 +7,6 @@ export { default as ConfirmPageContainerContent, ConfirmPageContainerSummary, } from './confirm-page-container-content'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +export { SnapInsight } from './flask/snap-insight'; +///: END:ONLY_INCLUDE_IN diff --git a/ui/components/app/confirm-page-container/index.scss b/ui/components/app/confirm-page-container/index.scss index ca3e12aa7..ca1ac9fb1 100644 --- a/ui/components/app/confirm-page-container/index.scss +++ b/ui/components/app/confirm-page-container/index.scss @@ -2,6 +2,9 @@ @import 'confirm-page-container-header/index'; @import 'confirm-detail-row/index'; @import 'confirm-page-container-navigation/index'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +@import 'flask/index'; +///: END:ONLY_INCLUDE_IN .confirm-page-container { &__dialog { diff --git a/ui/components/app/flask/snap-content-footer/index.js b/ui/components/app/flask/snap-content-footer/index.js new file mode 100644 index 000000000..9eccd24ee --- /dev/null +++ b/ui/components/app/flask/snap-content-footer/index.js @@ -0,0 +1 @@ +export { default } from './snap-content-footer'; diff --git a/ui/components/app/flask/snap-content-footer/index.scss b/ui/components/app/flask/snap-content-footer/index.scss new file mode 100644 index 000000000..972d3328e --- /dev/null +++ b/ui/components/app/flask/snap-content-footer/index.scss @@ -0,0 +1,13 @@ +.snap-content-footer { + i { + color: var(--color-icon-muted); + padding-right: 4px; + } + + .button { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100px; + } +} diff --git a/ui/components/app/flask/snap-content-footer/snap-content-footer.js b/ui/components/app/flask/snap-content-footer/snap-content-footer.js new file mode 100644 index 000000000..6b0371d9e --- /dev/null +++ b/ui/components/app/flask/snap-content-footer/snap-content-footer.js @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { useHistory } from 'react-router-dom'; + +import Typography from '../../../ui/typography/typography'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { SNAPS_VIEW_ROUTE } from '../../../../helpers/constants/routes'; +import { + COLORS, + TYPOGRAPHY, + JUSTIFY_CONTENT, + ALIGN_ITEMS, +} from '../../../../helpers/constants/design-system'; +import Button from '../../../ui/button'; +import Box from '../../../ui/box/box'; + +export default function SnapContentFooter({ snapName, snapId }) { + const t = useI18nContext(); + const history = useHistory(); + + const handleNameClick = (e) => { + e.stopPropagation(); + history.push(`${SNAPS_VIEW_ROUTE}/${encodeURIComponent(snapId)}`); + }; + // TODO: add truncation to the snap name, need to pick a character length at which to cut off + return ( + + + + {t('snapContent', [ + , + ])} + + + ); +} + +SnapContentFooter.propTypes = { + /** + * The name of the snap who's content is displayed + */ + snapName: PropTypes.string, + /** + * The id of the snap + */ + snapId: PropTypes.string, +}; diff --git a/ui/components/app/flask/snap-content-footer/snap-content-footer.stories.js b/ui/components/app/flask/snap-content-footer/snap-content-footer.stories.js new file mode 100644 index 000000000..dbc7a12bf --- /dev/null +++ b/ui/components/app/flask/snap-content-footer/snap-content-footer.stories.js @@ -0,0 +1,17 @@ +import React from 'react'; + +import SnapContentFooter from '.'; + +export default { + title: 'Components/App/Flask/SnapContentFooter', + id: __filename, + component: SnapContentFooter, + args: { + snapName: 'Test Snap', + snapId: 'local:test-snap', + }, +}; + +export const DefaultStory = (args) => ; + +DefaultStory.storyName = 'Default'; diff --git a/ui/components/ui/tabs/dropdown-tab/dropdown-tab.js b/ui/components/ui/tabs/dropdown-tab/dropdown-tab.js new file mode 100644 index 000000000..4deccf850 --- /dev/null +++ b/ui/components/ui/tabs/dropdown-tab/dropdown-tab.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import Dropdown from '../../dropdown'; + +export const DropdownTab = (props) => { + const { + activeClassName, + className, + 'data-testid': dataTestId, + isActive, + onClick, + onChange, + tabIndex, + options, + selectedOption, + } = props; + + return ( +
  • { + event.preventDefault(); + onClick(tabIndex); + }} + > + +
  • + ); +}; + +DropdownTab.propTypes = { + activeClassName: PropTypes.string, + className: PropTypes.string, + 'data-testid': PropTypes.string, + isActive: PropTypes.bool, // required, but added using React.cloneElement + options: PropTypes.arrayOf( + PropTypes.exact({ + name: PropTypes.string, + value: PropTypes.string.isRequired, + }), + ).isRequired, + selectedOption: PropTypes.string, + onChange: PropTypes.func, + onClick: PropTypes.func, + tabIndex: PropTypes.number, // required, but added using React.cloneElement +}; + +DropdownTab.defaultProps = { + activeClassName: undefined, + className: undefined, + onChange: undefined, + onClick: undefined, + selectedOption: undefined, +}; diff --git a/ui/components/ui/tabs/dropdown-tab/index.js b/ui/components/ui/tabs/dropdown-tab/index.js new file mode 100644 index 000000000..82d1738ac --- /dev/null +++ b/ui/components/ui/tabs/dropdown-tab/index.js @@ -0,0 +1,3 @@ +import { DropdownTab } from './dropdown-tab'; + +export default DropdownTab; diff --git a/ui/components/ui/tabs/dropdown-tab/index.scss b/ui/components/ui/tabs/dropdown-tab/index.scss new file mode 100644 index 000000000..01ee7f077 --- /dev/null +++ b/ui/components/ui/tabs/dropdown-tab/index.scss @@ -0,0 +1,23 @@ +.tab { + .dropdown__select { + border: none; + font-size: unset; + width: 100%; + background-color: unset; + padding-left: 8px; + padding-right: 20px; + line-height: unset; + + option { + background-color: var(--color-background-default); + } + + &:focus-visible { + outline: none; + } + } + + .dropdown__icon-caret-down { + right: 0; + } +} diff --git a/ui/components/ui/tabs/index.js b/ui/components/ui/tabs/index.js index 43366ec6f..c20ebbfc1 100644 --- a/ui/components/ui/tabs/index.js +++ b/ui/components/ui/tabs/index.js @@ -1,4 +1,5 @@ import Tabs from './tabs.component'; import Tab from './tab'; +import DropdownTab from './dropdown-tab'; -export { Tabs, Tab }; +export { Tabs, Tab, DropdownTab }; diff --git a/ui/components/ui/tabs/index.scss b/ui/components/ui/tabs/index.scss index 96ccf695c..b3f230d98 100644 --- a/ui/components/ui/tabs/index.scss +++ b/ui/components/ui/tabs/index.scss @@ -1,4 +1,5 @@ @import 'tab/index'; +@import 'dropdown-tab/index'; .tabs { flex-grow: 1; diff --git a/ui/components/ui/tabs/tabs.stories.js b/ui/components/ui/tabs/tabs.stories.js index 0266eec6c..f3047e491 100644 --- a/ui/components/ui/tabs/tabs.stories.js +++ b/ui/components/ui/tabs/tabs.stories.js @@ -1,4 +1,5 @@ import React from 'react'; +import DropdownTab from './dropdown-tab'; import Tab from './tab/tab.component'; import Tabs from './tabs.component'; @@ -41,6 +42,14 @@ export const DefaultStory = (args) => { onTabClick={args.onTabClick} > {args.tabs.map((tabProps, i) => renderTab(tabProps, i))} + + This is a dropdown Tab + ); }; diff --git a/ui/helpers/utils/permission.js b/ui/helpers/utils/permission.js index 6b0d20009..3405cc857 100644 --- a/ui/helpers/utils/permission.js +++ b/ui/helpers/utils/permission.js @@ -94,6 +94,11 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({ leftIcon: 'fas fa-infinity', rightIcon: null, }, + [EndowmentPermissions['endowment:transaction-insight']]: { + label: (t) => t('permission_transactionInsight'), + leftIcon: 'fas fa-info', + rightIcon: null, + }, ///: END:ONLY_INCLUDE_IN [UNKNOWN_PERMISSION]: { label: (t, permissionName) => diff --git a/ui/hooks/flask/useTransactionInsightSnap.js b/ui/hooks/flask/useTransactionInsightSnap.js new file mode 100644 index 000000000..6206bc30d --- /dev/null +++ b/ui/hooks/flask/useTransactionInsightSnap.js @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { handleSnapRequest } from '../../store/actions'; +import { getPermissionSubjects } from '../../selectors'; + +const INSIGHT_PERMISSION = 'endowment:transaction-insight'; + +export function useTransactionInsightSnap({ transaction, chainId, snapId }) { + const subjects = useSelector(getPermissionSubjects); + if (!subjects[snapId]?.permissions[INSIGHT_PERMISSION]) { + throw new Error( + 'This snap does not have the transaction insight endowment.', + ); + } + const [data, setData] = useState(undefined); + + useEffect(() => { + async function fetchInsight() { + const d = await handleSnapRequest({ + snapId, + origin: 'test', + handler: 'onTransaction', + request: { + jsonrpc: '2.0', + method: ' ', + params: { transaction, chainId }, + }, + }); + setData(d); + } + if (transaction) { + fetchInsight(); + } + }, [snapId, transaction, chainId]); + + return data; +} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js index 75e2599d6..b1f081ff1 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -1,5 +1,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import { stripHexPrefix } from 'ethereumjs-util'; +///: END:ONLY_INCLUDE_IN import ConfirmPageContainer from '../../components/app/confirm-page-container'; import TransactionDecoding from '../../components/app/transaction-decoding'; import { isBalanceSufficient } from '../send/send.utils'; @@ -42,7 +45,7 @@ import GasDetailsItem from '../../components/app/gas-details-item'; import GasTiming from '../../components/app/gas-timing/gas-timing.component'; import LedgerInstructionField from '../../components/app/ledger-instruction-field'; import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message'; - +import Typography from '../../components/ui/typography/typography'; import { COLORS, FONT_STYLE, @@ -55,12 +58,21 @@ import { removePollingTokenFromAppState, } from '../../store/actions'; -import Typography from '../../components/ui/typography/typography'; import { MIN_GAS_LIMIT_DEC } from '../send/send.constants'; -import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network'; import { hexToDecimal } from '../../../shared/lib/metamask-controller-utils'; import { hexWEIToDecGWEI } from '../../../shared/lib/transactions-controller-utils'; +///: BEGIN:ONLY_INCLUDE_IN(flask) +import { SnapInsight } from '../../components/app/confirm-page-container/flask/snap-insight'; +import { DropdownTab, Tab } from '../../components/ui/tabs'; +///: END:ONLY_INCLUDE_IN + +import { + NETWORK_TO_NAME_MAP, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + CHAIN_ID_TO_NETWORK_ID_MAP, + ///: END:ONLY_INCLUDE_IN +} from '../../../shared/constants/network'; import TransactionAlerts from './transaction-alerts'; const renderHeartBeatIfNotInTest = () => @@ -150,6 +162,9 @@ export default class ConfirmTransactionBase extends Component { showBuyModal: PropTypes.func, isBuyableChain: PropTypes.bool, setApproveForAllArg: PropTypes.bool, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightSnaps: PropTypes.arrayOf(PropTypes.object), + ///: END:ONLY_INCLUDE_IN }; state = { @@ -159,6 +174,9 @@ export default class ConfirmTransactionBase extends Component { ethGasPriceWarning: '', editingGas: false, userAcknowledgedGasMissing: false, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + selectedInsightSnapId: this.props.insightSnaps[0]?.id, + ///: END:ONLY_INCLUDE_IN }; componentDidUpdate(prevProps) { @@ -302,6 +320,12 @@ export default class ConfirmTransactionBase extends Component { this.setState({ editingGas: false }); } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + handleSnapSelected(snapId) { + this.setState({ selectedInsightSnapId: snapId }); + } + ///: END:ONLY_INCLUDE_IN + setUserAcknowledgedGasMissing() { this.setState({ userAcknowledgedGasMissing: true }); } @@ -729,6 +753,61 @@ export default class ConfirmTransactionBase extends Component { ); } + ///: BEGIN:ONLY_INCLUDE_IN(flask) + renderInsight() { + const { txData, insightSnaps } = this.props; + const { selectedInsightSnapId } = this.state; + const { txParams, chainId } = txData; + + const selectedSnap = insightSnaps.find( + ({ id }) => id === selectedInsightSnapId, + ); + + const networkId = CHAIN_ID_TO_NETWORK_ID_MAP[chainId]; + const caip2ChainId = `eip155:${networkId ?? stripHexPrefix(chainId)}`; + + if ( + txData.type !== TRANSACTION_TYPES.CONTRACT_INTERACTION || + !insightSnaps.length + ) { + return null; + } + + const dropdownOptions = insightSnaps.map( + ({ id, manifest: { proposedName } }) => ({ + value: id, + name: proposedName, + }), + ); + + return insightSnaps.length > 1 ? ( + this.handleSnapSelected(snapId)} + > + + + ) : ( + + + + ); + } + ///: END:ONLY_INCLUDE_IN + handleEdit() { const { txData, @@ -1111,6 +1190,9 @@ export default class ConfirmTransactionBase extends Component { detailsComponent={this.renderDetails()} dataComponent={this.renderData(functionType)} dataHexComponent={this.renderDataHex(functionType)} + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightComponent={this.renderInsight()} + ///: END:ONLY_INCLUDE_IN contentComponent={contentComponent} nonce={customNonceValue || nonce} unapprovedTxCount={unapprovedTxCount} diff --git a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js index 1599d759b..6df5f6f3d 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -35,6 +35,9 @@ import { getEIP1559V2Enabled, getIsBuyableChain, getEnsResolutionByAddress, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + getInsightSnaps, + ///: END:ONLY_INCLUDE_IN } from '../../selectors'; import { getMostRecentOverviewPage } from '../../ducks/history/history'; import { @@ -198,6 +201,10 @@ const mapStateToProps = (state, ownProps) => { const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state); const eip1559V2Enabled = getEIP1559V2Enabled(state); + ///: BEGIN:ONLY_INCLUDE_IN(flask) + const insightSnaps = getInsightSnaps(state); + ///: END:ONLY_INCLUDE_IN + return { balance, fromAddress, @@ -250,6 +257,9 @@ const mapStateToProps = (state, ownProps) => { chainId, eip1559V2Enabled, isBuyableChain, + ///: BEGIN:ONLY_INCLUDE_IN(flask) + insightSnaps, + ///: END:ONLY_INCLUDE_IN }; }; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index fab590c36..961bf339f 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -62,10 +62,11 @@ import { getLedgerTransportStatus, } from '../ducks/app/app'; import { isEqualCaseInsensitive } from '../../shared/modules/string-utils'; +import { hexToDecimal } from '../../shared/lib/metamask-controller-utils'; ///: BEGIN:ONLY_INCLUDE_IN(flask) import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes'; +import { getPermissionSubjects } from './permissions'; ///: END:ONLY_INCLUDE_IN -import { hexToDecimal } from '../../shared/lib/metamask-controller-utils'; /** * One of the only remaining valid uses of selecting the network subkey of the @@ -754,6 +755,17 @@ export function getSnaps(state) { return state.metamask.snaps; } +export function getInsightSnaps(state) { + const snaps = Object.values(state.metamask.snaps); + const subjects = getPermissionSubjects(state); + + const insightSnaps = snaps.filter( + ({ id }) => subjects[id]?.permissions['endowment:transaction-insight'], + ); + + return insightSnaps; +} + export const getSnapsRouteObjects = createSelector(getSnaps, (snaps) => { return Object.values(snaps).map((snap) => { return { diff --git a/ui/store/actions.js b/ui/store/actions.js index 9ad80ca8c..ee9d64896 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -1056,6 +1056,10 @@ export async function removeSnapError(msgData) { return submitRequestToBackground('removeSnapError', [msgData]); } +export async function handleSnapRequest(args) { + return submitRequestToBackground('handleSnapRequest', [args]); +} + export function dismissNotifications(ids) { return async (dispatch) => { await submitRequestToBackground('dismissNotifications', [ids]);