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]);