diff --git a/web/cashtab/src/assets/mail.svg b/web/cashtab/src/assets/mail.svg new file mode 100644 index 000000000..540677838 --- /dev/null +++ b/web/cashtab/src/assets/mail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap b/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap index b5cb8e720..ae1260646 100644 --- a/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap +++ b/web/cashtab/src/components/Airdrop/__tests__/__snapshots__/Airdrop.test.js.snap @@ -1,701 +1,701 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

edit.svg
0.06 XEC
,
,
XEC Airdrop Calculator
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; exports[`Without wallet defined 1`] = ` Array [
edit.svg
You currently have 0 XEC
Deposit some funds to use this feature
,
,
XEC Airdrop Calculator
, ] `; diff --git a/web/cashtab/src/components/Common/CustomIcons.js b/web/cashtab/src/components/Common/CustomIcons.js index f6880e9fa..2239ebd3a 100644 --- a/web/cashtab/src/components/Common/CustomIcons.js +++ b/web/cashtab/src/components/Common/CustomIcons.js @@ -1,191 +1,198 @@ import * as React from 'react'; import styled from 'styled-components'; import { CopyOutlined, DollarOutlined, LoadingOutlined, WalletOutlined, QrcodeOutlined, SettingOutlined, LockOutlined, ContactsOutlined, FireOutlined, } from '@ant-design/icons'; import { Image } from 'antd'; import { currency } from 'components/Common/Ticker'; import { ReactComponent as Send } from 'assets/send.svg'; import { ReactComponent as Receive } from 'assets/receive.svg'; import { ReactComponent as Genesis } from 'assets/flask.svg'; import { ReactComponent as Unparsed } from 'assets/alert-circle.svg'; import { ReactComponent as Home } from 'assets/home.svg'; import { ReactComponent as Settings } from 'assets/cog.svg'; import { ReactComponent as CopySolid } from 'assets/copy.svg'; import { ReactComponent as LinkSolid } from 'assets/external-link-square-alt.svg'; import { ReactComponent as Airdrop } from 'assets/airdrop-icon.svg'; import { ReactComponent as Pdf } from 'assets/file-pdf.svg'; import { ReactComponent as Plus } from 'assets/plus.svg'; import { ReactComponent as Download } from 'assets/download.svg'; import { ReactComponent as Edit } from 'assets/edit.svg'; import { ReactComponent as Trashcan } from 'assets/trashcan.svg'; import { ReactComponent as FingerprintSVG } from 'assets/fingerprint-solid.svg'; import { ReactComponent as EyeSVG } from 'assets/eye.svg'; import { ReactComponent as EyeInvisibleSVG } from 'assets/eye-invisible.svg'; import { ReactComponent as Audit } from 'assets/audit.svg'; - +import { ReactComponent as Mail } from 'assets/mail.svg'; export const CashLoadingIcon = ; export const CashReceivedNotificationIcon = () => ( ); export const TokenReceivedNotificationIcon = () => ( ); export const MessageSignedNotificationIcon = () => ( ); export const ThemedBurnOutlined = styled(FireOutlined)` color: ${props => props.theme.eCashPurple} !important; `; export const ThemedCopyOutlined = styled(CopyOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedDollarOutlined = styled(DollarOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedWalletOutlined = styled(WalletOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedQrcodeOutlined = styled(QrcodeOutlined)` color: ${props => props.theme.walletBackground} !important; `; export const ThemedSettingOutlined = styled(SettingOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedLockOutlined = styled(LockOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedContactsOutlined = styled(ContactsOutlined)` color: ${props => props.theme.icons.outlined} !important; `; export const ThemedContactSendOutlined = styled(Send)` color: ${props => props.theme.icons.outlined} !important; transform: rotate(-35deg); padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedCopySolid = styled(CopySolid)` fill: ${props => props.theme.contrast}; padding: 0rem 0rem 0.27rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedLinkSolid = styled(LinkSolid)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedPdfSolid = styled(Pdf)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedPlusOutlined = styled(Plus)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedDownloadOutlined = styled(Download)` fill: ${props => props.theme.contrast}; padding: 0.15rem 0rem 0.18rem 0rem; height: 1.3em; width: 1.3em; `; export const ThemedEditOutlined = styled(Edit)` stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 20px; height: 20px; cursor: pointer; `; export const ThemedTrashcanOutlined = styled(Trashcan)` stroke: ${props => props.theme.eCashBlue}; fill: ${props => props.theme.eCashBlue}; width: 20px; height: 20px; cursor: pointer; `; +export const ThemedMailOutlined = styled(Mail)` + stroke: ${props => props.theme.eCashBlue}; + fill: ${props => props.theme.eCashBlue}; + width: 20px; + height: 20px; + cursor: pointer; +`; export const ThemedFingerprintSVG = styled(FingerprintSVG)` fill: ${props => props.theme.buttons.primary.color}; `; export const ThemedEyeSVG = styled(EyeSVG)` fill: ${props => props.theme.buttons.primary.color}; width: 15px; height: 15px; cursor: pointer; `; export const ThemedInvisibleEyeSVG = styled(EyeInvisibleSVG)` fill: ${props => props.theme.buttons.primary.color}; width: 15px; height: 15px; cursor: pointer; `; export const ThemedSignAndVerifyMsg = styled(Audit)` min-width: 24px; `; export const LoadingBlock = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; flex-direction: column; svg { width: 50px; height: 50px; fill: ${props => props.theme.eCashBlue}; } `; export const CashLoader = () => ( ); export const ReceiveIcon = () => ; export const GenesisIcon = () => ; export const UnparsedIcon = () => ; export const HomeIcon = () => ; export const SettingsIcon = () => ; export const AirdropIcon = () => ; export const SendIcon = styled(Send)` transform: rotate(-35deg); `; export const CustomSpinner = ; diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap index aa38b4932..1a36b7c8e 100644 --- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap +++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap @@ -1,1129 +1,1129 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances and tokens 1`] = `

Backup your wallet

Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.

Manage Wallets

Contact List

Fiat Currency

US Dollar ($)

General Settings

Lock App
Not Supported
Send Confirmations
Hide messages from unknown sender
`; exports[`Without wallet defined 1`] = `

Backup your wallet

Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.

Manage Wallets

Contact List

Fiat Currency

US Dollar ($)

General Settings

Lock App
Not Supported
Send Confirmations
Hide messages from unknown sender
`; diff --git a/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap b/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap index ac679af15..2f2e18d78 100644 --- a/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap +++ b/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap @@ -1,631 +1,631 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

edit.svg
0 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

edit.svg
0 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

edit.svg
0.06 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

edit.svg
0 XEC
,
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Without wallet defined 1`] = `

Welcome to Cashtab!

Cashtab is an open source, non-custodial web wallet for eCash .

Want to learn more? Check out the Cashtab documentation.

`; diff --git a/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap b/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap index d1285a175..30b34231f 100644 --- a/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap +++ b/web/cashtab/src/components/Receive/__tests__/__snapshots__/Receive.test.js.snap @@ -1,819 +1,819 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = `

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Wallet with BCH balances and tokens 1`] = `

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Wallet with BCH balances and tokens and state field 1`] = `

MigrationTestAlpha

edit.svg
0.06 XEC

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Wallet without BCH balance 1`] = `

MigrationTestAlpha

edit.svg
You currently have 0 XEC
Deposit some funds to use this feature

Receive XEC

Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
ecash: qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
XEC
eToken
`; exports[`Without wallet defined 1`] = `

Welcome to Cashtab!

Cashtab is an open source, non-custodial web wallet for eCash .

Want to learn more? Check out the Cashtab documentation.

`; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index ea2e07ebb..d7c5daa8d 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,1027 +1,1039 @@ 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; ${DestinationAddressSingleCtn} { overflow: hidden; max-height: ${props => (props.open ? '0rem' : '17rem')}; } ${DestinationAddressMultiCtn} { overflow: hidden; max-height: ${props => (props.open ? '17rem' : '0rem')}; } `; +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