Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/components/Send/Send.js
import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||
import { useLocation } from 'react-router-dom'; | import { useLocation } from 'react-router-dom'; | ||||
import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
import { WalletContext } from '@utils/context'; | import { WalletContext } from '@utils/context'; | ||||
import { | import { | ||||
AntdFormWrapper, | AntdFormWrapper, | ||||
SendBchInput, | SendBchInput, | ||||
DestinationAddressSingle, | DestinationAddressSingle, | ||||
DestinationAddressMulti, | |||||
} from '@components/Common/EnhancedInputs'; | } from '@components/Common/EnhancedInputs'; | ||||
import { | import { | ||||
StyledCollapse, | StyledCollapse, | ||||
AdvancedCollapse, | AdvancedCollapse, | ||||
} from '@components/Common/StyledCollapse'; | } from '@components/Common/StyledCollapse'; | ||||
import { | import { | ||||
Form, | Form, | ||||
message, | message, | ||||
Modal, | Modal, | ||||
Alert, | Alert, | ||||
Collapse, | Collapse, | ||||
Input, | Input, | ||||
Button, | |||||
notification, | notification, | ||||
} from 'antd'; | } from 'antd'; | ||||
const { Panel } = Collapse; | const { Panel } = Collapse; | ||||
const { TextArea } = Input; | const { TextArea } = Input; | ||||
import { Row, Col } from 'antd'; | import { Row, Col } from 'antd'; | ||||
import Paragraph from 'antd/lib/typography/Paragraph'; | import Paragraph from 'antd/lib/typography/Paragraph'; | ||||
import PrimaryButton, { | import PrimaryButton, { | ||||
SecondaryButton, | SecondaryButton, | ||||
SmartButton, | SmartButton, | ||||
} from '@components/Common/PrimaryButton'; | } from '@components/Common/PrimaryButton'; | ||||
import useBCH from '@hooks/useBCH'; | import useBCH from '@hooks/useBCH'; | ||||
import useWindowDimensions from '@hooks/useWindowDimensions'; | import useWindowDimensions from '@hooks/useWindowDimensions'; | ||||
import { | import { | ||||
sendXecNotification, | sendXecNotification, | ||||
errorNotification, | errorNotification, | ||||
messageSignedNotification, | messageSignedNotification, | ||||
} from '@components/Common/Notifications'; | } from '@components/Common/Notifications'; | ||||
import { isMobile, isIOS, isSafari } from 'react-device-detect'; | import { isMobile, isIOS, isSafari } from 'react-device-detect'; | ||||
import { | import { | ||||
currency, | currency, | ||||
isValidTokenPrefix, | isValidTokenPrefix, | ||||
parseAddress, | parseAddress, | ||||
toLegacy, | toLegacy, | ||||
toLegacyArray, | |||||
} from '@components/Common/Ticker.js'; | } from '@components/Common/Ticker.js'; | ||||
import { Event } from '@utils/GoogleAnalytics'; | import { Event } from '@utils/GoogleAnalytics'; | ||||
import { fiatToCrypto, shouldRejectAmountInput } from '@utils/validation'; | import { | ||||
fiatToCrypto, | |||||
shouldRejectAmountInput, | |||||
isValidSendToMany, | |||||
} from '@utils/validation'; | |||||
import BalanceHeader from '@components/Common/BalanceHeader'; | import BalanceHeader from '@components/Common/BalanceHeader'; | ||||
import BalanceHeaderFiat from '@components/Common/BalanceHeaderFiat'; | import BalanceHeaderFiat from '@components/Common/BalanceHeaderFiat'; | ||||
import { | import { | ||||
ZeroBalanceHeader, | ZeroBalanceHeader, | ||||
ConvertAmount, | ConvertAmount, | ||||
AlertMsg, | AlertMsg, | ||||
} from '@components/Common/Atoms'; | } from '@components/Common/Atoms'; | ||||
import { getWalletState } from '@utils/cashMethods'; | import { getWalletState } from '@utils/cashMethods'; | ||||
Show All 13 Lines | const StyledSpacer = styled.div` | ||||
background-color: ${props => props.theme.wallet.borders.color}; | background-color: ${props => props.theme.wallet.borders.color}; | ||||
margin: 60px 0 50px; | margin: 60px 0 50px; | ||||
`; | `; | ||||
const SignMessageLabel = styled.div` | const SignMessageLabel = styled.div` | ||||
text-align: left; | text-align: left; | ||||
color: #0074c2; | color: #0074c2; | ||||
`; | `; | ||||
const RecipientModeLabel = styled.div` | |||||
color: silver; | |||||
`; | |||||
const TextAreaLabel = styled.div` | const TextAreaLabel = styled.div` | ||||
text-align: left; | text-align: left; | ||||
color: #0074c2; | color: #0074c2; | ||||
padding-left: 1px; | padding-left: 1px; | ||||
`; | `; | ||||
// Note jestBCH is only used for unit tests; BCHJS must be mocked for jest | // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest | ||||
const SendBCH = ({ jestBCH, passLoadingStatus }) => { | const SendBCH = ({ jestBCH, passLoadingStatus }) => { | ||||
// use balance parameters from wallet.state object and not legacy balances parameter from walletState, if user has migrated wallet | // 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 | // 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 | // If the wallet object from ContextValue has a `state key`, then check which keys are in the wallet object | ||||
// Else set it as blank | // Else set it as blank | ||||
const ContextValue = React.useContext(WalletContext); | const ContextValue = React.useContext(WalletContext); | ||||
const location = useLocation(); | const location = useLocation(); | ||||
const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; | const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; | ||||
const walletState = getWalletState(wallet); | const walletState = getWalletState(wallet); | ||||
const { balances, slpBalancesAndUtxos } = walletState; | const { balances, slpBalancesAndUtxos } = walletState; | ||||
// Modal settings | // Modal settings | ||||
const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); | const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); | ||||
const [msgToSign, setMsgToSign] = useState(''); | const [msgToSign, setMsgToSign] = useState(''); | ||||
const [signMessageIsValid, setSignMessageIsValid] = useState(null); | const [signMessageIsValid, setSignMessageIsValid] = useState(null); | ||||
const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); | |||||
// Get device window width | // Get device window width | ||||
// If this is less than 769, the page will open with QR scanner open | // If this is less than 769, the page will open with QR scanner open | ||||
const { width } = useWindowDimensions(); | const { width } = useWindowDimensions(); | ||||
// Load with QR code open if device is mobile and NOT iOS + anything but safari | // Load with QR code open if device is mobile and NOT iOS + anything but safari | ||||
const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari); | const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari); | ||||
const [formData, setFormData] = useState({ | const [formData, setFormData] = useState({ | ||||
dirty: true, | dirty: true, | ||||
Show All 15 Lines | const SendBCH = ({ jestBCH, passLoadingStatus }) => { | ||||
const [sigCopySuccess, setSigCopySuccess] = useState(''); | const [sigCopySuccess, setSigCopySuccess] = useState(''); | ||||
const showModal = () => { | const showModal = () => { | ||||
setIsModalVisible(true); | setIsModalVisible(true); | ||||
}; | }; | ||||
const handleOk = () => { | const handleOk = () => { | ||||
setIsModalVisible(false); | setIsModalVisible(false); | ||||
submit(); | send(); | ||||
}; | }; | ||||
const handleCancel = () => { | const handleCancel = () => { | ||||
setIsModalVisible(false); | setIsModalVisible(false); | ||||
}; | }; | ||||
const { getBCH, getRestUrl, sendBch, calcFee, signPkMessage } = useBCH(); | const { getBCH, getRestUrl, sendXec, calcFee, signPkMessage } = useBCH(); | ||||
// jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); | // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); | ||||
const BCH = jestBCH ? jestBCH : getBCH(); | const BCH = jestBCH ? jestBCH : getBCH(); | ||||
// If the balance has changed, unlock the UI | // 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 | // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked | ||||
useEffect(() => { | useEffect(() => { | ||||
passLoadingStatus(false); | passLoadingStatus(false); | ||||
Show All 38 Lines | function populateFormsFromUrl(txInfo) { | ||||
if (txInfo && txInfo.address && txInfo.value) { | if (txInfo && txInfo.address && txInfo.value) { | ||||
setFormData({ | setFormData({ | ||||
address: txInfo.address, | address: txInfo.address, | ||||
value: txInfo.value, | value: txInfo.value, | ||||
}); | }); | ||||
} | } | ||||
} | } | ||||
async function submit() { | 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({ | setFormData({ | ||||
...formData, | ...formData, | ||||
dirty: false, | dirty: false, | ||||
}); | }); | ||||
let optionalOpReturnMsg = formData.opReturnMsg; | |||||
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, value } = 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 = | |||||
toLegacyArray(addressAndValueArray); | |||||
const link = await sendXec( | |||||
BCH, | |||||
wallet, | |||||
slpBalancesAndUtxos.nonSlpUtxos, | |||||
currency.defaultFee, | |||||
optionalOpReturnMsg, | |||||
true, // indicate send mode is one to many | |||||
cleanAddressAndValueArray, | |||||
); | |||||
sendXecNotification(link); | |||||
} catch (e) { | |||||
handleSendXecError(e, isOneToManyXECSend); | |||||
} | |||||
} else { | |||||
// standard one to one XEC send transaction | |||||
if ( | if ( | ||||
!formData.address || | !formData.address || | ||||
!formData.value || | !formData.value || | ||||
Number(formData.value) <= 0 | Number(formData.value) <= 0 | ||||
) { | ) { | ||||
return; | return; | ||||
} | } | ||||
let optionalOpReturnMsg = formData.opReturnMsg; | |||||
// Event("Category", "Action", "Label") | // Event("Category", "Action", "Label") | ||||
// Track number of BCHA send transactions and whether users | // Track number of BCHA send transactions and whether users | ||||
// are sending BCHA or USD | // are sending BCHA or USD | ||||
Event('Send.js', 'Send', selectedCurrency); | Event('Send.js', 'Send', selectedCurrency); | ||||
passLoadingStatus(true); | passLoadingStatus(true); | ||||
const { address, value } = formData; | const { address, value } = formData; | ||||
// Get the param-free address | // Get the param-free address | ||||
let cleanAddress = address.split('?')[0]; | let cleanAddress = address.split('?')[0]; | ||||
// Ensure address has bitcoincash: prefix and checksum | // Ensure address has bitcoincash: prefix and checksum | ||||
cleanAddress = toLegacy(cleanAddress); | cleanAddress = toLegacy(cleanAddress); | ||||
let hasValidCashPrefix; | let hasValidCashPrefix; | ||||
try { | try { | ||||
hasValidCashPrefix = cleanAddress.startsWith( | hasValidCashPrefix = cleanAddress.startsWith( | ||||
currency.legacyPrefix + ':', | currency.legacyPrefix + ':', | ||||
); | ); | ||||
} catch (err) { | } catch (err) { | ||||
hasValidCashPrefix = false; | hasValidCashPrefix = false; | ||||
console.log(`toLegacy() returned an error:`, cleanAddress); | console.log(`toLegacy() returned an error:`, cleanAddress); | ||||
} | } | ||||
if (!hasValidCashPrefix) { | if (!hasValidCashPrefix) { | ||||
// set loading to false and set address validation to false | // set loading to false and set address validation to false | ||||
// Now that the no-prefix case is handled, this happens when user tries to send | // Now that the no-prefix case is handled, this happens when user tries to send | ||||
// BCHA to an SLPA address | // BCHA to an SLPA address | ||||
passLoadingStatus(false); | passLoadingStatus(false); | ||||
setSendBchAddressError( | setSendBchAddressError( | ||||
`Destination is not a valid ${currency.ticker} address`, | `Destination is not a valid ${currency.ticker} address`, | ||||
); | ); | ||||
return; | return; | ||||
} | } | ||||
// Calculate the amount in BCH | // Calculate the amount in BCH | ||||
let bchValue = value; | let bchValue = value; | ||||
if (selectedCurrency !== 'XEC') { | if (selectedCurrency !== 'XEC') { | ||||
bchValue = fiatToCrypto(value, fiatPrice); | bchValue = fiatToCrypto(value, fiatPrice); | ||||
} | } | ||||
try { | try { | ||||
const link = await sendBch( | const link = await sendXec( | ||||
BCH, | BCH, | ||||
wallet, | wallet, | ||||
slpBalancesAndUtxos.nonSlpUtxos, | slpBalancesAndUtxos.nonSlpUtxos, | ||||
cleanAddress, | |||||
bchValue, | |||||
currency.defaultFee, | currency.defaultFee, | ||||
optionalOpReturnMsg, | optionalOpReturnMsg, | ||||
false, // sendToMany boolean flag | |||||
null, // address array not applicable for one to many tx | |||||
cleanAddress, | |||||
bchValue, | |||||
); | ); | ||||
sendXecNotification(link); | sendXecNotification(link); | ||||
} catch (e) { | } catch (e) { | ||||
// Set loading to false here as well, as balance may not change depending on where error occured in try loop | handleSendXecError(e, isOneToManyXECSend); | ||||
passLoadingStatus(false); | |||||
let message; | |||||
if (!e.error && !e.message) { | |||||
message = `Transaction failed: no response from ${getRestUrl()}.`; | |||||
} else if ( | |||||
/Could not communicate with full node or other external service/.test( | |||||
e.error, | |||||
) | |||||
) { | |||||
message = 'Could not communicate with API. Please try again.'; | |||||
} else if ( | |||||
e.error && | |||||
e.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 = e.message || e.error || JSON.stringify(e); | |||||
} | } | ||||
errorNotification(e, message, 'Sending XEC'); | |||||
} | } | ||||
} | } | ||||
const handleAddressChange = e => { | const handleAddressChange = e => { | ||||
const { value, name } = e.target; | const { value, name } = e.target; | ||||
let error = false; | let error = false; | ||||
let addressString = value; | let addressString = value; | ||||
▲ Show 20 Lines • Show All 48 Lines • ▼ Show 20 Lines | const handleAddressChange = e => { | ||||
// Set address field to user input | // Set address field to user input | ||||
setFormData(p => ({ | setFormData(p => ({ | ||||
...p, | ...p, | ||||
[name]: value, | [name]: value, | ||||
})); | })); | ||||
}; | }; | ||||
const handleMultiAddressChange = e => { | |||||
const { value, name } = e.target; | |||||
let error; | |||||
if (!value) { | |||||
error = 'recipient input must not be blank'; | |||||
} | |||||
//convert each line from the <TextArea> input into array | |||||
let addressStringArray = value.split('\n'); | |||||
const arrayLength = addressStringArray.length; | |||||
// loop through each row in the <TextArea> 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'; | |||||
break; | |||||
} | |||||
let addressString = addressStringArray[i].split(',')[0]; | |||||
let valueString = addressStringArray[i].split(',')[1]; | |||||
let addressInfo = parseAddress(BCH, addressString); | |||||
let validation = isValidSendToMany( | |||||
addressInfo, | |||||
valueString, | |||||
currency.ticker, | |||||
); | |||||
if (validation !== true) { | |||||
// not using 'if(validation)' since error strings can be interpreted the same | |||||
error = validation; | |||||
} | |||||
if (error) { | |||||
// if one line is invalid, break loop and avoid wasting further iterations | |||||
break; | |||||
} | |||||
} | |||||
// no error displayed if isValid returns true, otherwise display error message | |||||
if (error) { | |||||
setSendBchAddressError(error); | |||||
} else { | |||||
setSendBchAddressError(false); | |||||
} | |||||
// Set address field to user input | |||||
setFormData(p => ({ | |||||
...p, | |||||
[name]: value, | |||||
})); | |||||
}; | |||||
const handleSelectedCurrencyChange = e => { | const handleSelectedCurrencyChange = e => { | ||||
setSelectedCurrency(e); | setSelectedCurrency(e); | ||||
// Clear input field to prevent accidentally sending 1 BCH instead of 1 USD | // Clear input field to prevent accidentally sending 1 BCH instead of 1 USD | ||||
setFormData(p => ({ | setFormData(p => ({ | ||||
...p, | ...p, | ||||
value: '', | value: '', | ||||
})); | })); | ||||
}; | }; | ||||
const handleOpReturnMsgChange = e => { | const handleOpReturnMsgChange = e => { | ||||
const { value, name } = e.target; | const { value, name } = e.target; | ||||
setFormData(p => ({ | setFormData(p => ({ | ||||
...p, | ...p, | ||||
[name]: value, | [name]: value, | ||||
})); | })); | ||||
}; | }; | ||||
// true: renders the multi recipient <TextArea> | |||||
// false: renders the single recipient <Input> | |||||
const handleOneToManyXECSend = sendXecMode => { | |||||
setIsOneToManyXECSend(sendXecMode); | |||||
}; | |||||
const handleBchAmountChange = e => { | const handleBchAmountChange = e => { | ||||
const { value, name } = e.target; | const { value, name } = e.target; | ||||
let bchValue = value; | let bchValue = value; | ||||
const error = shouldRejectAmountInput( | const error = shouldRejectAmountInput( | ||||
bchValue, | bchValue, | ||||
selectedCurrency, | selectedCurrency, | ||||
fiatPrice, | fiatPrice, | ||||
balances.totalBalance, | balances.totalBalance, | ||||
▲ Show 20 Lines • Show All 148 Lines • ▼ Show 20 Lines | return ( | ||||
<Row type="flex"> | <Row type="flex"> | ||||
<Col span={24}> | <Col span={24}> | ||||
<Form | <Form | ||||
style={{ | style={{ | ||||
width: 'auto', | width: 'auto', | ||||
}} | }} | ||||
> | > | ||||
{!isOneToManyXECSend ? ( | |||||
<> | |||||
<Button | |||||
type="text" | |||||
block | |||||
onClick={() => setIsOneToManyXECSend(true)} | |||||
> | |||||
<RecipientModeLabel> | |||||
Switch to multiple recipients | |||||
</RecipientModeLabel> | |||||
</Button> | |||||
<DestinationAddressSingle | <DestinationAddressSingle | ||||
loadWithCameraOpen={scannerSupported} | loadWithCameraOpen={scannerSupported} | ||||
validateStatus={sendBchAddressError ? 'error' : ''} | validateStatus={ | ||||
sendBchAddressError ? 'error' : '' | |||||
} | |||||
help={ | help={ | ||||
sendBchAddressError ? sendBchAddressError : '' | sendBchAddressError | ||||
? sendBchAddressError | |||||
: '' | |||||
} | } | ||||
onScan={result => | onScan={result => | ||||
handleAddressChange({ | handleAddressChange({ | ||||
target: { | target: { | ||||
name: 'address', | name: 'address', | ||||
value: result, | value: result, | ||||
}, | }, | ||||
}) | }) | ||||
} | } | ||||
inputProps={{ | inputProps={{ | ||||
placeholder: `${currency.ticker} Address`, | placeholder: `${currency.ticker} Address`, | ||||
name: 'address', | name: 'address', | ||||
onChange: e => handleAddressChange(e), | onChange: e => handleAddressChange(e), | ||||
required: true, | required: true, | ||||
value: formData.address, | value: formData.address, | ||||
}} | }} | ||||
></DestinationAddressSingle> | ></DestinationAddressSingle> | ||||
<SendBchInput | <SendBchInput | ||||
activeFiatCode={ | activeFiatCode={ | ||||
cashtabSettings && cashtabSettings.fiatCurrency | cashtabSettings && | ||||
cashtabSettings.fiatCurrency | |||||
? cashtabSettings.fiatCurrency.toUpperCase() | ? cashtabSettings.fiatCurrency.toUpperCase() | ||||
: 'USD' | : 'USD' | ||||
} | } | ||||
validateStatus={sendBchAmountError ? 'error' : ''} | validateStatus={ | ||||
help={sendBchAmountError ? sendBchAmountError : ''} | sendBchAmountError ? 'error' : '' | ||||
} | |||||
help={ | |||||
sendBchAmountError | |||||
? sendBchAmountError | |||||
: '' | |||||
} | |||||
onMax={onMax} | onMax={onMax} | ||||
inputProps={{ | inputProps={{ | ||||
name: 'value', | name: 'value', | ||||
dollar: selectedCurrency === 'USD' ? 1 : 0, | dollar: | ||||
selectedCurrency === 'USD' ? 1 : 0, | |||||
placeholder: 'Amount', | placeholder: 'Amount', | ||||
onChange: e => handleBchAmountChange(e), | onChange: e => handleBchAmountChange(e), | ||||
required: true, | required: true, | ||||
value: formData.value, | value: formData.value, | ||||
}} | }} | ||||
selectProps={{ | selectProps={{ | ||||
value: selectedCurrency, | value: selectedCurrency, | ||||
disabled: queryStringText !== null, | disabled: queryStringText !== null, | ||||
onChange: e => handleSelectedCurrencyChange(e), | onChange: e => | ||||
handleSelectedCurrencyChange(e), | |||||
}} | }} | ||||
></SendBchInput> | ></SendBchInput> | ||||
{priceApiError && ( | {priceApiError && ( | ||||
<AlertMsg> | <AlertMsg> | ||||
Error fetching fiat price. Setting send by{' '} | Error fetching fiat price. Setting send | ||||
by{' '} | |||||
{currency.fiatCurrencies[ | {currency.fiatCurrencies[ | ||||
cashtabSettings.fiatCurrency | cashtabSettings.fiatCurrency | ||||
].slug.toUpperCase()}{' '} | ].slug.toUpperCase()}{' '} | ||||
disabled | disabled | ||||
</AlertMsg> | </AlertMsg> | ||||
)} | )} | ||||
<ConvertAmount> | <ConvertAmount> | ||||
{fiatPriceString !== '' && '='} {fiatPriceString} | {fiatPriceString !== '' && '='}{' '} | ||||
{fiatPriceString} | |||||
</ConvertAmount> | </ConvertAmount> | ||||
</> | |||||
) : ( | |||||
<> | |||||
<Button | |||||
type="text" | |||||
block | |||||
onClick={() => setIsOneToManyXECSend(false)} | |||||
> | |||||
<RecipientModeLabel> | |||||
Switch to a single recipient | |||||
</RecipientModeLabel> | |||||
</Button> | |||||
<DestinationAddressMulti | |||||
validateStatus={ | |||||
sendBchAddressError ? 'error' : '' | |||||
} | |||||
help={ | |||||
sendBchAddressError | |||||
? sendBchAddressError | |||||
: '' | |||||
} | |||||
inputProps={{ | |||||
placeholder: `One XEC address & value per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`, | |||||
name: 'address', | |||||
onChange: e => | |||||
handleMultiAddressChange(e), | |||||
required: true, | |||||
value: formData.address, | |||||
}} | |||||
></DestinationAddressMulti> | |||||
</> | |||||
)} | |||||
<div | <div | ||||
style={{ | style={{ | ||||
paddingTop: '32px', | paddingTop: '32px', | ||||
}} | }} | ||||
> | > | ||||
<AdvancedCollapse | <AdvancedCollapse | ||||
style={{ | style={{ | ||||
marginBottom: '24px', | marginBottom: '24px', | ||||
▲ Show 20 Lines • Show All 45 Lines • ▼ Show 20 Lines | return ( | ||||
<> | <> | ||||
{txInfoFromUrl ? ( | {txInfoFromUrl ? ( | ||||
<PrimaryButton | <PrimaryButton | ||||
onClick={() => showModal()} | onClick={() => showModal()} | ||||
> | > | ||||
Send | Send | ||||
</PrimaryButton> | </PrimaryButton> | ||||
) : ( | ) : ( | ||||
<PrimaryButton onClick={() => submit()}> | <PrimaryButton onClick={() => send()}> | ||||
Send | Send | ||||
</PrimaryButton> | </PrimaryButton> | ||||
)} | )} | ||||
</> | </> | ||||
)} | )} | ||||
</div> | </div> | ||||
{queryStringText && ( | {queryStringText && ( | ||||
<Alert | <Alert | ||||
▲ Show 20 Lines • Show All 112 Lines • Show Last 20 Lines |