Changeset View
Changeset View
Standalone View
Standalone View
src/cashtab/src/components/Send/SendToken.js
- This file was added.
import React, { useState, useEffect } from "react"; | |||||
import { WalletContext } from "../../utils/context"; | |||||
import { Alert, Form, notification, message } from "antd"; | |||||
import { CashSpin, CashSpinIcon } from "../Common/CustomSpinner"; | |||||
import { Row, Col } from "antd"; | |||||
import Paragraph from "antd/lib/typography/Paragraph"; | |||||
import PrimaryButton, { SecondaryButton } from "../Common/PrimaryButton"; | |||||
import { CashLoader } from "../Common/CustomIcons"; | |||||
import { FormItemWithMaxAddon, FormItemWithQRCodeAddon } from "../Common/EnhancedInputs"; | |||||
import useBCH from "../../hooks/useBCH"; | |||||
import { BalanceHeader } from "./Send"; | |||||
import { Redirect } from "react-router-dom"; | |||||
import useWindowDimensions from "../../hooks/useWindowDimensions"; | |||||
import { isMobile, isIOS, isSafari } from "react-device-detect"; | |||||
import { Img } from "react-image"; | |||||
import makeBlockie from "ethereum-blockies-base64"; | |||||
import BigNumber from "bignumber.js"; | |||||
import { currency } from "../Common/Ticker.js"; | |||||
const SendToken = ({ tokenId }) => { | |||||
const { wallet, tokens, slpBalancesAndUtxos, apiError } = React.useContext(WalletContext); | |||||
const token = tokens.find(token => token.tokenId === tokenId); | |||||
const [sendTokenAmountError, setSendTokenAmountError] = 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 = width < 769 && isMobile && !(isIOS && !isSafari); | |||||
const [formData, setFormData] = useState({ | |||||
dirty: true, | |||||
value: "", | |||||
address: "" | |||||
}); | |||||
const [loading, setLoading] = useState(false); | |||||
const { getBCH, getRestUrl, sendToken } = useBCH(); | |||||
const BCH = getBCH(); | |||||
async function submit() { | |||||
setFormData({ | |||||
...formData, | |||||
dirty: false | |||||
}); | |||||
if ( | |||||
!formData.address || | |||||
!formData.value || | |||||
Number(formData.value <= 0) || | |||||
sendTokenAmountError | |||||
) { | |||||
return; | |||||
} | |||||
setLoading(true); | |||||
const { address, value } = formData; | |||||
try { | |||||
const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, { | |||||
tokenId: tokenId, | |||||
tokenReceiverAddress: address, | |||||
amount: value | |||||
}); | |||||
notification.success({ | |||||
message: "Success", | |||||
description: ( | |||||
<a href={link} target="_blank" rel="noopener noreferrer"> | |||||
<Paragraph>Transaction successful. Click or tap here for more details</Paragraph> | |||||
</a> | |||||
), | |||||
duration: 5 | |||||
}); | |||||
} catch (e) { | |||||
setLoading(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 { | |||||
message = e.message || e.error || JSON.stringify(e); | |||||
} | |||||
console.log(e); | |||||
notification.error({ | |||||
message: "Error", | |||||
description: message, | |||||
duration: 3 | |||||
}); | |||||
console.error(e); | |||||
} | |||||
} | |||||
const handleSlpAmountChange = e => { | |||||
let error = false; | |||||
const { value, name } = e.target; | |||||
// test if exceeds balance using BigNumber | |||||
let isGreaterThanBalance = false; | |||||
if (!isNaN(value)) { | |||||
const bigValue = new BigNumber(value); | |||||
// Returns 1 if greater, -1 if less, 0 if the same, null if n/a | |||||
isGreaterThanBalance = bigValue.comparedTo(token.balance); | |||||
} | |||||
// Validate value for > 0 | |||||
if (isNaN(value)) { | |||||
error = "Amount must be a number"; | |||||
} else if (value <= 0) { | |||||
error = "Amount must be greater than 0"; | |||||
} else if (token && token.balance && isGreaterThanBalance === 1) { | |||||
error = `Amount cannot exceed your ${token.info.tokenTicker} balance of ${token.balance}`; | |||||
} else if (!isNaN(value) && value.toString().includes(".")) { | |||||
if (value.toString().split(".")[1].length > token.info.decimals) { | |||||
error = `This token only supports ${token.info.decimals} decimal places`; | |||||
} | |||||
} | |||||
setSendTokenAmountError(error); | |||||
setFormData(p => ({ ...p, [name]: value })); | |||||
}; | |||||
const handleChange = e => { | |||||
const { value, name } = e.target; | |||||
setFormData(p => ({ ...p, [name]: value })); | |||||
}; | |||||
const onMax = async () => { | |||||
// Clear this error before updating field | |||||
setSendTokenAmountError(false); | |||||
try { | |||||
let value = token.balance; | |||||
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"); | |||||
} | |||||
}; | |||||
useEffect(() => { | |||||
// 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 | |||||
setLoading(false); | |||||
}, [token]); | |||||
return ( | |||||
<> | |||||
{!token && <Redirect to="/" />} | |||||
{token && ( | |||||
<> | |||||
<BalanceHeader> | |||||
<p>Available balance</p> | |||||
<h3> | |||||
{token.balance.toString()} {token.info.tokenTicker} | |||||
</h3> | |||||
</BalanceHeader> | |||||
<Row type="flex"> | |||||
<Col span={24}> | |||||
<CashSpin spinning={loading} indicator={CashSpinIcon}> | |||||
<Form style={{ width: "auto" }}> | |||||
<FormItemWithQRCodeAddon | |||||
loadWithCameraOpen={scannerSupported} | |||||
validateStatus={!formData.dirty && !formData.address ? "error" : ""} | |||||
help={ | |||||
!formData.dirty && !formData.address ? "Should be a valid bch address" : "" | |||||
} | |||||
onScan={result => setFormData({ ...formData, address: result })} | |||||
inputProps={{ | |||||
placeholder: `${currency.tokenTicker} Address`, | |||||
name: "address", | |||||
onChange: e => handleChange(e), | |||||
required: true, | |||||
value: formData.address | |||||
}} | |||||
/> | |||||
<FormItemWithMaxAddon | |||||
validateStatus={sendTokenAmountError ? "error" : ""} | |||||
help={sendTokenAmountError ? sendTokenAmountError : ""} | |||||
onMax={onMax} | |||||
inputProps={{ | |||||
name: "value", | |||||
placeholder: "Amount", | |||||
prefix: ( | |||||
<Img | |||||
src={`${currency.tokenIconsUrl}/${tokenId}.png`} | |||||
width={16} | |||||
height={16} | |||||
unloader={ | |||||
<img | |||||
alt={`identicon of tokenId ${tokenId} `} | |||||
heigh="16" | |||||
width="16" | |||||
style={{ | |||||
borderRadius: "50%" | |||||
}} | |||||
key={`identicon-${tokenId}`} | |||||
src={makeBlockie(tokenId)} | |||||
/> | |||||
} | |||||
/> | |||||
), | |||||
suffix: token.info.tokenTicker, | |||||
onChange: e => handleSlpAmountChange(e), | |||||
required: true, | |||||
value: formData.value | |||||
}} | |||||
/> | |||||
<div style={{ paddingTop: "12px" }}> | |||||
{apiError || sendTokenAmountError ? ( | |||||
<> | |||||
<SecondaryButton>Send {token.info.tokenName}</SecondaryButton> | |||||
{apiError && <CashLoader />} | |||||
</> | |||||
) : ( | |||||
<PrimaryButton onClick={() => submit()}> | |||||
Send {token.info.tokenName} | |||||
</PrimaryButton> | |||||
)} | |||||
</div> | |||||
{apiError && ( | |||||
<p style={{ color: "red" }}> | |||||
<b>An error occured on our end. Reconnecting...</b> | |||||
</p> | |||||
)} | |||||
</Form> | |||||
<Alert | |||||
message="Warning" | |||||
description="This wallet sends transactions with a Bitcoin ABC node. SLP tokens existing before the Nov 15, 2020 fork may not be supported on both chains." | |||||
type="warning" | |||||
showIcon | |||||
closable | |||||
style={{ textAlign: "left" }} | |||||
/> | |||||
</CashSpin> | |||||
</Col> | |||||
</Row> | |||||
</> | |||||
)} | |||||
</> | |||||
); | |||||
}; | |||||
export default SendToken; |