1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.component.js
Mark Stacey 22f931e6b2
Prevent automatic rejection of confirmations (#13194)
* Prevent automatic rejection of confirmations

Confirmations are now only automatically rejected if a user explicitly
closes the notification window. If we close the window programmatically
because there are no notifications left to show, nothing gets rejected.

This partially avoids a race condition where a confirmation gets
rejected automatically without the user having seen the confirmation
first. This could happen if the confirmation was processed just as the
notification window was being closed.

It's still possible for a confirmation that the user has never seen to
get rejected as a result of the user closing the window. But at least
now it's no longer possible for a confirmation to get rejected in this
manner after the user resolves the last confirmation in the queue.

* Fix bug that prevented automatic closure detection

All windows were being detected as explicit window closures,
essentially just as they were previously, because this variable was
cleared too soon.

* Re-open popup when necessary

After the window is automatically closed, a confirmation may have been
queued up while the window was closing. If so, the popup is now re-
opened.
2022-01-05 13:39:19 -03:30

198 lines
6.1 KiB
JavaScript

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Button from '../../components/ui/button';
import Identicon from '../../components/ui/identicon';
import TokenBalance from '../../components/ui/token-balance';
import { isEqualCaseInsensitive } from '../../helpers/utils/util';
export default class ConfirmAddSuggestedToken extends Component {
static contextTypes = {
t: PropTypes.func,
trackEvent: PropTypes.func,
};
static propTypes = {
history: PropTypes.object,
acceptWatchAsset: PropTypes.func,
rejectWatchAsset: PropTypes.func,
mostRecentOverviewPage: PropTypes.string.isRequired,
suggestedAssets: PropTypes.array,
tokens: PropTypes.array,
};
componentDidMount() {
this._checksuggestedAssets();
}
componentDidUpdate() {
this._checksuggestedAssets();
}
_checksuggestedAssets() {
const {
mostRecentOverviewPage,
suggestedAssets = [],
history,
} = this.props;
if (suggestedAssets.length > 0) {
return;
}
history.push(mostRecentOverviewPage);
}
getTokenName(name, symbol) {
return typeof name === 'undefined' ? symbol : `${name} (${symbol})`;
}
render() {
const {
suggestedAssets,
tokens,
rejectWatchAsset,
history,
mostRecentOverviewPage,
acceptWatchAsset,
} = this.props;
const hasTokenDuplicates = this.checkTokenDuplicates(
suggestedAssets,
tokens,
);
const reusesName = this.checkNameReuse(suggestedAssets, tokens);
return (
<div className="page-container">
<div className="page-container__header">
<div className="page-container__title">
{this.context.t('addSuggestedTokens')}
</div>
<div className="page-container__subtitle">
{this.context.t('likeToImportTokens')}
</div>
{hasTokenDuplicates ? (
<div className="warning">{this.context.t('knownTokenWarning')}</div>
) : null}
{reusesName ? (
<div className="warning">
{this.context.t('reusedTokenNameWarning')}
</div>
) : null}
</div>
<div className="page-container__content">
<div className="confirm-import-token">
<div className="confirm-import-token__header">
<div className="confirm-import-token__token">
{this.context.t('token')}
</div>
<div className="confirm-import-token__balance">
{this.context.t('balance')}
</div>
</div>
<div className="confirm-import-token__token-list">
{suggestedAssets.map(({ asset }) => {
return (
<div
className="confirm-import-token__token-list-item"
key={asset.address}
>
<div className="confirm-import-token__token confirm-import-token__data">
<Identicon
className="confirm-import-token__token-icon"
diameter={48}
address={asset.address}
image={asset.image}
/>
<div className="confirm-import-token__name">
{this.getTokenName(asset.name, asset.symbol)}
</div>
</div>
<div className="confirm-import-token__balance">
<TokenBalance token={asset} />
</div>
</div>
);
})}
</div>
</div>
</div>
<div className="page-container__footer">
<footer>
<Button
type="secondary"
large
className="page-container__footer-button"
onClick={async () => {
await Promise.all(
suggestedAssets.map(async ({ id }) => rejectWatchAsset(id)),
);
history.push(mostRecentOverviewPage);
}}
>
{this.context.t('cancel')}
</Button>
<Button
type="primary"
large
className="page-container__footer-button"
disabled={suggestedAssets.length === 0}
onClick={async () => {
await Promise.all(
suggestedAssets.map(async ({ asset, id }) => {
await acceptWatchAsset(id);
this.context.trackEvent({
event: 'Token Added',
category: 'Wallet',
sensitiveProperties: {
token_symbol: asset.symbol,
token_contract_address: asset.address,
token_decimal_precision: asset.decimals,
unlisted: asset.unlisted,
source: 'dapp',
},
});
}),
);
history.push(mostRecentOverviewPage);
}}
>
{this.context.t('addToken')}
</Button>
</footer>
</div>
</div>
);
}
checkTokenDuplicates(suggestedAssets, tokens) {
const pending = suggestedAssets.map(({ asset }) =>
asset.address.toUpperCase(),
);
const existing = tokens.map((token) => token.address.toUpperCase());
const dupes = pending.filter((proposed) => {
return existing.includes(proposed);
});
return dupes.length > 0;
}
/**
* Returns true if any suggestedAssets both:
* - Share a symbol with an existing `tokens` member.
* - Does not share an address with that same `tokens` member.
* This should be flagged as possibly deceptive or confusing.
*/
checkNameReuse(suggestedAssets, tokens) {
const duplicates = suggestedAssets.filter(({ asset }) => {
const dupes = tokens.filter(
(old) =>
old.symbol === asset.symbol &&
!isEqualCaseInsensitive(old.address, asset.address),
);
return dupes.length > 0;
});
return duplicates.length > 0;
}
}