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 styled from 'styled-components'; | import styled from 'styled-components'; | ||||
import { WalletContext } from '@utils/context'; | import { WalletContext } from '@utils/context'; | ||||
import { Form, notification, message, Spin, Modal } from 'antd'; | import { Form, notification, message, Spin, Modal, Alert } from 'antd'; | ||||
import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; | import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; | ||||
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, | ||||
} from '@components/Common/PrimaryButton'; | } from '@components/Common/PrimaryButton'; | ||||
import { | import { | ||||
SendBchInput, | SendBchInput, | ||||
FormItemWithQRCodeAddon, | FormItemWithQRCodeAddon, | ||||
} from '@components/Common/EnhancedInputs'; | } from '@components/Common/EnhancedInputs'; | ||||
import useBCH from '@hooks/useBCH'; | import useBCH from '@hooks/useBCH'; | ||||
import useWindowDimensions from '@hooks/useWindowDimensions'; | import useWindowDimensions from '@hooks/useWindowDimensions'; | ||||
import { isMobile, isIOS, isSafari } from 'react-device-detect'; | import { isMobile, isIOS, isSafari } from 'react-device-detect'; | ||||
import { currency } from '@components/Common/Ticker.js'; | import { | ||||
currency, | |||||
isToken, | |||||
parseAddress, | |||||
toLegacy, | |||||
} from '@components/Common/Ticker.js'; | |||||
import { Event } from '@utils/GoogleAnalytics'; | import { Event } from '@utils/GoogleAnalytics'; | ||||
export const BalanceHeader = styled.div` | export const BalanceHeader = styled.div` | ||||
p { | p { | ||||
color: #777; | color: #777; | ||||
width: 100%; | width: 100%; | ||||
font-size: 14px; | font-size: 14px; | ||||
margin-bottom: 0px; | margin-bottom: 0px; | ||||
} | } | ||||
▲ Show 20 Lines • Show All 52 Lines • ▼ Show 20 Lines | const SendBCH = ({ filledAddress, callbackTxId }) => { | ||||
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, | ||||
value: '', | value: '', | ||||
address: filledAddress || '', | address: filledAddress || '', | ||||
}); | }); | ||||
const [loading, setLoading] = useState(false); | const [loading, setLoading] = useState(false); | ||||
const [queryStringText, setQueryStringText] = useState(null); | |||||
const [sendBchAddressError, setSendBchAddressError] = useState(false); | |||||
const [sendBchAmountError, setSendBchAmountError] = useState(false); | const [sendBchAmountError, setSendBchAmountError] = useState(false); | ||||
const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); | const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); | ||||
// Support cashtab button from web pages | // Support cashtab button from web pages | ||||
const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); | const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); | ||||
// Show a confirmation modal on transactions created by populating form from web page button | // Show a confirmation modal on transactions created by populating form from web page button | ||||
const [isModalVisible, setIsModalVisible] = useState(false); | const [isModalVisible, setIsModalVisible] = useState(false); | ||||
Show All 24 Lines | useEffect(() => { | ||||
// Manually parse for txInfo object on page load when Send.js is loaded with a query string | // Manually parse for txInfo object on page load when Send.js is loaded with a query string | ||||
// Do not set txInfo in state if query strings are not present | // Do not set txInfo in state if query strings are not present | ||||
if ( | if ( | ||||
!window.location || | !window.location || | ||||
!window.location.hash || | !window.location.hash || | ||||
window.location.hash === '#/send' | window.location.hash === '#/send' | ||||
) { | ) { | ||||
console.log(`No tx info in URL`); | |||||
return; | return; | ||||
} | } | ||||
const txInfoArr = window.location.hash.split('?')[1].split('&'); | const txInfoArr = window.location.hash.split('?')[1].split('&'); | ||||
// Iterate over this to create object | // Iterate over this to create object | ||||
const txInfo = {}; | const txInfo = {}; | ||||
for (let i = 0; i < txInfoArr.length; i += 1) { | for (let i = 0; i < txInfoArr.length; i += 1) { | ||||
Show All 30 Lines | async function submit() { | ||||
// 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); | ||||
setLoading(true); | setLoading(true); | ||||
const { address, value } = formData; | const { address, value } = formData; | ||||
// Get the param-free address | |||||
let cleanAddress = address.split('?')[0]; | |||||
// Ensure address has bitcoincash: prefix and checksum | |||||
cleanAddress = toLegacy(cleanAddress); | |||||
// If there was an error converting the address | |||||
if (!cleanAddress.startsWith('bitcoincash:')) { | |||||
// return as above with other errors | |||||
console.log(`toLegacy() returned an error:`, cleanAddress); | |||||
// Note: the address must be valid to get to this point, so unsure if this can be produced | |||||
return; | |||||
} | |||||
// Calculate the amount in BCH | // Calculate the amount in BCH | ||||
let bchValue = value; | let bchValue = value; | ||||
if (selectedCurrency === 'USD') { | if (selectedCurrency === 'USD') { | ||||
bchValue = (value / fiatPrice).toFixed(8); | bchValue = (value / fiatPrice).toFixed(8); | ||||
} | } | ||||
try { | try { | ||||
const link = await sendBch( | const link = await sendBch( | ||||
BCH, | BCH, | ||||
wallet, | wallet, | ||||
slpBalancesAndUtxos.nonSlpUtxos, | slpBalancesAndUtxos.nonSlpUtxos, | ||||
{ | { | ||||
addresses: [filledAddress || address], | addresses: [filledAddress || cleanAddress], | ||||
values: [bchValue], | values: [bchValue], | ||||
}, | }, | ||||
callbackTxId, | callbackTxId, | ||||
); | ); | ||||
notification.success({ | notification.success({ | ||||
message: 'Success', | message: 'Success', | ||||
description: ( | description: ( | ||||
Show All 34 Lines | async function submit() { | ||||
message: 'Error', | message: 'Error', | ||||
description: message, | description: message, | ||||
duration: 5, | duration: 5, | ||||
}); | }); | ||||
console.error(e); | console.error(e); | ||||
} | } | ||||
} | } | ||||
const handleChange = e => { | const handleAddressChange = e => { | ||||
const { value, name } = e.target; | const { value, name } = e.target; | ||||
let error = false; | |||||
let addressString = value; | |||||
setFormData(p => ({ ...p, [name]: value })); | // parse address | ||||
const addressInfo = parseAddress(BCH, addressString); | |||||
/* | |||||
Model | |||||
addressInfo = | |||||
{ | |||||
address: '', | |||||
isValid: false, | |||||
queryString: '', | |||||
amount: null, | |||||
}; | |||||
*/ | |||||
const { address, isValid, 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 = 'Address is not a valid cash address'; | |||||
// If valid address but token format | |||||
if (isToken(address)) { | |||||
error = `Token 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 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 => ({ ...p, value: '' })); | setFormData(p => ({ ...p, value: '' })); | ||||
}; | }; | ||||
▲ Show 20 Lines • Show All 104 Lines • ▼ Show 20 Lines | return ( | ||||
<Row type="flex"> | <Row type="flex"> | ||||
<Col span={24}> | <Col span={24}> | ||||
<Spin spinning={loading} indicator={CashLoadingIcon}> | <Spin spinning={loading} indicator={CashLoadingIcon}> | ||||
<Form style={{ width: 'auto' }}> | <Form style={{ width: 'auto' }}> | ||||
<FormItemWithQRCodeAddon | <FormItemWithQRCodeAddon | ||||
loadWithCameraOpen={scannerSupported} | loadWithCameraOpen={scannerSupported} | ||||
disabled={Boolean(filledAddress)} | disabled={Boolean(filledAddress)} | ||||
validateStatus={ | validateStatus={ | ||||
!formData.dirty && !formData.address | sendBchAddressError ? 'error' : '' | ||||
? 'error' | |||||
: '' | |||||
} | } | ||||
help={ | help={ | ||||
!formData.dirty && !formData.address | sendBchAddressError | ||||
? `Should be a valid ${currency.ticker} address` | ? sendBchAddressError | ||||
: '' | : '' | ||||
} | } | ||||
onScan={result => | onScan={result => | ||||
setFormData({ | handleAddressChange({ | ||||
...formData, | target: { | ||||
address: result, | name: 'address', | ||||
value: result, | |||||
}, | |||||
}) | }) | ||||
} | } | ||||
inputProps={{ | inputProps={{ | ||||
disabled: Boolean(filledAddress), | disabled: Boolean(filledAddress), | ||||
placeholder: `${currency.ticker} Address`, | placeholder: `${currency.ticker} Address`, | ||||
name: 'address', | name: 'address', | ||||
onChange: e => handleChange(e), | onChange: e => handleAddressChange(e), | ||||
required: true, | required: true, | ||||
value: filledAddress || formData.address, | value: filledAddress || formData.address, | ||||
}} | }} | ||||
></FormItemWithQRCodeAddon> | ></FormItemWithQRCodeAddon> | ||||
<SendBchInput | <SendBchInput | ||||
validateStatus={ | validateStatus={ | ||||
sendBchAmountError ? 'error' : '' | sendBchAmountError ? 'error' : '' | ||||
} | } | ||||
help={ | help={ | ||||
sendBchAmountError ? sendBchAmountError : '' | 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, | |||||
onChange: e => | onChange: e => | ||||
handleSelectedCurrencyChange(e), | handleSelectedCurrencyChange(e), | ||||
}} | }} | ||||
></SendBchInput> | ></SendBchInput> | ||||
<ConvertAmount>= {fiatPriceString}</ConvertAmount> | <ConvertAmount>= {fiatPriceString}</ConvertAmount> | ||||
<div style={{ paddingTop: '12px' }}> | <div style={{ paddingTop: '12px' }}> | ||||
{!balances.totalBalance || | {!balances.totalBalance || | ||||
apiError || | apiError || | ||||
sendBchAmountError ? ( | sendBchAmountError || | ||||
sendBchAddressError ? ( | |||||
<SecondaryButton>Send</SecondaryButton> | <SecondaryButton>Send</SecondaryButton> | ||||
) : ( | ) : ( | ||||
<> | <> | ||||
{txInfoFromUrl ? ( | {txInfoFromUrl ? ( | ||||
<PrimaryButton | <PrimaryButton | ||||
onClick={() => showModal()} | onClick={() => showModal()} | ||||
> | > | ||||
Send | Send | ||||
</PrimaryButton> | </PrimaryButton> | ||||
) : ( | ) : ( | ||||
<PrimaryButton | <PrimaryButton | ||||
onClick={() => submit()} | onClick={() => submit()} | ||||
> | > | ||||
Send | Send | ||||
</PrimaryButton> | </PrimaryButton> | ||||
)} | )} | ||||
</> | </> | ||||
)} | )} | ||||
</div> | </div> | ||||
{queryStringText && ( | |||||
<Alert | |||||
message={`You are sending a transaction to an address including query parameters "${queryStringText}." Only the "amount" parameter, in units of ${currency.ticker} satoshis, is currently supported.`} | |||||
type="warning" | |||||
/> | |||||
)} | |||||
{apiError && ( | {apiError && ( | ||||
<> | <> | ||||
<CashLoader /> | <CashLoader /> | ||||
<p style={{ color: 'red' }}> | <p style={{ color: 'red' }}> | ||||
<b> | <b> | ||||
An error occured on our end. | An error occured on our end. | ||||
Reconnecting... | Reconnecting... | ||||
</b> | </b> | ||||
Show All 12 Lines |