Changeset View
Changeset View
Standalone View
Standalone View
web/cashtab/src/components/Send/SendToken.js
import React, { useState, useEffect } from 'react'; | import React, { useState, useEffect } from 'react'; | ||||
import { WalletContext } from '@utils/context'; | import { WalletContext } from '@utils/context'; | ||||
import { Form, notification, message, Spin, Row, Col } from 'antd'; | import { Form, notification, message, Spin, Row, Col, Alert } 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 { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; | import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; | ||||
import { | import { | ||||
FormItemWithMaxAddon, | FormItemWithMaxAddon, | ||||
FormItemWithQRCodeAddon, | FormItemWithQRCodeAddon, | ||||
} from '@components/Common/EnhancedInputs'; | } from '@components/Common/EnhancedInputs'; | ||||
import useBCH from '@hooks/useBCH'; | import useBCH from '@hooks/useBCH'; | ||||
import { BalanceHeader } from './Send'; | import { BalanceHeader } from './Send'; | ||||
import { Redirect } from 'react-router-dom'; | import { Redirect } from 'react-router-dom'; | ||||
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 { Img } from 'react-image'; | import { Img } from 'react-image'; | ||||
import makeBlockie from 'ethereum-blockies-base64'; | import makeBlockie from 'ethereum-blockies-base64'; | ||||
import BigNumber from 'bignumber.js'; | import BigNumber from 'bignumber.js'; | ||||
import { currency } from '@components/Common/Ticker.js'; | import { currency, parseAddress, isToken } from '@components/Common/Ticker.js'; | ||||
import { Event } from '@utils/GoogleAnalytics'; | import { Event } from '@utils/GoogleAnalytics'; | ||||
const SendToken = ({ tokenId }) => { | const SendToken = ({ tokenId }) => { | ||||
const { wallet, tokens, slpBalancesAndUtxos, apiError } = React.useContext( | const { wallet, tokens, slpBalancesAndUtxos, apiError } = React.useContext( | ||||
WalletContext, | WalletContext, | ||||
); | ); | ||||
const token = tokens.find(token => token.tokenId === tokenId); | const token = tokens.find(token => token.tokenId === tokenId); | ||||
const [queryStringText, setQueryStringText] = useState(null); | |||||
const [sendTokenAddressError, setSendTokenAddressError] = useState(false); | |||||
const [sendTokenAmountError, setSendTokenAmountError] = useState(false); | const [sendTokenAmountError, setSendTokenAmountError] = 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); | ||||
Show All 27 Lines | async function submit() { | ||||
// Event("Category", "Action", "Label") | // Event("Category", "Action", "Label") | ||||
// Track number of SLPA send transactions and | // Track number of SLPA send transactions and | ||||
// SLPA token IDs | // SLPA token IDs | ||||
Event('SendToken.js', 'Send', tokenId); | Event('SendToken.js', 'Send', tokenId); | ||||
setLoading(true); | setLoading(true); | ||||
const { address, value } = formData; | const { address, value } = formData; | ||||
// Clear params from address | |||||
let cleanAddress = address.split('?')[0]; | |||||
try { | try { | ||||
const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, { | const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, { | ||||
tokenId: tokenId, | tokenId: tokenId, | ||||
tokenReceiverAddress: address, | tokenReceiverAddress: cleanAddress, | ||||
amount: value, | amount: value, | ||||
}); | }); | ||||
notification.success({ | notification.success({ | ||||
message: 'Success', | message: 'Success', | ||||
description: ( | description: ( | ||||
<a href={link} target="_blank" rel="noopener noreferrer"> | <a href={link} target="_blank" rel="noopener noreferrer"> | ||||
<Paragraph> | <Paragraph> | ||||
▲ Show 20 Lines • Show All 52 Lines • ▼ Show 20 Lines | const handleSlpAmountChange = e => { | ||||
if (value.toString().split('.')[1].length > token.info.decimals) { | if (value.toString().split('.')[1].length > token.info.decimals) { | ||||
error = `This token only supports ${token.info.decimals} decimal places`; | error = `This token only supports ${token.info.decimals} decimal places`; | ||||
} | } | ||||
} | } | ||||
setSendTokenAmountError(error); | setSendTokenAmountError(error); | ||||
setFormData(p => ({ ...p, [name]: value })); | setFormData(p => ({ ...p, [name]: value })); | ||||
}; | }; | ||||
const handleChange = e => { | const handleTokenAddressChange = e => { | ||||
const { value, name } = e.target; | const { value, name } = e.target; | ||||
// validate for token address | |||||
// validate for parameters | |||||
// show warning that query strings are not supported | |||||
setFormData(p => ({ ...p, [name]: value })); | let error = false; | ||||
let addressString = value; | |||||
// parse address | |||||
const addressInfo = parseAddress(BCH, addressString); | |||||
/* | |||||
Model | |||||
addressInfo = | |||||
{ | |||||
address: '', | |||||
isValid: false, | |||||
queryString: '', | |||||
amount: null, | |||||
}; | |||||
*/ | |||||
const { address, isValid, queryString } = 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 valid'; | |||||
// If valid address but token format | |||||
} else if (!isToken(address)) { | |||||
error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`; | |||||
} | |||||
setSendTokenAddressError(error); | |||||
setFormData(p => ({ | |||||
...p, | |||||
[name]: value, | |||||
})); | |||||
}; | }; | ||||
const onMax = async () => { | const onMax = async () => { | ||||
// Clear this error before updating field | // Clear this error before updating field | ||||
setSendTokenAmountError(false); | setSendTokenAmountError(false); | ||||
try { | try { | ||||
let value = token.balance; | let value = token.balance; | ||||
Show All 36 Lines | return ( | ||||
style={{ color: 'red' }} | style={{ color: 'red' }} | ||||
spinning={loading} | spinning={loading} | ||||
indicator={CashLoadingIcon} | indicator={CashLoadingIcon} | ||||
> | > | ||||
<Form style={{ width: 'auto' }}> | <Form style={{ width: 'auto' }}> | ||||
<FormItemWithQRCodeAddon | <FormItemWithQRCodeAddon | ||||
loadWithCameraOpen={scannerSupported} | loadWithCameraOpen={scannerSupported} | ||||
validateStatus={ | validateStatus={ | ||||
!formData.dirty && !formData.address | sendTokenAddressError ? 'error' : '' | ||||
? 'error' | |||||
: '' | |||||
} | } | ||||
help={ | help={ | ||||
!formData.dirty && !formData.address | sendTokenAddressError | ||||
? 'Should be a valid bch address' | ? sendTokenAddressError | ||||
: '' | : '' | ||||
} | } | ||||
onScan={result => | onScan={result => | ||||
setFormData({ | handleTokenAddressChange({ | ||||
...formData, | target: { | ||||
address: result, | name: 'address', | ||||
value: result, | |||||
}, | |||||
}) | }) | ||||
} | } | ||||
inputProps={{ | inputProps={{ | ||||
placeholder: `${currency.tokenTicker} Address`, | placeholder: `${currency.tokenTicker} Address`, | ||||
name: 'address', | name: 'address', | ||||
onChange: e => handleChange(e), | onChange: e => | ||||
handleTokenAddressChange(e), | |||||
required: true, | required: true, | ||||
value: formData.address, | value: formData.address, | ||||
}} | }} | ||||
/> | /> | ||||
<FormItemWithMaxAddon | <FormItemWithMaxAddon | ||||
validateStatus={ | validateStatus={ | ||||
sendTokenAmountError ? 'error' : '' | sendTokenAmountError ? 'error' : '' | ||||
} | } | ||||
▲ Show 20 Lines • Show All 46 Lines • ▼ Show 20 Lines | return ( | ||||
suffix: token.info.tokenTicker, | suffix: token.info.tokenTicker, | ||||
onChange: e => | onChange: e => | ||||
handleSlpAmountChange(e), | handleSlpAmountChange(e), | ||||
required: true, | required: true, | ||||
value: formData.value, | value: formData.value, | ||||
}} | }} | ||||
/> | /> | ||||
<div style={{ paddingTop: '12px' }}> | <div style={{ paddingTop: '12px' }}> | ||||
{apiError || sendTokenAmountError ? ( | {apiError || | ||||
sendTokenAmountError || | |||||
sendTokenAddressError ? ( | |||||
<> | <> | ||||
<SecondaryButton> | <SecondaryButton> | ||||
Send {token.info.tokenName} | Send {token.info.tokenName} | ||||
</SecondaryButton> | </SecondaryButton> | ||||
{apiError && <CashLoader />} | {apiError && <CashLoader />} | ||||
</> | </> | ||||
) : ( | ) : ( | ||||
<PrimaryButton | <PrimaryButton | ||||
onClick={() => submit()} | onClick={() => submit()} | ||||
> | > | ||||
Send {token.info.tokenName} | Send {token.info.tokenName} | ||||
</PrimaryButton> | </PrimaryButton> | ||||
)} | )} | ||||
</div> | </div> | ||||
{queryStringText && ( | |||||
<Alert | |||||
message={`You are sending a transaction to an address including query parameters "${queryStringText}." Token transactions do not support query parameters and they will be ignored.`} | |||||
type="warning" | |||||
/> | |||||
)} | |||||
{apiError && ( | {apiError && ( | ||||
<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> | ||||
</p> | </p> | ||||
)} | )} | ||||
Show All 11 Lines |