import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; import { isValidHexAddress } from '../../../shared/modules/hexstring-utils'; import { getAmountErrorObject, getGasFeeErrorObject, getToAddressForGasUpdate, doesAmountErrorRequireUpdate, } from './send.utils'; import { getToWarningObject, getToErrorObject, } from './send-content/add-recipient/add-recipient'; import SendHeader from './send-header'; import AddRecipient from './send-content/add-recipient'; import SendContent from './send-content'; import SendFooter from './send-footer'; import EnsInput from './send-content/add-recipient/ens-input'; import { INVALID_RECIPIENT_ADDRESS_ERROR, KNOWN_RECIPIENT_ADDRESS_ERROR, CONTRACT_ADDRESS_ERROR, } from './send.constants'; export default class SendTransactionScreen extends Component { static propTypes = { addressBook: PropTypes.arrayOf(PropTypes.object), amount: PropTypes.string, blockGasLimit: PropTypes.string, conversionRate: PropTypes.number, editingTransactionId: PropTypes.string, fetchBasicGasEstimates: PropTypes.func.isRequired, from: PropTypes.object, gasLimit: PropTypes.string, gasPrice: PropTypes.string, gasTotal: PropTypes.string, history: PropTypes.object, chainId: PropTypes.string, primaryCurrency: PropTypes.string, resetSendState: PropTypes.func.isRequired, selectedAddress: PropTypes.string, sendToken: PropTypes.object, showHexData: PropTypes.bool, to: PropTypes.string, toNickname: PropTypes.string, tokens: PropTypes.array, tokenBalance: PropTypes.string, tokenContract: PropTypes.object, updateAndSetGasLimit: PropTypes.func.isRequired, updateSendEnsResolution: PropTypes.func.isRequired, updateSendEnsResolutionError: PropTypes.func.isRequired, updateSendErrors: PropTypes.func.isRequired, updateSendTo: PropTypes.func.isRequired, updateSendTokenBalance: PropTypes.func.isRequired, updateToNicknameIfNecessary: PropTypes.func.isRequired, scanQrCode: PropTypes.func.isRequired, qrCodeDetected: PropTypes.func.isRequired, qrCodeData: PropTypes.object, sendTokenAddress: PropTypes.string, gasIsExcessive: PropTypes.bool.isRequired, }; static contextTypes = { t: PropTypes.func, metricsEvent: PropTypes.func, }; state = { query: '', toError: null, toWarning: null, internalSearch: false, }; constructor(props) { super(props); this.dValidate = debounce(this.validate, 1000); } componentDidUpdate(prevProps) { const { amount, conversionRate, from: { address, balance }, gasTotal, chainId, primaryCurrency, sendToken, tokenBalance, updateSendErrors, updateSendTo, updateSendTokenBalance, tokenContract, to, toNickname, addressBook, updateToNicknameIfNecessary, qrCodeData, qrCodeDetected, } = this.props; const { toError, toWarning } = this.state; let updateGas = false; const { from: { balance: prevBalance }, gasTotal: prevGasTotal, tokenBalance: prevTokenBalance, chainId: prevChainId, sendToken: prevSendToken, to: prevTo, } = prevProps; const uninitialized = [prevBalance, prevGasTotal].every((n) => n === null); const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({ balance, gasTotal, prevBalance, prevGasTotal, prevTokenBalance, sendToken, tokenBalance, }); if (amountErrorRequiresUpdate) { const amountErrorObject = getAmountErrorObject({ amount, balance, conversionRate, gasTotal, primaryCurrency, sendToken, tokenBalance, }); const gasFeeErrorObject = sendToken ? getGasFeeErrorObject({ balance, conversionRate, gasTotal, primaryCurrency, sendToken, }) : { gasFee: null }; updateSendErrors(Object.assign(amountErrorObject, gasFeeErrorObject)); } if (!uninitialized) { if (chainId !== prevChainId && chainId !== undefined) { updateSendTokenBalance({ sendToken, tokenContract, address, }); updateToNicknameIfNecessary(to, toNickname, addressBook); this.props.fetchBasicGasEstimates(); updateGas = true; } } const prevTokenAddress = prevSendToken && prevSendToken.address; const sendTokenAddress = sendToken && sendToken.address; if (sendTokenAddress && prevTokenAddress !== sendTokenAddress) { this.updateSendToken(); this.validate(this.state.query); updateGas = true; } let scannedAddress; if (qrCodeData) { if (qrCodeData.type === 'address') { scannedAddress = qrCodeData.values.address.toLowerCase(); if (isValidHexAddress(scannedAddress, { allowNonPrefixed: false })) { const currentAddress = prevTo?.toLowerCase(); if (currentAddress !== scannedAddress) { updateSendTo(scannedAddress); updateGas = true; // Clean up QR code data after handling qrCodeDetected(null); } } else { scannedAddress = null; qrCodeDetected(null); this.setState({ toError: INVALID_RECIPIENT_ADDRESS_ERROR }); } } } if (updateGas) { if (scannedAddress) { this.updateGas({ to: scannedAddress }); } else { this.updateGas(); } } // If selecting ETH after selecting a token, clear token related messages. if (prevSendToken && !sendToken) { let error = toError; let warning = toWarning; if (toError === CONTRACT_ADDRESS_ERROR) { error = null; } if (toWarning === KNOWN_RECIPIENT_ADDRESS_ERROR) { warning = null; } this.setState({ toError: error, toWarning: warning, }); } } componentDidMount() { this.props.fetchBasicGasEstimates().then(() => { this.updateGas(); }); } UNSAFE_componentWillMount() { this.updateSendToken(); // Show QR Scanner modal if ?scan=true if (window.location.search === '?scan=true') { this.props.scanQrCode(); // Clear the queryString param after showing the modal const cleanUrl = window.location.href.split('?')[0]; window.history.pushState({}, null, `${cleanUrl}`); window.location.hash = '#send'; } } componentWillUnmount() { this.props.resetSendState(); } onRecipientInputChange = (query) => { const { internalSearch } = this.state; if (!internalSearch) { if (query) { this.dValidate(query); } else { this.dValidate.cancel(); this.validate(query); } } this.setState({ query }); }; setInternalSearch(internalSearch) { this.setState({ query: '', internalSearch }); } validate(query) { const { tokens, sendToken, chainId, sendTokenAddress } = this.props; const { internalSearch } = this.state; if (!query || internalSearch) { this.setState({ toError: '', toWarning: '' }); return; } const toErrorObject = getToErrorObject(query, sendTokenAddress, chainId); const toWarningObject = getToWarningObject(query, tokens, sendToken); this.setState({ toError: toErrorObject.to, toWarning: toWarningObject.to, }); } updateSendToken() { const { from: { address }, sendToken, tokenContract, updateSendTokenBalance, } = this.props; updateSendTokenBalance({ sendToken, tokenContract, address, }); } updateGas({ to: updatedToAddress, amount: value, data } = {}) { const { amount, blockGasLimit, editingTransactionId, gasLimit, gasPrice, selectedAddress, sendToken, to: currentToAddress, updateAndSetGasLimit, } = this.props; updateAndSetGasLimit({ blockGasLimit, editingTransactionId, gasLimit, gasPrice, selectedAddress, sendToken, to: getToAddressForGasUpdate(updatedToAddress, currentToAddress), value: value || amount, data, }); } render() { const { history, to } = this.props; let content; if (to) { content = this.renderSendContent(); } else { content = this.renderAddRecipient(); } return (
{this.renderInput()} {content}
); } renderInput() { const { internalSearch } = this.state; return ( { this.context.metricsEvent({ eventOpts: { category: 'Transactions', action: 'Edit Screen', name: 'Used QR scanner', }, }); this.props.scanQrCode(); }} onChange={this.onRecipientInputChange} onValidAddressTyped={(address) => this.props.updateSendTo(address, '')} onPaste={(text) => { this.props.updateSendTo(text) && this.updateGas(); }} onReset={() => this.props.updateSendTo('', '')} updateEnsResolution={this.props.updateSendEnsResolution} updateEnsResolutionError={this.props.updateSendEnsResolutionError} internalSearch={internalSearch} /> ); } renderAddRecipient() { const { toError, toWarning } = this.state; return ( this.updateGas({ to, amount, data }) } query={this.state.query} toError={toError} toWarning={toWarning} setInternalSearch={(internalSearch) => this.setInternalSearch(internalSearch) } /> ); } renderSendContent() { const { history, showHexData, gasIsExcessive } = this.props; const { toWarning, toError } = this.state; return [ this.updateGas({ to, amount, data }) } showHexData={showHexData} warning={toWarning} error={toError} gasIsExcessive={gasIsExcessive} />, , ]; } }