diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index cb4496c6d..7494f39df 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -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" }, 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 665d1924f..54dcb6d62 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 @@ -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 (
)} @@ -160,11 +183,53 @@ export default class ConfirmPageContainerContent extends Component { {this.renderContent()} {!supportsEIP1559V2 && !hasSimulationError && - (errorKey || errorMessage) && ( + (errorKey || errorMessage) && + currentTransaction.type !== TRANSACTION_TYPES.SIMPLE_SEND && (
)} + {showInsuffienctFundsError && ( +
+ {currentTransaction.chainId === MAINNET_CHAIN_ID ? ( + + {t('insufficientCurrency', [nativeCurrency, networkName])} + + + {t('orDeposit')} + + } + useIcon + iconFillColor="#d73a49" + type="danger" + /> + ) : ( + + {t('insufficientCurrency', [nativeCurrency, networkName])} + {t('buyOther', [nativeCurrency])} + + } + useIcon + iconFillColor="#d73a49" + type="danger" + /> + )} +
+ )} + { props.hasSimulationError = false; props.disabled = true; props.errorKey = TRANSACTION_ERROR_KEY; + props.currentTransaction = { + type: 'transfer', + }; const { queryByText, getByText } = renderWithProvider( , store, 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 b932af183..f52888db2 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 @@ -100,4 +100,12 @@ &__total-value { position: relative; } + + &__link { + background: transparent; + border: 0 transparent; + display: inline; + padding: 0; + font-size: $font-size-h7; + } } 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 10f326594..25c9ff937 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 @@ -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 (
@@ -192,7 +209,7 @@ export default class ConfirmPageContainer extends Component { className="send__dialog" onClick={() => this.setState({ showNicknamePopovers: true })} > - {this.context.t('newAccountDetectedDialogMessage')} + {t('newAccountDetectedDialogMessage')} {this.state.showNicknamePopovers ? ( )} - {shouldDisplayWarning && ( + {shouldDisplayWarning && errorKey === INSUFFICIENT_FUNDS_ERROR_KEY && ( +
+ {currentTransaction.chainId === MAINNET_CHAIN_ID ? ( + + {t('insufficientCurrency', [nativeCurrency, networkName])} + + + {t('orDeposit')} + + } + useIcon + iconFillColor="#d73a49" + type="danger" + /> + ) : ( + + {t('insufficientCurrency', [nativeCurrency, networkName])} + {t('buyOther', [nativeCurrency])} + + } + useIcon + iconFillColor="#d73a49" + type="danger" + /> + )} +
+ )} + {shouldDisplayWarning && errorKey !== INSUFFICIENT_FUNDS_ERROR_KEY && (
@@ -245,14 +307,14 @@ export default class ConfirmPageContainer extends Component { {contentComponent && ( {unapprovedTxCount > 1 && ( - {this.context.t('rejectTxsN', [unapprovedTxCount])} + {t('rejectTxsN', [unapprovedTxCount])} )} diff --git a/ui/components/app/confirm-page-container/confirm-page-container.container.js b/ui/components/app/confirm-page-container/confirm-page-container.container.js index ec5ba84c0..d0d287aed 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.container.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.container.js @@ -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); diff --git a/ui/components/app/confirm-page-container/index.scss b/ui/components/app/confirm-page-container/index.scss index 9723c2d23..20aa1d94f 100644 --- a/ui/components/app/confirm-page-container/index.scss +++ b/ui/components/app/confirm-page-container/index.scss @@ -7,4 +7,12 @@ &__content-component-wrapper { height: 100%; } + + &__link { + background: transparent; + border: 0 transparent; + display: inline; + padding: 0; + font-size: $font-size-h7; + } } 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 f7f9774f3..a7202b6ca 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -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} /> this.handleCloseEditGas()} currentTransaction={txData} supportsEIP1559V2={this.supportsEIP1559V2} + nativeCurrency={nativeCurrency} /> ); 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 8b13a1034..c6a6598ad 100644 --- a/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -285,6 +285,7 @@ export const mapDispatchToProps = (dispatch) => { updateTransactionGasFees: (gasFees) => { dispatch(updateTransactionGasFees({ ...gasFees, expectHexWei: true })); }, + showBuyModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })), }; }; diff --git a/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js b/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js index ae6b195ad..bbb1883ab 100644 --- a/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js +++ b/ui/pages/confirm-transaction-base/transaction-alerts/transaction-alerts.js @@ -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 && } + {balanceError && + chainId === MAINNET_CHAIN_ID && + type === TRANSACTION_TYPES.DEPLOY_CONTRACT ? ( + + {t('insufficientCurrency', [nativeCurrency, networkName])}{' '} + {' '} + {t('orDeposit')} + + } + useIcon + iconFillColor="#d73a49" + type="danger" + /> + ) : null} + {balanceError && + chainId !== MAINNET_CHAIN_ID && + type === TRANSACTION_TYPES.DEPLOY_CONTRACT ? ( + + {t('insufficientCurrency', [nativeCurrency, networkName])} + {t('buyOther', [nativeCurrency])} + + } + useIcon + iconFillColor="#d73a49" + type="danger" + /> + ) : null} {estimateUsed === PRIORITY_LEVELS.LOW && ( { 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(); }); });