diff --git a/web/cashtab/src/components/Common/StyledCollapse.js b/web/cashtab/src/components/Common/StyledCollapse.js index 3142c3f53..4c6b9bc19 100644 --- a/web/cashtab/src/components/Common/StyledCollapse.js +++ b/web/cashtab/src/components/Common/StyledCollapse.js @@ -1,150 +1,150 @@ import React from 'react'; import styled from 'styled-components'; import { Collapse } from 'antd'; import PropTypes from 'prop-types'; const { Panel } = Collapse; export const StyledCollapse = styled(Collapse)` background: ${props => props.theme.collapses.background} !important; border: 1px solid ${props => props.theme.collapses.border} !important; .ant-collapse-content { border-top: none; background-color: ${props => props.theme.collapses.expandedBackground} !important; } .ant-collapse-item { border-bottom: none !important; } *:not(button) { color: ${props => props.theme.collapses.color} !important; } `; const CenteredTitleCollapse = styled.div``; const CollapseCtn = styled.div` .ant-collapse-header { .anticon { flex: 1; } ${CenteredTitleCollapse} { flex: 2; } } .ant-form-small { color: ${props => props.theme.lightGrey} !important; } `; export const TokenCollapse = styled(Collapse)` ${({ disabled = false, ...props }) => disabled === true ? ` background: ${props.theme.buttons.secondary.background} !important; .ant-collapse-header { font-size: 18px; font-weight: bold; color: ${props.theme.buttons.secondary.color} !important; svg { color: ${props.theme.buttons.secondary.color} !important; } } .ant-collapse-arrow { font-size: 18px; } ` : ` background: ${props.theme.eCashBlue} !important; .ant-collapse-header { font-size: 18px; font-weight: bold; color: ${props.theme.contrast} !important; svg { color: ${props.theme.contrast} !important; } } .ant-collapse-arrow { font-size: 18px; } `} `; export const AdvancedCollapse = styled(Collapse)` .ant-input-textarea-show-count:after { color: ${props => props.theme.lightGrey} !important; } .ant-collapse-content { background-color: ${props => props.theme.advancedCollapse.expandedBackground} !important; } ${({ disabled = false, ...props }) => disabled === true ? ` background: ${props.theme.buttons.secondary.background} !important; .ant-collapse-header { font-size: 18px; font-weight: normal; color: ${props.theme.buttons.secondary.color} !important; svg { color: ${props.theme.buttons.secondary.color} !important; } } .ant-collapse-arrow { font-size: 18px; } ` : ` background: ${props.theme.advancedCollapse.background} !important; .ant-collapse-header { font-size: 18px; font-weight: bold; color: ${props.theme.advancedCollapse.color} !important; svg { color: ${props.theme.advancedCollapse.icon} !important; } } .ant-collapse-arrow { font-size: 18px; } `} `; export const CustomCollapseCtn = ({ panelHeader, children, optionalDefaultActiveKey, optionalKey, }) => { return ( {panelHeader} } key={optionalKey} > {children} ); }; CustomCollapseCtn.propTypes = { optionalDefaultActiveKey: PropTypes.arrayOf(PropTypes.string), - panelHeader: PropTypes.string, + panelHeader: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), children: PropTypes.node, optionalKey: PropTypes.string, }; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index d7c5daa8d..79ccdeed3 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,1039 +1,1051 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import { WalletContext } from 'utils/context'; import { AntdFormWrapper, SendBchInput, DestinationAddressSingle, DestinationAddressMulti, } from 'components/Common/EnhancedInputs'; import { ThemedMailOutlined } from 'components/Common/CustomIcons'; import { CustomCollapseCtn } from 'components/Common/StyledCollapse'; import { Form, message, Modal, Alert, Input } from 'antd'; import { Row, Col, Switch } from 'antd'; import PrimaryButton, { DisabledButton } from 'components/Common/PrimaryButton'; import useBCH from 'hooks/useBCH'; import useWindowDimensions from 'hooks/useWindowDimensions'; import { sendXecNotification, errorNotification, } from 'components/Common/Notifications'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { currency, parseAddressForParams } from 'components/Common/Ticker.js'; import { Event } from 'utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput, isValidXecAddress, isValidEtokenAddress, isValidXecSendAmount, } from 'utils/validation'; import BalanceHeader from 'components/Common/BalanceHeader'; import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; import { ZeroBalanceHeader, ConvertAmount, AlertMsg, WalletInfoCtn, SidePaddingCtn, FormLabel, } from 'components/Common/Atoms'; import { getWalletState, toLegacyCash, toLegacyCashArray, fromSatoshisToXec, calcFee, } from 'utils/cashMethods'; import ApiError from 'components/Common/ApiError'; import { formatFiatBalance, formatBalance } from 'utils/formatting'; import styled from 'styled-components'; import WalletLabel from 'components/Common/WalletLabel.js'; const { TextArea } = Input; const TextAreaLabel = styled.div` text-align: left; color: ${props => props.theme.forms.text}; padding-left: 1px; white-space: nowrap; `; const AmountPreviewCtn = styled.div` margin-top: -30px; `; const SendInputCtn = styled.div` .ant-form-item-with-help { margin-bottom: 32px; } `; const LocaleFormattedValue = styled.h3` color: ${props => props.theme.contrast}; font-weight: bold; margin-bottom: 0; `; const SendAddressHeader = styled.div` display: flex; align-items: center; `; const DestinationAddressSingleCtn = styled.div``; const DestinationAddressMultiCtn = styled.div``; const ExpandingAddressInputCtn = styled.div` - min-height: 14rem; + min-height: 15rem; ${DestinationAddressSingleCtn} { overflow: hidden; - max-height: ${props => (props.open ? '0rem' : '17rem')}; + transition: ${props => + props.open + ? 'max-height 200ms ease-in, opacity 200ms ease-out' + : 'max-height 200ms cubic-bezier(0, 1, 0, 1), opacity 200ms ease-in'}; + max-height: ${props => (props.open ? '0rem' : '14rem')}; + opacity: ${props => (props.open ? 0 : 1)}; } ${DestinationAddressMultiCtn} { overflow: hidden; - max-height: ${props => (props.open ? '17rem' : '0rem')}; + transition: ${props => + props.open + ? 'max-height 200ms ease-in, transform 200ms ease-out, opacity 200ms ease-in' + : 'max-height 200ms cubic-bezier(0, 1, 0, 1), transform 200ms ease-out'}; + max-height: ${props => (props.open ? '14rem' : '0rem')}; + transform: ${props => + props.open ? 'translateY(0%)' : 'translateY(100%)'}; + opacity: ${props => (props.open ? 1 : 0)}; } `; const PanelHeaderCtn = styled.div` display: flex; justify-content: center; align-items: center; gap: 1rem; `; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const SendBCH = ({ jestBCH, passLoadingStatus }) => { // use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet // this handles edge case of user with old wallet who has not opened latest Cashtab version yet // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object // Else set it as blank const ContextValue = React.useContext(WalletContext); const location = useLocation(); const { BCH, wallet, fiatPrice, apiError, cashtabSettings, changeCashtabSettings, chronik, } = ContextValue; const walletState = getWalletState(wallet); const { balances, slpBalancesAndUtxos } = walletState; // Modal settings const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); const [opReturnMsg, setOpReturnMsg] = useState(false); const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] = useState(false); const [bchObj, setBchObj] = useState(false); // Get device window width // If this is less than 769, the page will open with QR scanner open const { width } = useWindowDimensions(); // Load with QR code open if device is mobile and NOT iOS + anything but safari const scannerSupported = cashtabSettings && cashtabSettings.autoCameraOn === true && width < 769 && isMobile && !(isIOS && !isSafari); const [formData, setFormData] = useState({ value: '', address: '', airdropTokenId: '', }); const [queryStringText, setQueryStringText] = useState(null); const [sendBchAddressError, setSendBchAddressError] = useState(false); const [sendBchAmountError, setSendBchAmountError] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); // Support cashtab button from web pages const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); // Show a confirmation modal on transactions created by populating form from web page button const [isModalVisible, setIsModalVisible] = useState(false); const [airdropFlag, setAirdropFlag] = useState(false); const userLocale = navigator.language; const clearInputForms = () => { setFormData({ value: '', address: '', }); setOpReturnMsg(''); // OP_RETURN message has its own state field }; const checkForConfirmationBeforeSendXec = () => { if (txInfoFromUrl) { setIsModalVisible(true); } else if (cashtabSettings.sendModal) { setIsModalVisible(cashtabSettings.sendModal); } else { // if the user does not have the send confirmation enabled in settings then send directly send(); } }; const handleOk = () => { setIsModalVisible(false); send(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getRestUrl, sendXec } = useBCH(); // If the balance has changed, unlock the UI // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked useEffect(() => { passLoadingStatus(false); }, [balances.totalBalance]); useEffect(() => { // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const activeBCH = jestBCH ? jestBCH : BCH; // set the BCH instance to state, for other functions to reference setBchObj(activeBCH); }, [BCH]); useEffect(() => { // Manually parse for txInfo object on page load when Send.js is loaded with a query string // if this was routed from Wallet screen's Reply to message link then prepopulate the address and value field if (location && location.state && location.state.replyAddress) { setFormData({ address: location.state.replyAddress, value: `${fromSatoshisToXec(currency.dustSats).toString()}`, }); } // if this was routed from the Contact List if (location && location.state && location.state.contactSend) { setFormData({ address: location.state.contactSend, }); } // if this was routed from the Airdrop screen's Airdrop Calculator then // switch to multiple recipient mode and prepopulate the recipients field if ( location && location.state && location.state.airdropRecipients && location.state.airdropTokenId ) { setIsOneToManyXECSend(true); setFormData({ address: location.state.airdropRecipients, airdropTokenId: location.state.airdropTokenId, }); // validate the airdrop outputs from the calculator handleMultiAddressChange({ target: { value: location.state.airdropRecipients, }, }); setAirdropFlag(true); } // Do not set txInfo in state if query strings are not present if ( !window.location || !window.location.hash || window.location.hash === '#/send' ) { return; } const txInfoArr = window.location.hash.split('?')[1].split('&'); // Iterate over this to create object const txInfo = {}; for (let i = 0; i < txInfoArr.length; i += 1) { let txInfoKeyValue = txInfoArr[i].split('='); let key = txInfoKeyValue[0]; let value = txInfoKeyValue[1]; txInfo[key] = value; } console.log(`txInfo from page params`, txInfo); setTxInfoFromUrl(txInfo); populateFormsFromUrl(txInfo); }, []); function populateFormsFromUrl(txInfo) { if (txInfo && txInfo.address && txInfo.value) { setFormData({ address: txInfo.address, value: txInfo.value, }); } } function handleSendXecError(errorObj, oneToManyFlag) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop passLoadingStatus(false); let message; if (!errorObj.error && !errorObj.message) { message = `Transaction failed: no response from ${getRestUrl()}.`; } else if ( /Could not communicate with full node or other external service/.test( errorObj.error, ) ) { message = 'Could not communicate with API. Please try again.'; } else if ( errorObj.error && errorObj.error.includes( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)', ) ) { message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`; } else { message = errorObj.message || errorObj.error || JSON.stringify(errorObj); } if (oneToManyFlag) { errorNotification(errorObj, message, 'Sending XEC one to many'); } else { errorNotification(errorObj, message, 'Sending XEC'); } } async function send() { setFormData({ ...formData, }); if (isOneToManyXECSend) { // this is a one to many XEC send transactions // ensure multi-recipient input is not blank if (!formData.address) { return; } // Event("Category", "Action", "Label") // Track number of XEC send-to-many transactions Event('Send.js', 'SendToMany', selectedCurrency); passLoadingStatus(true); const { address } = formData; //convert each line from TextArea input let addressAndValueArray = address.split('\n'); try { // construct array of XEC->BCH addresses due to bch-api constraint let cleanAddressAndValueArray = toLegacyCashArray(addressAndValueArray); const link = await sendXec( bchObj, chronik, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, opReturnMsg, true, // indicate send mode is one to many cleanAddressAndValueArray, null, null, false, // one to many tx msg can't be encrypted airdropFlag, formData.airdropTokenId, ); sendXecNotification(link); clearInputForms(); setAirdropFlag(false); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } else { // standard one to one XEC send transaction if ( !formData.address || !formData.value || Number(formData.value) <= 0 ) { return; } // Event("Category", "Action", "Label") // Track number of BCHA send transactions and whether users // are sending BCHA or USD Event('Send.js', 'Send', selectedCurrency); passLoadingStatus(true); const { address, value } = formData; // Get the param-free address let cleanAddress = address.split('?')[0]; // Ensure address has bitcoincash: prefix and checksum cleanAddress = toLegacyCash(cleanAddress); // Calculate the amount in BCH let bchValue = value; if (selectedCurrency !== 'XEC') { bchValue = fiatToCrypto(value, fiatPrice); } // encrypted message limit truncation let optionalOpReturnMsg; if (isEncryptedOptionalOpReturnMsg) { optionalOpReturnMsg = opReturnMsg.substring( 0, currency.opReturn.encryptedMsgCharLimit, ); } else { optionalOpReturnMsg = opReturnMsg; } try { const link = await sendXec( bchObj, chronik, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, optionalOpReturnMsg, false, // sendToMany boolean flag null, // address array not applicable for one to many tx cleanAddress, bchValue, isEncryptedOptionalOpReturnMsg, ); sendXecNotification(link); clearInputForms(); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } } const handleAddressChange = e => { const { value, name } = e.target; let error = false; let addressString = value; // parse address for parameters const addressInfo = parseAddressForParams(addressString); // validate address const isValid = isValidXecAddress(addressInfo.address); /* Model addressInfo = { address: '', queryString: '', amount: null, }; */ const { address, queryString, amount } = addressInfo; // If query string, // Show an alert that only amount and currency.ticker are supported setQueryStringText(queryString); // Is this valid address? if (!isValid) { error = `Invalid ${currency.ticker} address`; // If valid address but token format if (isValidEtokenAddress(address)) { error = `eToken addresses are not supported for ${currency.ticker} sends`; } } setSendBchAddressError(error); // Set amount if it's in the query string if (amount !== null) { // Set currency to BCHA setSelectedCurrency(currency.ticker); // Use this object to mimic user input and get validation for the value let amountObj = { target: { name: 'value', value: amount, }, }; handleBchAmountChange(amountObj); setFormData({ ...formData, value: amount, }); } // Set address field to user input setFormData(p => ({ ...p, [name]: value, })); }; const handleMultiAddressChange = e => { const { value, name } = e.target; let error; if (!value) { error = 'Input must not be blank'; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } //convert each line from the input into array let addressStringArray = value.split('\n'); const arrayLength = addressStringArray.length; // loop through each row in the input for (let i = 0; i < arrayLength; i++) { if (addressStringArray[i].trim() === '') { // if this line is a line break or bunch of spaces error = 'Empty spaces and rows must be removed'; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } let addressString = addressStringArray[i].split(',')[0]; let valueString = addressStringArray[i].split(',')[1]; const validAddress = isValidXecAddress(addressString); const validValueString = isValidXecSendAmount(valueString); if (!validAddress) { error = `Invalid XEC address: ${addressString}${ valueString !== undefined ? `, ${valueString}` : '' }`; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } if (!validValueString) { error = `Amount must be at least ${fromSatoshisToXec( currency.dustSats, ).toString()} XEC: ${addressString}, ${valueString}`; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } } // If iterate to end of array with no errors, then there is no error msg setSendBchAddressError(false); // Set address field to user input setFormData(p => ({ ...p, [name]: value, })); }; const handleSelectedCurrencyChange = e => { setSelectedCurrency(e); // Clear input field to prevent accidentally sending 1 BCH instead of 1 USD setFormData(p => ({ ...p, value: '', })); }; const handleBchAmountChange = e => { const { value, name } = e.target; let bchValue = value; const error = shouldRejectAmountInput( bchValue, selectedCurrency, fiatPrice, balances.totalBalance, ); setSendBchAmountError(error); setFormData(p => ({ ...p, [name]: value, })); }; const onMax = async () => { // Clear amt error setSendBchAmountError(false); // Set currency to BCH setSelectedCurrency(currency.ticker); try { const txFeeSats = calcFee(slpBalancesAndUtxos.nonSlpUtxos); const txFeeBch = txFeeSats / 10 ** currency.cashDecimals; let value = balances.totalBalance - txFeeBch >= 0 ? (balances.totalBalance - txFeeBch).toFixed( currency.cashDecimals, ) : 0; setFormData({ ...formData, value, }); } catch (err) { console.log(`Error in onMax:`); console.log(err); message.error( 'Unable to calculate the max value due to network errors', ); } }; // Display price in USD below input field for send amount, if it can be calculated let fiatPriceString = ''; if (fiatPrice !== null && !isNaN(formData.value)) { if (selectedCurrency === currency.ticker) { // calculate conversion to fiatPrice fiatPriceString = `${(fiatPrice * Number(formData.value)).toFixed( 2, )}`; // formats to fiat locale style fiatPriceString = formatFiatBalance( Number(fiatPriceString), userLocale, ); // insert symbol and currency before/after the locale formatted fiat balance fiatPriceString = `${ cashtabSettings ? `${ currency.fiatCurrencies[cashtabSettings.fiatCurrency] .symbol } ` : '$ ' } ${fiatPriceString} ${ cashtabSettings && cashtabSettings.fiatCurrency ? cashtabSettings.fiatCurrency.toUpperCase() : 'USD' }`; } else { fiatPriceString = `${ formData.value ? formatFiatBalance( Number(fiatToCrypto(formData.value, fiatPrice)), userLocale, ) : formatFiatBalance(0, userLocale) } ${currency.ticker}`; } } const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC'; return ( <> {isOneToManyXECSend ? `are you sure you want to send the following One to Many transaction? ${formData.address}` : `Are you sure you want to send ${formData.value}${' '} ${selectedCurrency} to ${formData.address}?`} {!balances.totalBalance ? ( You currently have 0 {currency.ticker} Deposit some funds to use this feature ) : ( <> > )} {' '} Send to Multiple Recipients: { setIsOneToManyXECSend( !isOneToManyXECSend, ); setIsEncryptedOptionalOpReturnMsg( false, ); }} style={{ marginBottom: '7px', }} /> handleAddressChange({ target: { name: 'address', value: result, }, }) } inputProps={{ placeholder: `${currency.ticker} Address`, name: 'address', onChange: e => handleAddressChange(e), required: true, value: formData.address, }} > Amount handleBchAmountChange(e), required: true, value: formData.value, disabled: priceApiError, }} selectProps={{ value: selectedCurrency, disabled: queryStringText !== null, onChange: e => handleSelectedCurrencyChange( e, ), }} > {priceApiError && ( Error fetching fiat price. Setting send by{' '} {currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].slug.toUpperCase()}{' '} disabled )} <> handleMultiAddressChange(e), required: true, value: formData.address, }} > > {!priceApiError && !isOneToManyXECSend && ( <> {formatBalance( formData.value, userLocale, )}{' '} {selectedCurrency} {fiatPriceString !== '' && '='}{' '} {fiatPriceString} > )} {queryStringText && ( )} {!balances.totalBalance || apiError || sendBchAmountError || sendBchAddressError || priceApiError ? ( Send ) : ( <> {txInfoFromUrl ? ( checkForConfirmationBeforeSendXec() } > Send ) : ( { checkForConfirmationBeforeSendXec(); }} > Send )} > )} Message } optionalDefaultActiveKey={ location && location.state && location.state.replyAddress ? ['1'] : ['0'] } optionalKey="1" > Message: { setIsEncryptedOptionalOpReturnMsg( prev => !prev, ); setIsOneToManyXECSend(false); }} /> {isEncryptedOptionalOpReturnMsg ? ( ) : ( )} setOpReturnMsg(e.target.value) } showCount maxLength={ isEncryptedOptionalOpReturnMsg ? currency.opReturn .encryptedMsgCharLimit : location && location.state && location.state.airdropTokenId ? currency.opReturn .unencryptedAirdropMsgCharLimit : currency.opReturn .unencryptedMsgCharLimit } onKeyDown={e => e.keyCode == 13 ? e.preventDefault() : '' } /> {apiError && } > ); }; /* passLoadingStatus must receive a default prop that is a function in order to pass the rendering unit test in Send.test.js status => {console.log(status)} is an arbitrary stub function */ SendBCH.defaultProps = { passLoadingStatus: status => { console.log(status); }, }; SendBCH.propTypes = { jestBCH: PropTypes.object, passLoadingStatus: PropTypes.func, }; export default SendBCH; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap index 8b6556112..a7e607e9a 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -1,2902 +1,2902 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [ MigrationTestAlpha edit.svg eye-invisible.svg You currently have 0 XEC Deposit some funds to use this feature , Send to Multiple Recipients: Amount XEC max } required={true} style={ Object { "height": "189px", } } value="" /> 0 XEC = $ NaN USD Send mail.svg Message , ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [ MigrationTestAlpha edit.svg eye-invisible.svg You currently have 0 XEC Deposit some funds to use this feature , Send to Multiple Recipients: Amount XEC max } required={true} style={ Object { "height": "189px", } } value="" /> 0 XEC = $ NaN USD Send mail.svg Message , ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [ MigrationTestAlpha edit.svg eye-invisible.svg 0.06 XEC , Send to Multiple Recipients: Amount XEC max } required={true} style={ Object { "height": "189px", } } value="" /> 0 XEC = $ NaN USD Send mail.svg Message , ] `; exports[`Wallet without BCH balance 1`] = ` Array [ MigrationTestAlpha edit.svg eye-invisible.svg You currently have 0 XEC Deposit some funds to use this feature , Send to Multiple Recipients: Amount XEC max } required={true} style={ Object { "height": "189px", } } value="" /> 0 XEC = $ NaN USD Send mail.svg Message , ] `; exports[`Without wallet defined 1`] = ` Array [ edit.svg eye-invisible.svg You currently have 0 XEC Deposit some funds to use this feature , Send to Multiple Recipients: Amount XEC max } required={true} style={ Object { "height": "189px", } } value="" /> 0 XEC = $ NaN USD Send mail.svg Message , ] `;
{isOneToManyXECSend ? `are you sure you want to send the following One to Many transaction? ${formData.address}` : `Are you sure you want to send ${formData.value}${' '} ${selectedCurrency} to ${formData.address}?`}