mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
[FLASK] Snaps Insight (#15814)
Co-authored-by: Frederik Bolding <frederik.bolding@gmail.com> Co-authored-by: Hassan Malik <41640681+hmalik88@users.noreply.github.com>
This commit is contained in:
parent
8b5630025b
commit
c9dc59ea2a
@ -2684,6 +2684,10 @@
|
|||||||
"message": "Show notifications.",
|
"message": "Show notifications.",
|
||||||
"description": "The description for the `snap_notify` permission"
|
"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": {
|
"permission_unknown": {
|
||||||
"message": "Unknown permission: $1",
|
"message": "Unknown permission: $1",
|
||||||
"description": "$1 is the name of a requested permission that is not recognized."
|
"description": "$1 is the name of a requested permission that is not recognized."
|
||||||
@ -3239,6 +3243,10 @@
|
|||||||
"message": "Added on $1 from $2",
|
"message": "Added on $1 from $2",
|
||||||
"description": "$1 represents the date the snap was installed, $2 represents which origin installed the snap."
|
"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": {
|
"snapError": {
|
||||||
"message": "Snap Error: '$1'. Error Code: '$2'",
|
"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."
|
"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": {
|
"snaps": {
|
||||||
"message": "Snaps"
|
"message": "Snaps"
|
||||||
},
|
},
|
||||||
|
"snapsInsightLoading": {
|
||||||
|
"message": "Loading transaction insight..."
|
||||||
|
},
|
||||||
|
"snapsNoInsight": {
|
||||||
|
"message": "The snap didn't return any insight"
|
||||||
|
},
|
||||||
"snapsSettingsDescription": {
|
"snapsSettingsDescription": {
|
||||||
"message": "Manage your Snaps"
|
"message": "Manage your Snaps"
|
||||||
},
|
},
|
||||||
|
@ -1867,6 +1867,10 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
this.controllerMessenger,
|
this.controllerMessenger,
|
||||||
'SnapController:remove',
|
'SnapController:remove',
|
||||||
),
|
),
|
||||||
|
handleSnapRequest: this.controllerMessenger.call.bind(
|
||||||
|
this.controllerMessenger,
|
||||||
|
'SnapController:handleRequest',
|
||||||
|
),
|
||||||
dismissNotifications: this.dismissNotifications.bind(this),
|
dismissNotifications: this.dismissNotifications.bind(this),
|
||||||
markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
|
markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
@import 'edit-gas-fee-popover/edit-gas-tooltip/index';
|
@import 'edit-gas-fee-popover/edit-gas-tooltip/index';
|
||||||
@import 'flask/experimental-area/index';
|
@import 'flask/experimental-area/index';
|
||||||
@import 'flask/snaps-authorship-pill/index';
|
@import 'flask/snaps-authorship-pill/index';
|
||||||
|
@import 'flask/snap-content-footer/index';
|
||||||
@import 'flask/snap-install-warning/index';
|
@import 'flask/snap-install-warning/index';
|
||||||
@import 'flask/snap-remove-warning/index';
|
@import 'flask/snap-remove-warning/index';
|
||||||
@import 'flask/snap-settings-card/index';
|
@import 'flask/snap-settings-card/index';
|
||||||
|
@ -23,6 +23,9 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
dataComponent: PropTypes.node,
|
dataComponent: PropTypes.node,
|
||||||
dataHexComponent: PropTypes.node,
|
dataHexComponent: PropTypes.node,
|
||||||
detailsComponent: PropTypes.node,
|
detailsComponent: PropTypes.node,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent: PropTypes.node,
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
errorKey: PropTypes.string,
|
errorKey: PropTypes.string,
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
hideSubtitle: PropTypes.bool,
|
hideSubtitle: PropTypes.bool,
|
||||||
@ -59,15 +62,37 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
renderContent() {
|
renderContent() {
|
||||||
const { detailsComponent, dataComponent } = this.props;
|
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) {
|
if (detailsComponent && dataComponent) {
|
||||||
return this.renderTabs();
|
return this.renderTabs();
|
||||||
}
|
}
|
||||||
return detailsComponent || dataComponent;
|
|
||||||
|
return (
|
||||||
|
detailsComponent ||
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent ||
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
dataComponent
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTabs() {
|
renderTabs() {
|
||||||
const { t } = this.context;
|
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 (
|
return (
|
||||||
<Tabs>
|
<Tabs>
|
||||||
@ -88,6 +113,12 @@ export default class ConfirmPageContainerContent extends Component {
|
|||||||
{dataHexComponent}
|
{dataHexComponent}
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -82,12 +82,28 @@
|
|||||||
|
|
||||||
color: var(--color-text-alternative);
|
color: var(--color-text-alternative);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin: 0 8px;
|
|
||||||
|
|
||||||
& button {
|
& button {
|
||||||
font-size: unset;
|
font-size: unset;
|
||||||
color: var(--color-text-alternative);
|
color: var(--color-text-alternative);
|
||||||
text-transform: uppercase;
|
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%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,9 @@ export default class ConfirmPageContainer extends Component {
|
|||||||
dataComponent: PropTypes.node,
|
dataComponent: PropTypes.node,
|
||||||
dataHexComponent: PropTypes.node,
|
dataHexComponent: PropTypes.node,
|
||||||
detailsComponent: PropTypes.node,
|
detailsComponent: PropTypes.node,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent: PropTypes.node,
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
tokenAddress: PropTypes.string,
|
tokenAddress: PropTypes.string,
|
||||||
nonce: PropTypes.string,
|
nonce: PropTypes.string,
|
||||||
warning: PropTypes.string,
|
warning: PropTypes.string,
|
||||||
@ -155,6 +158,9 @@ export default class ConfirmPageContainer extends Component {
|
|||||||
isBuyableChain,
|
isBuyableChain,
|
||||||
networkIdentifier,
|
networkIdentifier,
|
||||||
setApproveForAllArg,
|
setApproveForAllArg,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent,
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const showAddToAddressDialog =
|
const showAddToAddressDialog =
|
||||||
@ -242,6 +248,9 @@ export default class ConfirmPageContainer extends Component {
|
|||||||
detailsComponent={detailsComponent}
|
detailsComponent={detailsComponent}
|
||||||
dataComponent={dataComponent}
|
dataComponent={dataComponent}
|
||||||
dataHexComponent={dataHexComponent}
|
dataHexComponent={dataHexComponent}
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent={insightComponent}
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
errorKey={errorKey}
|
errorKey={errorKey}
|
||||||
tokenAddress={tokenAddress}
|
tokenAddress={tokenAddress}
|
||||||
|
1
ui/components/app/confirm-page-container/flask/index.js
Normal file
1
ui/components/app/confirm-page-container/flask/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { SnapInsight } from './snap-insight';
|
@ -0,0 +1,3 @@
|
|||||||
|
.snap-insight {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
111
ui/components/app/confirm-page-container/flask/snap-insight.js
Normal file
111
ui/components/app/confirm-page-container/flask/snap-insight.js
Normal file
@ -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 (
|
||||||
|
<Box
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
height="full"
|
||||||
|
marginTop={hasNoData && 12}
|
||||||
|
marginBottom={hasNoData && 12}
|
||||||
|
alignItems={hasNoData && ALIGN_ITEMS.CENTER}
|
||||||
|
justifyContent={hasNoData && JUSTIFY_CONTENT.CENTER}
|
||||||
|
textAlign={hasNoData && TEXT_ALIGN.CENTER}
|
||||||
|
className="snap-insight"
|
||||||
|
>
|
||||||
|
{data ? (
|
||||||
|
<Box
|
||||||
|
height="full"
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
className="snap-insight__container"
|
||||||
|
>
|
||||||
|
{Object.keys(data).length ? (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||||
|
paddingTop={0}
|
||||||
|
paddingRight={6}
|
||||||
|
paddingBottom={3}
|
||||||
|
paddingLeft={6}
|
||||||
|
className="snap-insight__container__data"
|
||||||
|
>
|
||||||
|
{Object.keys(data).map((key, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<Typography
|
||||||
|
fontWeight="bold"
|
||||||
|
marginTop={3}
|
||||||
|
variant={TYPOGRAPHY.H6}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant={TYPOGRAPHY.H6}>{data[key]}</Typography>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<SnapContentFooter
|
||||||
|
snapName={selectedSnap.manifest.proposedName}
|
||||||
|
snapId={selectedSnap.id}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Typography color={COLORS.TEXT_ALTERNATIVE} variant={TYPOGRAPHY.H6}>
|
||||||
|
{t('snapsNoInsight')}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Preloader size={40} />
|
||||||
|
<Typography
|
||||||
|
marginTop={3}
|
||||||
|
color={COLORS.TEXT_ALTERNATIVE}
|
||||||
|
variant={TYPOGRAPHY.H6}
|
||||||
|
>
|
||||||
|
{t('snapsInsightLoading')}
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SnapInsight.propTypes = {
|
||||||
|
/*
|
||||||
|
* The transaction data object
|
||||||
|
*/
|
||||||
|
transaction: PropTypes.object,
|
||||||
|
/*
|
||||||
|
* CAIP2 Chain ID
|
||||||
|
*/
|
||||||
|
chainId: PropTypes.string,
|
||||||
|
/*
|
||||||
|
* The insight snap selected
|
||||||
|
*/
|
||||||
|
selectedSnap: PropTypes.object,
|
||||||
|
};
|
@ -7,3 +7,6 @@ export {
|
|||||||
default as ConfirmPageContainerContent,
|
default as ConfirmPageContainerContent,
|
||||||
ConfirmPageContainerSummary,
|
ConfirmPageContainerSummary,
|
||||||
} from './confirm-page-container-content';
|
} from './confirm-page-container-content';
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
export { SnapInsight } from './flask/snap-insight';
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
@ -2,6 +2,9 @@
|
|||||||
@import 'confirm-page-container-header/index';
|
@import 'confirm-page-container-header/index';
|
||||||
@import 'confirm-detail-row/index';
|
@import 'confirm-detail-row/index';
|
||||||
@import 'confirm-page-container-navigation/index';
|
@import 'confirm-page-container-navigation/index';
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
@import 'flask/index';
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
.confirm-page-container {
|
.confirm-page-container {
|
||||||
&__dialog {
|
&__dialog {
|
||||||
|
1
ui/components/app/flask/snap-content-footer/index.js
Normal file
1
ui/components/app/flask/snap-content-footer/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './snap-content-footer';
|
13
ui/components/app/flask/snap-content-footer/index.scss
Normal file
13
ui/components/app/flask/snap-content-footer/index.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 (
|
||||||
|
<Box
|
||||||
|
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||||
|
alignItems={ALIGN_ITEMS.CENTER}
|
||||||
|
paddingTop={4}
|
||||||
|
paddingBottom={4}
|
||||||
|
className="snap-content-footer"
|
||||||
|
>
|
||||||
|
<i className="fas fa-exclamation-circle fa-sm" />
|
||||||
|
<Typography color={COLORS.TEXT_MUTED} variant={TYPOGRAPHY.H7}>
|
||||||
|
{t('snapContent', [
|
||||||
|
<Button type="inline" onClick={handleNameClick} key="button">
|
||||||
|
{snapName}
|
||||||
|
</Button>,
|
||||||
|
])}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapContentFooter.propTypes = {
|
||||||
|
/**
|
||||||
|
* The name of the snap who's content is displayed
|
||||||
|
*/
|
||||||
|
snapName: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* The id of the snap
|
||||||
|
*/
|
||||||
|
snapId: PropTypes.string,
|
||||||
|
};
|
@ -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) => <SnapContentFooter {...args} />;
|
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default';
|
63
ui/components/ui/tabs/dropdown-tab/dropdown-tab.js
Normal file
63
ui/components/ui/tabs/dropdown-tab/dropdown-tab.js
Normal file
@ -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 (
|
||||||
|
<li
|
||||||
|
className={classnames('tab', className, {
|
||||||
|
'tab--active': isActive,
|
||||||
|
[activeClassName]: activeClassName && isActive,
|
||||||
|
})}
|
||||||
|
data-testid={dataTestId}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onClick(tabIndex);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dropdown
|
||||||
|
options={options}
|
||||||
|
selectedOption={selectedOption}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
3
ui/components/ui/tabs/dropdown-tab/index.js
Normal file
3
ui/components/ui/tabs/dropdown-tab/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { DropdownTab } from './dropdown-tab';
|
||||||
|
|
||||||
|
export default DropdownTab;
|
23
ui/components/ui/tabs/dropdown-tab/index.scss
Normal file
23
ui/components/ui/tabs/dropdown-tab/index.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import Tabs from './tabs.component';
|
import Tabs from './tabs.component';
|
||||||
import Tab from './tab';
|
import Tab from './tab';
|
||||||
|
import DropdownTab from './dropdown-tab';
|
||||||
|
|
||||||
export { Tabs, Tab };
|
export { Tabs, Tab, DropdownTab };
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@import 'tab/index';
|
@import 'tab/index';
|
||||||
|
@import 'dropdown-tab/index';
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import DropdownTab from './dropdown-tab';
|
||||||
import Tab from './tab/tab.component';
|
import Tab from './tab/tab.component';
|
||||||
import Tabs from './tabs.component';
|
import Tabs from './tabs.component';
|
||||||
|
|
||||||
@ -41,6 +42,14 @@ export const DefaultStory = (args) => {
|
|||||||
onTabClick={args.onTabClick}
|
onTabClick={args.onTabClick}
|
||||||
>
|
>
|
||||||
{args.tabs.map((tabProps, i) => renderTab(tabProps, i))}
|
{args.tabs.map((tabProps, i) => renderTab(tabProps, i))}
|
||||||
|
<DropdownTab
|
||||||
|
options={[
|
||||||
|
{ name: 'Insight Snap', value: 'Insight Snap' },
|
||||||
|
{ name: 'Tenderly Insight', value: 'Tenderly Insight' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
This is a dropdown Tab
|
||||||
|
</DropdownTab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -94,6 +94,11 @@ const PERMISSION_DESCRIPTIONS = deepFreeze({
|
|||||||
leftIcon: 'fas fa-infinity',
|
leftIcon: 'fas fa-infinity',
|
||||||
rightIcon: null,
|
rightIcon: null,
|
||||||
},
|
},
|
||||||
|
[EndowmentPermissions['endowment:transaction-insight']]: {
|
||||||
|
label: (t) => t('permission_transactionInsight'),
|
||||||
|
leftIcon: 'fas fa-info',
|
||||||
|
rightIcon: null,
|
||||||
|
},
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: END:ONLY_INCLUDE_IN
|
||||||
[UNKNOWN_PERMISSION]: {
|
[UNKNOWN_PERMISSION]: {
|
||||||
label: (t, permissionName) =>
|
label: (t, permissionName) =>
|
||||||
|
37
ui/hooks/flask/useTransactionInsightSnap.js
Normal file
37
ui/hooks/flask/useTransactionInsightSnap.js
Normal file
@ -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;
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
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 ConfirmPageContainer from '../../components/app/confirm-page-container';
|
||||||
import TransactionDecoding from '../../components/app/transaction-decoding';
|
import TransactionDecoding from '../../components/app/transaction-decoding';
|
||||||
import { isBalanceSufficient } from '../send/send.utils';
|
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 GasTiming from '../../components/app/gas-timing/gas-timing.component';
|
||||||
import LedgerInstructionField from '../../components/app/ledger-instruction-field';
|
import LedgerInstructionField from '../../components/app/ledger-instruction-field';
|
||||||
import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message';
|
import MultiLayerFeeMessage from '../../components/app/multilayer-fee-message';
|
||||||
|
import Typography from '../../components/ui/typography/typography';
|
||||||
import {
|
import {
|
||||||
COLORS,
|
COLORS,
|
||||||
FONT_STYLE,
|
FONT_STYLE,
|
||||||
@ -55,12 +58,21 @@ import {
|
|||||||
removePollingTokenFromAppState,
|
removePollingTokenFromAppState,
|
||||||
} from '../../store/actions';
|
} from '../../store/actions';
|
||||||
|
|
||||||
import Typography from '../../components/ui/typography/typography';
|
|
||||||
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
|
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 { hexToDecimal } from '../../../shared/lib/metamask-controller-utils';
|
||||||
import { hexWEIToDecGWEI } from '../../../shared/lib/transactions-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';
|
import TransactionAlerts from './transaction-alerts';
|
||||||
|
|
||||||
const renderHeartBeatIfNotInTest = () =>
|
const renderHeartBeatIfNotInTest = () =>
|
||||||
@ -150,6 +162,9 @@ export default class ConfirmTransactionBase extends Component {
|
|||||||
showBuyModal: PropTypes.func,
|
showBuyModal: PropTypes.func,
|
||||||
isBuyableChain: PropTypes.bool,
|
isBuyableChain: PropTypes.bool,
|
||||||
setApproveForAllArg: PropTypes.bool,
|
setApproveForAllArg: PropTypes.bool,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightSnaps: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@ -159,6 +174,9 @@ export default class ConfirmTransactionBase extends Component {
|
|||||||
ethGasPriceWarning: '',
|
ethGasPriceWarning: '',
|
||||||
editingGas: false,
|
editingGas: false,
|
||||||
userAcknowledgedGasMissing: false,
|
userAcknowledgedGasMissing: false,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
selectedInsightSnapId: this.props.insightSnaps[0]?.id,
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -302,6 +320,12 @@ export default class ConfirmTransactionBase extends Component {
|
|||||||
this.setState({ editingGas: false });
|
this.setState({ editingGas: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
handleSnapSelected(snapId) {
|
||||||
|
this.setState({ selectedInsightSnapId: snapId });
|
||||||
|
}
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
setUserAcknowledgedGasMissing() {
|
setUserAcknowledgedGasMissing() {
|
||||||
this.setState({ userAcknowledgedGasMissing: true });
|
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 ? (
|
||||||
|
<DropdownTab
|
||||||
|
className="confirm-page-container-content__tab"
|
||||||
|
options={dropdownOptions}
|
||||||
|
selectedOption={selectedInsightSnapId}
|
||||||
|
onChange={(snapId) => this.handleSnapSelected(snapId)}
|
||||||
|
>
|
||||||
|
<SnapInsight
|
||||||
|
transaction={txParams}
|
||||||
|
chainId={caip2ChainId}
|
||||||
|
selectedSnap={selectedSnap}
|
||||||
|
/>
|
||||||
|
</DropdownTab>
|
||||||
|
) : (
|
||||||
|
<Tab
|
||||||
|
className="confirm-page-container-content__tab"
|
||||||
|
name={selectedSnap.manifest.proposedName}
|
||||||
|
>
|
||||||
|
<SnapInsight
|
||||||
|
transaction={txParams}
|
||||||
|
chainId={caip2ChainId}
|
||||||
|
selectedSnap={selectedSnap}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
handleEdit() {
|
handleEdit() {
|
||||||
const {
|
const {
|
||||||
txData,
|
txData,
|
||||||
@ -1111,6 +1190,9 @@ export default class ConfirmTransactionBase extends Component {
|
|||||||
detailsComponent={this.renderDetails()}
|
detailsComponent={this.renderDetails()}
|
||||||
dataComponent={this.renderData(functionType)}
|
dataComponent={this.renderData(functionType)}
|
||||||
dataHexComponent={this.renderDataHex(functionType)}
|
dataHexComponent={this.renderDataHex(functionType)}
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightComponent={this.renderInsight()}
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
contentComponent={contentComponent}
|
contentComponent={contentComponent}
|
||||||
nonce={customNonceValue || nonce}
|
nonce={customNonceValue || nonce}
|
||||||
unapprovedTxCount={unapprovedTxCount}
|
unapprovedTxCount={unapprovedTxCount}
|
||||||
|
@ -35,6 +35,9 @@ import {
|
|||||||
getEIP1559V2Enabled,
|
getEIP1559V2Enabled,
|
||||||
getIsBuyableChain,
|
getIsBuyableChain,
|
||||||
getEnsResolutionByAddress,
|
getEnsResolutionByAddress,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
getInsightSnaps,
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
import { getMostRecentOverviewPage } from '../../ducks/history/history';
|
||||||
import {
|
import {
|
||||||
@ -198,6 +201,10 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state);
|
const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state);
|
||||||
const eip1559V2Enabled = getEIP1559V2Enabled(state);
|
const eip1559V2Enabled = getEIP1559V2Enabled(state);
|
||||||
|
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
const insightSnaps = getInsightSnaps(state);
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
|
|
||||||
return {
|
return {
|
||||||
balance,
|
balance,
|
||||||
fromAddress,
|
fromAddress,
|
||||||
@ -250,6 +257,9 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
chainId,
|
chainId,
|
||||||
eip1559V2Enabled,
|
eip1559V2Enabled,
|
||||||
isBuyableChain,
|
isBuyableChain,
|
||||||
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
|
insightSnaps,
|
||||||
|
///: END:ONLY_INCLUDE_IN
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -62,10 +62,11 @@ import {
|
|||||||
getLedgerTransportStatus,
|
getLedgerTransportStatus,
|
||||||
} from '../ducks/app/app';
|
} from '../ducks/app/app';
|
||||||
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
|
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
|
||||||
|
import { hexToDecimal } from '../../shared/lib/metamask-controller-utils';
|
||||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||||
import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes';
|
import { SNAPS_VIEW_ROUTE } from '../helpers/constants/routes';
|
||||||
|
import { getPermissionSubjects } from './permissions';
|
||||||
///: END:ONLY_INCLUDE_IN
|
///: 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
|
* 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;
|
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) => {
|
export const getSnapsRouteObjects = createSelector(getSnaps, (snaps) => {
|
||||||
return Object.values(snaps).map((snap) => {
|
return Object.values(snaps).map((snap) => {
|
||||||
return {
|
return {
|
||||||
|
@ -1056,6 +1056,10 @@ export async function removeSnapError(msgData) {
|
|||||||
return submitRequestToBackground('removeSnapError', [msgData]);
|
return submitRequestToBackground('removeSnapError', [msgData]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleSnapRequest(args) {
|
||||||
|
return submitRequestToBackground('handleSnapRequest', [args]);
|
||||||
|
}
|
||||||
|
|
||||||
export function dismissNotifications(ids) {
|
export function dismissNotifications(ids) {
|
||||||
return async (dispatch) => {
|
return async (dispatch) => {
|
||||||
await submitRequestToBackground('dismissNotifications', [ids]);
|
await submitRequestToBackground('dismissNotifications', [ids]);
|
||||||
|
Loading…
Reference in New Issue
Block a user