mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 09:57:02 +01:00
f829f0069d
* Add support for snap authorship component at the top of PermissionConnect * Add PermissionCellOptions * Add details popover * Add action for revoking dynamic permissions * Improve UI and revoke logic * Better eth_accounts screen support * Fix tests * Fix lint * More linting fixes * Fix missing fence * Add another fence * Unnest permission page to fix weird CSS issues * Hide footer on permissions connect when using a snap
551 lines
18 KiB
JavaScript
551 lines
18 KiB
JavaScript
import PropTypes from 'prop-types';
|
|
import React, { Component } from 'react';
|
|
import { Switch, Route } from 'react-router-dom';
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
import { ethErrors, serializeError } from 'eth-rpc-errors';
|
|
import { SubjectType } from '@metamask/subject-metadata-controller';
|
|
///: END:ONLY_INCLUDE_IN
|
|
import { getEnvironmentType } from '../../../app/scripts/lib/util';
|
|
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
|
|
import { MILLISECOND } from '../../../shared/constants/time';
|
|
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
|
import PermissionPageContainer from '../../components/app/permission-page-container';
|
|
import {
|
|
Box,
|
|
Icon,
|
|
IconName,
|
|
IconSize,
|
|
} from '../../components/component-library';
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
import SnapAuthorshipHeader from '../../components/app/snaps/snap-authorship-header/snap-authorship-header';
|
|
///: END:ONLY_INCLUDE_IN
|
|
import ChooseAccount from './choose-account';
|
|
import PermissionsRedirect from './redirect';
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
import SnapsConnect from './snaps/snaps-connect';
|
|
import SnapInstall from './snaps/snap-install';
|
|
import SnapUpdate from './snaps/snap-update';
|
|
import SnapResult from './snaps/snap-result';
|
|
///: END:ONLY_INCLUDE_IN
|
|
|
|
const APPROVE_TIMEOUT = MILLISECOND * 1200;
|
|
|
|
export default class PermissionConnect extends Component {
|
|
static propTypes = {
|
|
approvePermissionsRequest: PropTypes.func.isRequired,
|
|
rejectPermissionsRequest: PropTypes.func.isRequired,
|
|
getRequestAccountTabIds: PropTypes.func.isRequired,
|
|
accounts: PropTypes.array.isRequired,
|
|
currentAddress: PropTypes.string.isRequired,
|
|
origin: PropTypes.string,
|
|
showNewAccountModal: PropTypes.func.isRequired,
|
|
newAccountNumber: PropTypes.number.isRequired,
|
|
nativeCurrency: PropTypes.string,
|
|
permissionsRequest: PropTypes.object,
|
|
addressLastConnectedMap: PropTypes.object.isRequired,
|
|
lastConnectedInfo: PropTypes.object.isRequired,
|
|
permissionsRequestId: PropTypes.string,
|
|
history: PropTypes.object.isRequired,
|
|
connectPath: PropTypes.string.isRequired,
|
|
confirmPermissionPath: PropTypes.string.isRequired,
|
|
requestType: PropTypes.string.isRequired,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsConnectPath: PropTypes.string.isRequired,
|
|
snapInstallPath: PropTypes.string.isRequired,
|
|
snapUpdatePath: PropTypes.string.isRequired,
|
|
snapResultPath: PropTypes.string.isRequired,
|
|
requestState: PropTypes.object.isRequired,
|
|
approvePendingApproval: PropTypes.func.isRequired,
|
|
rejectPendingApproval: PropTypes.func.isRequired,
|
|
setSnapsInstallPrivacyWarningShownStatus: PropTypes.func.isRequired,
|
|
snapsInstallPrivacyWarningShown: PropTypes.bool.isRequired,
|
|
///: END:ONLY_INCLUDE_IN
|
|
hideTopBar: PropTypes.bool,
|
|
totalPages: PropTypes.string.isRequired,
|
|
page: PropTypes.string.isRequired,
|
|
targetSubjectMetadata: PropTypes.shape({
|
|
extensionId: PropTypes.string,
|
|
iconUrl: PropTypes.string,
|
|
name: PropTypes.string,
|
|
origin: PropTypes.string,
|
|
subjectType: PropTypes.string,
|
|
}),
|
|
isRequestingAccounts: PropTypes.bool.isRequired,
|
|
};
|
|
|
|
static defaultProps = {
|
|
origin: '',
|
|
nativeCurrency: '',
|
|
permissionsRequest: undefined,
|
|
permissionsRequestId: '',
|
|
};
|
|
|
|
static contextTypes = {
|
|
t: PropTypes.func,
|
|
};
|
|
|
|
state = {
|
|
redirecting: false,
|
|
selectedAccountAddresses: new Set([this.props.currentAddress]),
|
|
permissionsApproved: null,
|
|
origin: this.props.origin,
|
|
targetSubjectMetadata: this.props.targetSubjectMetadata || {},
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsInstallPrivacyWarningShown: this.props.snapsInstallPrivacyWarningShown,
|
|
///: END:ONLY_INCLUDE_IN
|
|
};
|
|
|
|
beforeUnload = () => {
|
|
const { permissionsRequestId, rejectPermissionsRequest } = this.props;
|
|
const { permissionsApproved } = this.state;
|
|
|
|
if (permissionsApproved === null && permissionsRequestId) {
|
|
rejectPermissionsRequest(permissionsRequestId);
|
|
}
|
|
};
|
|
|
|
removeBeforeUnload = () => {
|
|
const environmentType = getEnvironmentType();
|
|
if (environmentType === ENVIRONMENT_TYPE_NOTIFICATION) {
|
|
window.removeEventListener('beforeunload', this.beforeUnload);
|
|
}
|
|
};
|
|
|
|
componentDidMount() {
|
|
const {
|
|
connectPath,
|
|
confirmPermissionPath,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsConnectPath,
|
|
snapInstallPath,
|
|
snapUpdatePath,
|
|
snapResultPath,
|
|
requestType,
|
|
///: END:ONLY_INCLUDE_IN
|
|
getRequestAccountTabIds,
|
|
permissionsRequest,
|
|
history,
|
|
isRequestingAccounts,
|
|
} = this.props;
|
|
getRequestAccountTabIds();
|
|
|
|
if (!permissionsRequest) {
|
|
history.replace(DEFAULT_ROUTE);
|
|
return;
|
|
}
|
|
|
|
const environmentType = getEnvironmentType();
|
|
if (environmentType === ENVIRONMENT_TYPE_NOTIFICATION) {
|
|
window.addEventListener('beforeunload', this.beforeUnload);
|
|
}
|
|
|
|
if (history.location.pathname === connectPath && !isRequestingAccounts) {
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
|
|
switch (requestType) {
|
|
case 'wallet_installSnap':
|
|
history.replace(snapInstallPath);
|
|
break;
|
|
case 'wallet_updateSnap':
|
|
history.replace(snapUpdatePath);
|
|
break;
|
|
case 'wallet_installSnapResult':
|
|
history.replace(snapResultPath);
|
|
break;
|
|
case 'wallet_connectSnaps':
|
|
history.replace(snapsConnectPath);
|
|
break;
|
|
default:
|
|
///: END:ONLY_INCLUDE_IN
|
|
history.replace(confirmPermissionPath);
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
}
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
const { permissionsRequest, targetSubjectMetadata } = props;
|
|
const { targetSubjectMetadata: savedMetadata } = state;
|
|
|
|
if (
|
|
permissionsRequest &&
|
|
savedMetadata.origin !== targetSubjectMetadata?.origin
|
|
) {
|
|
return { targetSubjectMetadata };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const { permissionsRequest, lastConnectedInfo } = this.props;
|
|
const { redirecting, origin } = this.state;
|
|
|
|
if (!permissionsRequest && prevProps.permissionsRequest && !redirecting) {
|
|
const accountsLastApprovedTime =
|
|
lastConnectedInfo[origin]?.lastApproved || 0;
|
|
const initialAccountsLastApprovedTime =
|
|
prevProps.lastConnectedInfo[origin]?.lastApproved || 0;
|
|
|
|
const approved =
|
|
accountsLastApprovedTime > initialAccountsLastApprovedTime;
|
|
this.redirect(approved);
|
|
}
|
|
}
|
|
|
|
selectAccounts = (addresses) => {
|
|
const {
|
|
confirmPermissionPath,
|
|
requestType,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsConnectPath,
|
|
snapInstallPath,
|
|
snapUpdatePath,
|
|
snapResultPath,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = this.props;
|
|
this.setState(
|
|
{
|
|
selectedAccountAddresses: addresses,
|
|
},
|
|
() => {
|
|
switch (requestType) {
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
case 'wallet_installSnap':
|
|
this.props.history.push(snapInstallPath);
|
|
break;
|
|
case 'wallet_updateSnap':
|
|
this.props.history.push(snapUpdatePath);
|
|
break;
|
|
case 'wallet_installSnapResult':
|
|
this.props.history.push(snapResultPath);
|
|
break;
|
|
case 'wallet_connectSnaps':
|
|
this.props.history.replace(snapsConnectPath);
|
|
break;
|
|
///: END:ONLY_INCLUDE_IN
|
|
default:
|
|
this.props.history.push(confirmPermissionPath);
|
|
}
|
|
},
|
|
);
|
|
};
|
|
|
|
redirect(approved) {
|
|
const {
|
|
history,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
permissionsRequest,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = this.props;
|
|
|
|
let shouldRedirect = true;
|
|
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
const isRequestingSnap =
|
|
permissionsRequest?.permissions &&
|
|
Object.keys(permissionsRequest.permissions).includes('wallet_snap');
|
|
|
|
shouldRedirect = !isRequestingSnap;
|
|
///: END:ONLY_INCLUDE_IN
|
|
|
|
this.setState({
|
|
redirecting: shouldRedirect,
|
|
permissionsApproved: approved,
|
|
});
|
|
this.removeBeforeUnload();
|
|
|
|
if (shouldRedirect && approved) {
|
|
setTimeout(() => history.push(DEFAULT_ROUTE), APPROVE_TIMEOUT);
|
|
return;
|
|
}
|
|
history.push(DEFAULT_ROUTE);
|
|
}
|
|
|
|
cancelPermissionsRequest = async (requestId) => {
|
|
const { rejectPermissionsRequest } = this.props;
|
|
|
|
if (requestId) {
|
|
await rejectPermissionsRequest(requestId);
|
|
this.redirect(false);
|
|
}
|
|
};
|
|
|
|
goBack() {
|
|
const { history, connectPath } = this.props;
|
|
history.push(connectPath);
|
|
}
|
|
|
|
renderTopBar() {
|
|
const {
|
|
redirecting,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
targetSubjectMetadata,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = this.state;
|
|
const { page, isRequestingAccounts, totalPages } = this.props;
|
|
const { t } = this.context;
|
|
return redirecting ? null : (
|
|
<Box
|
|
style={{
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
marginBottom:
|
|
targetSubjectMetadata.subjectType === SubjectType.Snap && '14px',
|
|
boxShadow:
|
|
targetSubjectMetadata.subjectType === SubjectType.Snap &&
|
|
'var(--shadow-size-lg) var(--color-shadow-default)',
|
|
///: END:ONLY_INCLUDE_IN
|
|
}}
|
|
>
|
|
<div className="permissions-connect__top-bar">
|
|
{page === '2' && isRequestingAccounts ? (
|
|
<div
|
|
className="permissions-connect__back"
|
|
onClick={() => this.goBack()}
|
|
>
|
|
<Icon
|
|
name={IconName.ArrowLeft}
|
|
marginInlineEnd={1}
|
|
size={IconSize.Xs}
|
|
/>
|
|
{t('back')}
|
|
</div>
|
|
) : null}
|
|
{isRequestingAccounts ? (
|
|
<div className="permissions-connect__page-count">
|
|
{t('xOfY', [page, totalPages])}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
{
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
targetSubjectMetadata.subjectType === SubjectType.Snap && (
|
|
<SnapAuthorshipHeader
|
|
snapId={targetSubjectMetadata.origin}
|
|
boxShadow="none"
|
|
/>
|
|
)
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
approvePermissionsRequest,
|
|
accounts,
|
|
showNewAccountModal,
|
|
newAccountNumber,
|
|
nativeCurrency,
|
|
permissionsRequest,
|
|
addressLastConnectedMap,
|
|
permissionsRequestId,
|
|
connectPath,
|
|
confirmPermissionPath,
|
|
hideTopBar,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsConnectPath,
|
|
snapInstallPath,
|
|
snapUpdatePath,
|
|
snapResultPath,
|
|
requestState,
|
|
approvePendingApproval,
|
|
rejectPendingApproval,
|
|
setSnapsInstallPrivacyWarningShownStatus,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = this.props;
|
|
const {
|
|
selectedAccountAddresses,
|
|
permissionsApproved,
|
|
redirecting,
|
|
targetSubjectMetadata,
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsInstallPrivacyWarningShown,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = this.state;
|
|
|
|
return (
|
|
<div className="permissions-connect">
|
|
{!hideTopBar && this.renderTopBar()}
|
|
{redirecting && permissionsApproved ? (
|
|
<PermissionsRedirect subjectMetadata={targetSubjectMetadata} />
|
|
) : (
|
|
<Switch>
|
|
<Route
|
|
path={connectPath}
|
|
exact
|
|
render={() => (
|
|
<ChooseAccount
|
|
accounts={accounts}
|
|
nativeCurrency={nativeCurrency}
|
|
selectAccounts={(addresses) => this.selectAccounts(addresses)}
|
|
selectNewAccountViaModal={(handleAccountClick) => {
|
|
showNewAccountModal({
|
|
onCreateNewAccount: (address) =>
|
|
handleAccountClick(address),
|
|
newAccountNumber,
|
|
});
|
|
}}
|
|
addressLastConnectedMap={addressLastConnectedMap}
|
|
cancelPermissionsRequest={(requestId) =>
|
|
this.cancelPermissionsRequest(requestId)
|
|
}
|
|
permissionsRequestId={permissionsRequestId}
|
|
selectedAccountAddresses={selectedAccountAddresses}
|
|
targetSubjectMetadata={targetSubjectMetadata}
|
|
/>
|
|
)}
|
|
/>
|
|
<Route
|
|
path={confirmPermissionPath}
|
|
exact
|
|
render={() => (
|
|
<PermissionPageContainer
|
|
request={permissionsRequest || {}}
|
|
approvePermissionsRequest={(...args) => {
|
|
approvePermissionsRequest(...args);
|
|
this.redirect(true);
|
|
}}
|
|
rejectPermissionsRequest={(requestId) =>
|
|
this.cancelPermissionsRequest(requestId)
|
|
}
|
|
selectedIdentities={accounts.filter((account) =>
|
|
selectedAccountAddresses.has(account.address),
|
|
)}
|
|
targetSubjectMetadata={targetSubjectMetadata}
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
snapsInstallPrivacyWarningShown={
|
|
snapsInstallPrivacyWarningShown
|
|
}
|
|
setSnapsInstallPrivacyWarningShownStatus={
|
|
setSnapsInstallPrivacyWarningShownStatus
|
|
}
|
|
///: END:ONLY_INCLUDE_IN
|
|
/>
|
|
)}
|
|
/>
|
|
{
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
}
|
|
<Route
|
|
path={snapsConnectPath}
|
|
exact
|
|
render={() => (
|
|
<SnapsConnect
|
|
request={permissionsRequest || {}}
|
|
approveConnection={(...args) => {
|
|
approvePermissionsRequest(...args);
|
|
this.redirect(true);
|
|
}}
|
|
rejectConnection={(requestId) =>
|
|
this.cancelPermissionsRequest(requestId)
|
|
}
|
|
targetSubjectMetadata={targetSubjectMetadata}
|
|
snapsInstallPrivacyWarningShown={
|
|
snapsInstallPrivacyWarningShown
|
|
}
|
|
setSnapsInstallPrivacyWarningShownStatus={
|
|
setSnapsInstallPrivacyWarningShownStatus
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
{
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
{
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
}
|
|
<Route
|
|
path={snapInstallPath}
|
|
exact
|
|
render={() => (
|
|
<SnapInstall
|
|
request={permissionsRequest || {}}
|
|
requestState={requestState || {}}
|
|
approveSnapInstall={(requestId) => {
|
|
approvePendingApproval(requestId, {
|
|
...permissionsRequest,
|
|
permissions: requestState.permissions,
|
|
approvedAccounts: [...selectedAccountAddresses],
|
|
});
|
|
this.setState({ permissionsApproved: true });
|
|
}}
|
|
rejectSnapInstall={(requestId) => {
|
|
rejectPendingApproval(
|
|
requestId,
|
|
serializeError(ethErrors.provider.userRejectedRequest()),
|
|
);
|
|
this.setState({ permissionsApproved: true });
|
|
this.removeBeforeUnload();
|
|
}}
|
|
targetSubjectMetadata={targetSubjectMetadata}
|
|
/>
|
|
)}
|
|
/>
|
|
{
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
{
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
}
|
|
<Route
|
|
path={snapUpdatePath}
|
|
exact
|
|
render={() => (
|
|
<SnapUpdate
|
|
request={permissionsRequest || {}}
|
|
requestState={requestState || {}}
|
|
approveSnapUpdate={(requestId) => {
|
|
approvePendingApproval(requestId, {
|
|
...permissionsRequest,
|
|
permissions: requestState.permissions,
|
|
approvedAccounts: [...selectedAccountAddresses],
|
|
});
|
|
this.setState({ permissionsApproved: true });
|
|
}}
|
|
rejectSnapUpdate={(requestId) => {
|
|
rejectPendingApproval(
|
|
requestId,
|
|
serializeError(ethErrors.provider.userRejectedRequest()),
|
|
);
|
|
this.setState({ permissionsApproved: false });
|
|
this.removeBeforeUnload();
|
|
}}
|
|
targetSubjectMetadata={targetSubjectMetadata}
|
|
/>
|
|
)}
|
|
/>
|
|
{
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
{
|
|
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
|
}
|
|
<Route
|
|
path={snapResultPath}
|
|
exact
|
|
render={() => (
|
|
<SnapResult
|
|
request={permissionsRequest || {}}
|
|
requestState={requestState || {}}
|
|
approveSnapResult={(requestId) => {
|
|
approvePendingApproval(requestId);
|
|
this.setState({ permissionsApproved: true });
|
|
this.removeBeforeUnload();
|
|
}}
|
|
targetSubjectMetadata={targetSubjectMetadata}
|
|
/>
|
|
)}
|
|
/>
|
|
{
|
|
///: END:ONLY_INCLUDE_IN
|
|
}
|
|
</Switch>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
}
|