1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Implement Network specific insufficient currency warning #12965 (#13182)

* added warning

fix

added warning for send screen

fix

changed condition

fix

fixed test

review updates

* review updates

* fix

* fixed failing test

* added check for transaction type

* fixed failing unit test

* added description for localization

* review updates

* review updates
This commit is contained in:
dragana8 2022-02-23 16:03:01 +01:00 committed by GitHub
parent a1eaa33b45
commit 662c19d4da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 267 additions and 17 deletions

View File

@ -406,6 +406,13 @@
"message": "Transak supports debit card and bank transfers (depending on location) in 59+ countries. $1 deposits into your MetaMask account.",
"description": "$1 represents the cypto symbol to be purchased"
},
"buyEth": {
"message": "Buy ETH"
},
"buyOther": {
"message": "Buy $1 or deposit from another account.",
"description": "$1 is a token symbol"
},
"buyWithWyre": {
"message": "Buy ETH with Wyre"
},
@ -1480,6 +1487,10 @@
"insufficientBalance": {
"message": "Insufficient balance."
},
"insufficientCurrency": {
"message": "You do not have enough $1 in your account to pay for transaction fees on $2 network.",
"description": "$1 is currency, $2 is network"
},
"insufficientFunds": {
"message": "Insufficient funds."
},
@ -2223,6 +2234,9 @@
"or": {
"message": "or"
},
"orDeposit": {
"message": "or deposit from another account."
},
"origin": {
"message": "Origin"
},

View File

@ -2,9 +2,16 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Tabs, Tab } from '../../../ui/tabs';
import ErrorMessage from '../../../ui/error-message';
import Button from '../../../ui/button';
import ActionableMessage from '../../../ui/actionable-message/actionable-message';
import { PageContainerFooter } from '../../../ui/page-container';
import ErrorMessage from '../../../ui/error-message';
import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../../helpers/constants/error-keys';
import Typography from '../../../ui/typography';
import { TYPOGRAPHY } from '../../../../helpers/constants/design-system';
import { TRANSACTION_TYPES } from '../../../../../shared/constants/transaction';
import { MAINNET_CHAIN_ID } from '../../../../../shared/constants/network';
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.';
export default class ConfirmPageContainerContent extends Component {
@ -44,6 +51,10 @@ export default class ConfirmPageContainerContent extends Component {
hideTitle: PropTypes.bool,
supportsEIP1559V2: PropTypes.bool,
hasTopBorder: PropTypes.bool,
currentTransaction: PropTypes.string,
nativeCurrency: PropTypes.string,
networkName: PropTypes.string,
showBuyModal: PropTypes.func,
};
renderContent() {
@ -113,6 +124,10 @@ export default class ConfirmPageContainerContent extends Component {
hideUserAcknowledgedGasMissing,
supportsEIP1559V2,
hasTopBorder,
currentTransaction,
nativeCurrency,
networkName,
showBuyModal,
} = this.props;
const primaryAction = hideUserAcknowledgedGasMissing
@ -121,6 +136,14 @@ export default class ConfirmPageContainerContent extends Component {
label: this.context.t('tryAnywayOption'),
onClick: setUserAcknowledgedGasMissing,
};
const { t } = this.context;
const showInsuffienctFundsError =
supportsEIP1559V2 &&
!hasSimulationError &&
(errorKey || errorMessage) &&
errorKey === INSUFFICIENT_FUNDS_ERROR_KEY &&
currentTransaction.type === TRANSACTION_TYPES.SIMPLE_SEND;
return (
<div
@ -137,7 +160,7 @@ export default class ConfirmPageContainerContent extends Component {
<ActionableMessage
type="danger"
primaryAction={primaryAction}
message={this.context.t('simulationErrorMessage')}
message={t('simulationErrorMessage')}
/>
</div>
)}
@ -160,11 +183,53 @@ export default class ConfirmPageContainerContent extends Component {
{this.renderContent()}
{!supportsEIP1559V2 &&
!hasSimulationError &&
(errorKey || errorMessage) && (
(errorKey || errorMessage) &&
currentTransaction.type !== TRANSACTION_TYPES.SIMPLE_SEND && (
<div className="confirm-page-container-content__error-container">
<ErrorMessage errorMessage={errorMessage} errorKey={errorKey} />
</div>
)}
{showInsuffienctFundsError && (
<div className="confirm-page-container-content__error-container">
{currentTransaction.chainId === MAINNET_CHAIN_ID ? (
<ActionableMessage
className="actionable-message--warning"
message={
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrency', [nativeCurrency, networkName])}
<Button
key="link"
type="secondary"
className="confirm-page-container-content__link"
onClick={showBuyModal}
>
{t('buyEth')}
</Button>
{t('orDeposit')}
</Typography>
}
useIcon
iconFillColor="#d73a49"
type="danger"
/>
) : (
<ActionableMessage
className="actionable-message--warning"
message={
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrency', [nativeCurrency, networkName])}
{t('buyOther', [nativeCurrency])}
</Typography>
}
useIcon
iconFillColor="#d73a49"
type="danger"
/>
)}
</div>
)}
<PageContainerFooter
onCancel={onCancel}
cancelText={cancelText}

View File

@ -75,6 +75,9 @@ describe('Confirm Page Container Content', () => {
props.hasSimulationError = false;
props.disabled = true;
props.errorKey = TRANSACTION_ERROR_KEY;
props.currentTransaction = {
type: 'transfer',
};
const { queryByText, getByText } = renderWithProvider(
<ConfirmPageContainerContent {...props} />,
store,

View File

@ -100,4 +100,12 @@
&__total-value {
position: relative;
}
&__link {
background: transparent;
border: 0 transparent;
display: inline;
padding: 0;
font-size: $font-size-h7;
}
}

View File

@ -4,10 +4,15 @@ import PropTypes from 'prop-types';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import { GasFeeContextProvider } from '../../../contexts/gasFee';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
import {
NETWORK_TO_NAME_MAP,
MAINNET_CHAIN_ID,
} from '../../../../shared/constants/network';
import { PageContainerFooter } from '../../ui/page-container';
import Dialog from '../../ui/dialog';
import ErrorMessage from '../../ui/error-message';
import Button from '../../ui/button';
import ActionableMessage from '../../ui/actionable-message/actionable-message';
import SenderToRecipient from '../../ui/sender-to-recipient';
import NicknamePopovers from '../modals/nickname-popovers';
@ -15,6 +20,10 @@ import NicknamePopovers from '../modals/nickname-popovers';
import AdvancedGasFeePopover from '../advanced-gas-fee-popover';
import EditGasFeePopover from '../edit-gas-fee-popover/edit-gas-fee-popover';
import EditGasPopover from '../edit-gas-popover';
import ErrorMessage from '../../ui/error-message';
import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../helpers/constants/error-keys';
import Typography from '../../ui/typography';
import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
import EnableEIP1559V2Notice from './enableEIP1559V2-notice';
import {
@ -87,6 +96,8 @@ export default class ConfirmPageContainer extends Component {
contact: PropTypes.object,
isOwnedAccount: PropTypes.bool,
supportsEIP1559V2: PropTypes.bool,
nativeCurrency: PropTypes.string,
showBuyModal: PropTypes.func,
};
render() {
@ -139,6 +150,8 @@ export default class ConfirmPageContainer extends Component {
contact = {},
isOwnedAccount,
supportsEIP1559V2,
nativeCurrency,
showBuyModal,
} = this.props;
const showAddToAddressDialog =
@ -152,6 +165,10 @@ export default class ConfirmPageContainer extends Component {
currentTransaction.type === TRANSACTION_TYPES.DEPLOY_CONTRACT) &&
currentTransaction.txParams?.value === '0x0';
const networkName = NETWORK_TO_NAME_MAP[currentTransaction.chainId];
const { t } = this.context;
return (
<GasFeeContextProvider transaction={currentTransaction}>
<div className="page-container">
@ -192,7 +209,7 @@ export default class ConfirmPageContainer extends Component {
className="send__dialog"
onClick={() => this.setState({ showNicknamePopovers: true })}
>
{this.context.t('newAccountDetectedDialogMessage')}
{t('newAccountDetectedDialogMessage')}
</Dialog>
{this.state.showNicknamePopovers ? (
<NicknamePopovers
@ -224,20 +241,65 @@ export default class ConfirmPageContainer extends Component {
warning={warning}
onCancelAll={onCancelAll}
onCancel={onCancel}
cancelText={this.context.t('reject')}
cancelText={t('reject')}
onSubmit={onSubmit}
submitText={this.context.t('confirm')}
submitText={t('confirm')}
disabled={disabled}
unapprovedTxCount={unapprovedTxCount}
rejectNText={this.context.t('rejectTxsN', [unapprovedTxCount])}
rejectNText={t('rejectTxsN', [unapprovedTxCount])}
origin={origin}
ethGasPriceWarning={ethGasPriceWarning}
hideTitle={hideTitle}
supportsEIP1559V2={supportsEIP1559V2}
hasTopBorder={showAddToAddressDialog}
currentTransaction={currentTransaction}
nativeCurrency={nativeCurrency}
networkName={networkName}
showBuyModal={showBuyModal}
/>
)}
{shouldDisplayWarning && (
{shouldDisplayWarning && errorKey === INSUFFICIENT_FUNDS_ERROR_KEY && (
<div className="confirm-approve-content__warning">
{currentTransaction.chainId === MAINNET_CHAIN_ID ? (
<ActionableMessage
message={
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrency', [nativeCurrency, networkName])}
<Button
type="link"
className="page-container__link"
onClick={showBuyModal}
>
{t('buyEth')}
</Button>
{t('orDeposit')}
</Typography>
}
useIcon
iconFillColor="#d73a49"
type="danger"
/>
) : (
<ActionableMessage
message={
<Typography
variant={TYPOGRAPHY.H7}
align="left"
margin={[0, 0]}
>
{t('insufficientCurrency', [nativeCurrency, networkName])}
{t('buyOther', [nativeCurrency])}
</Typography>
}
useIcon
iconFillColor="#d73a49"
type="danger"
/>
)}
</div>
)}
{shouldDisplayWarning && errorKey !== INSUFFICIENT_FUNDS_ERROR_KEY && (
<div className="confirm-approve-content__warning">
<ErrorMessage errorKey={errorKey} />
</div>
@ -245,14 +307,14 @@ export default class ConfirmPageContainer extends Component {
{contentComponent && (
<PageContainerFooter
onCancel={onCancel}
cancelText={this.context.t('reject')}
cancelText={t('reject')}
onSubmit={onSubmit}
submitText={this.context.t('confirm')}
submitText={t('confirm')}
disabled={disabled}
>
{unapprovedTxCount > 1 && (
<a onClick={onCancelAll}>
{this.context.t('rejectTxsN', [unapprovedTxCount])}
{t('rejectTxsN', [unapprovedTxCount])}
</a>
)}
</PageContainerFooter>

View File

@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { getAccountsWithLabels, getAddressBookEntry } from '../../../selectors';
import { showModal } from '../../../store/actions';
import ConfirmPageContainer from './confirm-page-container.component';
function mapStateToProps(state, ownProps) {
@ -16,4 +17,13 @@ function mapStateToProps(state, ownProps) {
};
}
export default connect(mapStateToProps)(ConfirmPageContainer);
const mapDispatchToProps = (dispatch) => {
return {
showBuyModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ConfirmPageContainer);

View File

@ -7,4 +7,12 @@
&__content-component-wrapper {
height: 100%;
}
&__link {
background: transparent;
border: 0 transparent;
display: inline;
padding: 0;
font-size: $font-size-h7;
}
}

View File

@ -60,6 +60,7 @@ import {
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 TransactionAlerts from './transaction-alerts';
@ -144,6 +145,7 @@ export default class ConfirmTransactionBase extends Component {
hardwareWalletRequiresConnection: PropTypes.bool,
isMultiLayerFeeNetwork: PropTypes.bool,
eip1559V2Enabled: PropTypes.bool,
showBuyModal: PropTypes.func,
};
state = {
@ -323,6 +325,7 @@ export default class ConfirmTransactionBase extends Component {
supportsEIP1559,
isMultiLayerFeeNetwork,
nativeCurrency,
showBuyModal,
} = this.props;
const { t } = this.context;
const { userAcknowledgedGasMissing } = this.state;
@ -335,6 +338,7 @@ export default class ConfirmTransactionBase extends Component {
const hasSimulationError = Boolean(txData.simulationFails);
const renderSimulationFailureWarning =
hasSimulationError && !userAcknowledgedGasMissing;
const networkName = NETWORK_TO_NAME_MAP[txData.chainId];
const renderTotalMaxAmount = () => {
if (
@ -582,6 +586,11 @@ export default class ConfirmTransactionBase extends Component {
this.setUserAcknowledgedGasMissing()
}
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
chainId={txData.chainId}
nativeCurrency={nativeCurrency}
networkName={networkName}
showBuyModal={showBuyModal}
type={txData.type}
/>
<TransactionDetail
disabled={isDisabled()}
@ -1109,6 +1118,7 @@ export default class ConfirmTransactionBase extends Component {
handleCloseEditGas={() => this.handleCloseEditGas()}
currentTransaction={txData}
supportsEIP1559V2={this.supportsEIP1559V2}
nativeCurrency={nativeCurrency}
/>
</TransactionModalContextProvider>
);

View File

@ -285,6 +285,7 @@ export const mapDispatchToProps = (dispatch) => {
updateTransactionGasFees: (gasFees) => {
dispatch(updateTransactionGasFees({ ...gasFees, expectHexWei: true }));
},
showBuyModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
};
};

View File

@ -3,19 +3,25 @@ import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas';
import { INSUFFICIENT_FUNDS_ERROR_KEY } from '../../../helpers/constants/error-keys';
import { submittedPendingTransactionsSelector } from '../../../selectors/transactions';
import { useGasFeeContext } from '../../../contexts/gasFee';
import { useI18nContext } from '../../../hooks/useI18nContext';
import ActionableMessage from '../../../components/ui/actionable-message/actionable-message';
import ErrorMessage from '../../../components/ui/error-message';
import I18nValue from '../../../components/ui/i18n-value';
import Button from '../../../components/ui/button';
import Typography from '../../../components/ui/typography';
import { TYPOGRAPHY } from '../../../helpers/constants/design-system';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
import { MAINNET_CHAIN_ID } from '../../../../shared/constants/network';
const TransactionAlerts = ({
userAcknowledgedGasMissing,
setUserAcknowledgedGasMissing,
chainId,
nativeCurrency,
networkName,
showBuyModal,
type,
}) => {
const {
balanceError,
@ -90,7 +96,45 @@ const TransactionAlerts = ({
type="warning"
/>
)}
{balanceError && <ErrorMessage errorKey={INSUFFICIENT_FUNDS_ERROR_KEY} />}
{balanceError &&
chainId === MAINNET_CHAIN_ID &&
type === TRANSACTION_TYPES.DEPLOY_CONTRACT ? (
<ActionableMessage
className="actionable-message--warning"
message={
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrency', [nativeCurrency, networkName])}{' '}
<Button
type="link"
className="transaction-alerts__link"
onClick={showBuyModal}
>
{t('buyEth')}
</Button>{' '}
{t('orDeposit')}
</Typography>
}
useIcon
iconFillColor="#d73a49"
type="danger"
/>
) : null}
{balanceError &&
chainId !== MAINNET_CHAIN_ID &&
type === TRANSACTION_TYPES.DEPLOY_CONTRACT ? (
<ActionableMessage
className="actionable-message--warning"
message={
<Typography variant={TYPOGRAPHY.H7} align="left">
{t('insufficientCurrency', [nativeCurrency, networkName])}
{t('buyOther', [nativeCurrency])}
</Typography>
}
useIcon
iconFillColor="#d73a49"
type="danger"
/>
) : null}
{estimateUsed === PRIORITY_LEVELS.LOW && (
<ActionableMessage
dataTestId="low-gas-fee-alert"
@ -133,6 +177,11 @@ const TransactionAlerts = ({
TransactionAlerts.propTypes = {
userAcknowledgedGasMissing: PropTypes.bool,
setUserAcknowledgedGasMissing: PropTypes.func,
chainId: PropTypes.string,
nativeCurrency: PropTypes.string,
networkName: PropTypes.string,
showBuyModal: PropTypes.func,
type: PropTypes.string,
};
export default TransactionAlerts;

View File

@ -18,4 +18,12 @@
color: var(--primary-1);
}
}
&__link {
background: transparent;
border: 0 transparent;
display: inline;
padding: 0;
font-size: $font-size-h7;
}
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../../test/jest';
import { submittedPendingTransactionsSelector } from '../../../selectors/transactions';
import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction';
import { useGasFeeContext } from '../../../contexts/gasFee';
import configureStore from '../../../store/store';
import TransactionAlerts from './transaction-alerts';
@ -148,8 +149,19 @@ describe('TransactionAlerts', () => {
supportsEIP1559V2: true,
balanceError: true,
},
componentProps: {
nativeCurrency: 'ETH',
networkName: 'Ropsten',
showBuyModal: jest.fn(),
chainId: '0x1',
type: TRANSACTION_TYPES.DEPLOY_CONTRACT,
},
});
expect(getByText('Insufficient funds.')).toBeInTheDocument();
expect(
getByText(
/You do not have enough ETH in your account to pay for transaction fees on Ropsten network./u,
),
).toBeInTheDocument();
});
});