diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js index f6f3b4eba..304245118 100644 --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -1,304 +1,315 @@ -import React from 'react'; +import React, { useState } from 'react'; import 'antd/dist/antd.less'; import { Spin } from 'antd'; import { CashLoadingIcon } from '@components/Common/CustomIcons'; import '../index.css'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { theme } from '@assets/styles/theme'; import { FolderOpenFilled, CaretRightOutlined, SettingFilled, AppstoreAddOutlined, } from '@ant-design/icons'; import Wallet from '@components/Wallet/Wallet'; import Tokens from '@components/Tokens/Tokens'; import Send from '@components/Send/Send'; import SendToken from '@components/Send/SendToken'; import Configure from '@components/Configure/Configure'; import NotFound from '@components/NotFound'; import CashTab from '@assets/cashtab_xec.png'; import TabCash from '@assets/tabcash.png'; import ABC from '@assets/logo_topright.png'; import './App.css'; import { WalletContext } from '@utils/context'; import { checkForTokenById } from '@utils/tokenMethods.js'; import WalletLabel from '@components/Common/WalletLabel.js'; import { Route, Redirect, Switch, useLocation, useHistory, } from 'react-router-dom'; const GlobalStyle = createGlobalStyle` .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button { border-radius: 8px; background-color: ${props => props.theme.modals.buttons.background}; color: ${props => props.theme.wallet.text.secondary}; font-weight: bold; } .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover { color: ${props => props.theme.primary}; transition: color 0.3s; background-color: ${props => props.theme.modals.buttons.background}; } .selectedCurrencyOption { text-align: left; color: ${props => props.theme.wallet.text.secondary} !important; background-color: ${props => props.theme.contrast} !important; } .cashLoadingIcon { color: ${props => props.theme.primary} !important font-size: 48px !important; } .selectedCurrencyOption:hover { color: ${props => props.theme.contrast} !important; background-color: ${props => props.theme.primary} !important; } #addrSwitch { .ant-switch-checked { background-color: white !important; } } #addrSwitch.ant-switch-checked { background-image: ${props => props.theme.buttons.primary.backgroundImage} !important; } `; const CustomApp = styled.div` text-align: center; font-family: 'Gilroy', sans-serif; background-color: ${props => props.theme.app.background}; `; const Footer = styled.div` z-index: 2; background-color: ${props => props.theme.footer.background}; border-radius: 20px; position: fixed; bottom: 0; width: 500px; @media (max-width: 768px) { width: 100%; } border-top: 1px solid ${props => props.theme.wallet.borders.color}; `; export const NavButton = styled.button` :focus, :active { outline: none; } cursor: pointer; padding: 24px 12px 12px 12px; margin: 0 28px; @media (max-width: 475px) { margin: 0 20px; } @media (max-width: 420px) { margin: 0 12px; } @media (max-width: 350px) { margin: 0 8px; } background-color: ${props => props.theme.footer.background}; border: none; font-size: 12px; font-weight: bold; .anticon { display: block; color: ${props => props.theme.footer.navIconInactive}; font-size: 24px; margin-bottom: 6px; } ${({ active, ...props }) => active && ` color: ${props.theme.primary}; .anticon { color: ${props.theme.primary}; } `} `; export const WalletBody = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; min-height: 100vh; background-image: ${props => props.theme.app.sidebars}; background-attachment: fixed; `; export const WalletCtn = styled.div` position: relative; width: 500px; background-color: ${props => props.theme.footerBackground}; min-height: 100vh; padding: 10px 30px 120px 30px; background: ${props => props.theme.wallet.background}; -webkit-box-shadow: 0px 0px 24px 1px ${props => props.theme.wallet.shadow}; -moz-box-shadow: 0px 0px 24px 1px ${props => props.theme.wallet.shadow}; box-shadow: 0px 0px 24px 1px ${props => props.theme.wallet.shadow}; @media (max-width: 768px) { width: 100%; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } `; export const HeaderCtn = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; padding: 20px 0 30px; margin-bottom: 20px; justify-content: space-between; border-bottom: 1px solid ${props => props.theme.wallet.borders.color}; @media (max-width: 768px) { a { font-size: 12px; } padding: 10px 0 20px; } `; export const EasterEgg = styled.img` position: fixed; bottom: -195px; margin: 0; right: 10%; transition-property: bottom; transition-duration: 1.5s; transition-timing-function: ease-out; :hover { bottom: 0; } @media screen and (max-width: 1250px) { display: none; } `; export const CashTabLogo = styled.img` width: 120px; @media (max-width: 768px) { width: 110px; } `; export const AbcLogo = styled.img` width: 150px; @media (max-width: 768px) { width: 120px; } `; const App = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading, tokens } = ContextValue; + const [loadingUtxosAfterSend, setLoadingUtxosAfterSend] = useState(false); const hasTab = checkForTokenById( tokens, '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', ); const location = useLocation(); const history = useHistory(); const selectedKey = location && location.pathname ? location.pathname.substr(1) : ''; return ( - + {hasTab && ( )} - + ( )} /> {wallet ? ( ) : null} ); }; export default App; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index 9db2be3f0..c65b21dfd 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,565 +1,567 @@ import React, { useState, useEffect } from 'react'; import { WalletContext } from '@utils/context'; -import { Form, notification, message, Spin, Modal, Alert } from 'antd'; -import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; +import { Form, notification, message, Modal, Alert } from 'antd'; +import { CashLoader } from '@components/Common/CustomIcons'; import { Row, Col } from 'antd'; import Paragraph from 'antd/lib/typography/Paragraph'; import PrimaryButton, { SecondaryButton, } from '@components/Common/PrimaryButton'; import { SendBchInput, FormItemWithQRCodeAddon, } from '@components/Common/EnhancedInputs'; import useBCH from '@hooks/useBCH'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { currency, isValidTokenPrefix, parseAddress, toLegacy, } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput } from '@utils/validation'; import { BalanceHeader } from '@components/Common/BalanceHeader'; import { BalanceHeaderFiat } from '@components/Common/BalanceHeaderFiat'; import { ZeroBalanceHeader, ConvertAmount, AlertMsg, } from '@components/Common/Atoms'; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest -const SendBCH = ({ jestBCH, filledAddress, callbackTxId }) => { +const SendBCH = ({ + jestBCH, + passLoadingStatus, + filledAddress, + callbackTxId, +}) => { // 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 { wallet, fiatPrice, slpBalancesAndUtxos, apiError, cashtabSettings, } = ContextValue; let balances; const paramsInWalletState = wallet.state ? Object.keys(wallet.state) : []; // If wallet.state includes balances and parsedTxHistory params, use these // These are saved in indexedDb in the latest version of the app, hence accessible more quickly if (paramsInWalletState.includes('balances')) { balances = wallet.state.balances; } else { // If balances and parsedTxHistory are not in the wallet.state object, load them from Context // This is how the app used to work balances = ContextValue.balances; } // 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: filledAddress || '', }); - const [loading, setLoading] = useState(false); 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 showModal = () => { setIsModalVisible(true); }; const handleOk = () => { setIsModalVisible(false); submit(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getBCH, getRestUrl, sendBch, calcFee } = useBCH(); // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // 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(() => { - setLoading(false); + passLoadingStatus(false); }, [balances.totalBalance]); useEffect(() => { // 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 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, }); } } async function submit() { setFormData({ ...formData, dirty: false, }); 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); - setLoading(true); + passLoadingStatus(true); const { address, value } = formData; // Get the param-free address let cleanAddress = address.split('?')[0]; // Ensure address has bitcoincash: prefix and checksum cleanAddress = toLegacy(cleanAddress); let hasValidCashPrefix; try { hasValidCashPrefix = cleanAddress.startsWith( currency.legacyPrefix + ':', ); } catch (err) { hasValidCashPrefix = false; console.log(`toLegacy() returned an error:`, cleanAddress); } if (!hasValidCashPrefix) { // 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 // BCHA to an SLPA address - setLoading(false); + passLoadingStatus(false); setSendBchAddressError( `Destination is not a valid ${currency.ticker} address`, ); return; } // Calculate the amount in BCH let bchValue = value; if (selectedCurrency !== 'XEC') { bchValue = fiatToCrypto(value, fiatPrice); } try { const link = await sendBch( BCH, wallet, slpBalancesAndUtxos.nonSlpUtxos, filledAddress || cleanAddress, bchValue, currency.defaultFee, callbackTxId, ); notification.success({ message: 'Success', description: ( Transaction successful. Click or tap here for more details ), duration: 5, }); } catch (e) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop - setLoading(false); + 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); } notification.error({ message: 'Error', description: message, duration: 5, }); console.error(e); } } const handleAddressChange = e => { const { value, name } = e.target; 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, 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 (isValidTokenPrefix(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 => { setSelectedCurrency(e); // Clear input field to prevent accidentally sending 1 BCH instead of 1 USD setFormData(p => ({ ...p, value: '', })); }; const handleBchAmountChange = e => { const { value, name } = e.target; let bchValue = value; const error = shouldRejectAmountInput( bchValue, selectedCurrency, fiatPrice, balances.totalBalance, ); setSendBchAmountError(error); setFormData(p => ({ ...p, [name]: value, })); }; const onMax = async () => { // Clear amt error setSendBchAmountError(false); // Set currency to BCH setSelectedCurrency(currency.ticker); try { const txFeeSats = calcFee(BCH, slpBalancesAndUtxos.nonSlpUtxos); const txFeeBch = txFeeSats / 10 ** currency.cashDecimals; let value = balances.totalBalance - txFeeBch >= 0 ? (balances.totalBalance - txFeeBch).toFixed( currency.cashDecimals, ) : 0; 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', ); } }; // Display price in USD below input field for send amount, if it can be calculated let fiatPriceString = ''; if (fiatPrice !== null && !isNaN(formData.value)) { if (selectedCurrency === currency.ticker) { fiatPriceString = `${ cashtabSettings ? `${ currency.fiatCurrencies[cashtabSettings.fiatCurrency] .symbol } ` : '$ ' } ${(fiatPrice * Number(formData.value)).toFixed(2)} ${ cashtabSettings && cashtabSettings.fiatCurrency ? cashtabSettings.fiatCurrency.toUpperCase() : 'USD' }`; } else { fiatPriceString = `${ formData.value ? fiatToCrypto(formData.value, fiatPrice) : '0' } ${currency.ticker}`; } } const priceApiError = fiatPrice === null && selectedCurrency !== 'XEC'; return ( <>

Are you sure you want to send {formData.value}{' '} {currency.ticker} to {formData.address}?

{!balances.totalBalance ? ( You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : ( <> {fiatPrice !== null && ( )} )} - -
+ + handleAddressChange({ + target: { + name: 'address', + value: result, + }, + }) + } + inputProps={{ + disabled: Boolean(filledAddress), + placeholder: `${currency.ticker} Address`, + name: 'address', + onChange: e => handleAddressChange(e), + required: true, + value: filledAddress || formData.address, + }} + > + handleBchAmountChange(e), + required: true, + value: formData.value, + }} + selectProps={{ + value: selectedCurrency, + disabled: queryStringText !== null, + onChange: e => handleSelectedCurrencyChange(e), + }} + > + {priceApiError && ( + + Error fetching fiat price. Setting send by{' '} + {currency.fiatCurrencies[ + cashtabSettings.fiatCurrency + ].slug.toUpperCase()}{' '} + disabled + + )} + + {fiatPriceString !== '' && '='} {fiatPriceString} + +
- - handleAddressChange({ - target: { - name: 'address', - value: result, - }, - }) - } - inputProps={{ - disabled: Boolean(filledAddress), - placeholder: `${currency.ticker} Address`, - name: 'address', - onChange: e => handleAddressChange(e), - required: true, - value: filledAddress || formData.address, - }} - > - handleBchAmountChange(e), - required: true, - value: formData.value, - }} - selectProps={{ - value: selectedCurrency, - disabled: queryStringText !== null, - onChange: e => - handleSelectedCurrencyChange(e), - }} - > - {priceApiError && ( - - Error fetching fiat price. Setting send by{' '} - {currency.fiatCurrencies[ - cashtabSettings.fiatCurrency - ].slug.toUpperCase()}{' '} - disabled - - )} - - {fiatPriceString !== '' && '='}{' '} - {fiatPriceString} - -
- {!balances.totalBalance || - apiError || - sendBchAmountError || - sendBchAddressError ? ( - Send - ) : ( - <> - {txInfoFromUrl ? ( - showModal()} - > - Send - - ) : ( - submit()} - > - Send - - )} - - )} -
- {queryStringText && ( - - )} - {apiError && ( + {!balances.totalBalance || + apiError || + sendBchAmountError || + sendBchAddressError ? ( + Send + ) : ( <> - -

- - An error occured on our end. - Reconnecting... - -

+ {txInfoFromUrl ? ( + showModal()} + > + Send + + ) : ( + submit()}> + Send + + )} )} - - +
+ {queryStringText && ( + + )} + {apiError && ( + <> + +

+ + An error occured on our end. + Reconnecting... + +

+ + )} +
); }; +/* +passLoadingStatus must receive a default prop that is a function +in order to pass the rendering unit test in Send.test.js + +status => {console.log(status)} is an arbitrary stub function +*/ + +SendBCH.defaultProps = { + passLoadingStatus: status => { + console.log(status); + }, +}; + export default SendBCH; diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js index 88b68c791..37459299a 100644 --- a/web/cashtab/src/components/Send/SendToken.js +++ b/web/cashtab/src/components/Send/SendToken.js @@ -1,467 +1,464 @@ import React, { useState, useEffect } from 'react'; import { WalletContext } from '@utils/context'; import { Form, notification, message, - Spin, Row, Col, Alert, Descriptions, } from 'antd'; import Paragraph from 'antd/lib/typography/Paragraph'; import PrimaryButton, { SecondaryButton, } from '@components/Common/PrimaryButton'; -import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; +import { CashLoader } from '@components/Common/CustomIcons'; import { FormItemWithMaxAddon, FormItemWithQRCodeAddon, } from '@components/Common/EnhancedInputs'; import useBCH from '@hooks/useBCH'; import { BalanceHeader } from '@components/Common/BalanceHeader'; 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, parseAddress, isValidTokenPrefix, } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { isValidStoredWallet, convertEtokenToSimpleledger, } from '@utils/cashMethods'; -const SendToken = ({ tokenId, jestBCH }) => { +const SendToken = ({ tokenId, jestBCH, passLoadingStatus }) => { const { wallet, tokens, slpBalancesAndUtxos, apiError } = React.useContext( WalletContext, ); // If this wallet has migrated to latest storage structure, get token info from there // If not, use the tokens object (unless it's undefined, in which case use an empty array) const liveTokenState = isValidStoredWallet(wallet) && wallet.state.tokens ? wallet.state.tokens : tokens ? tokens : []; const token = liveTokenState.find(token => token.tokenId === tokenId); const [tokenStats, setTokenStats] = useState(null); const [queryStringText, setQueryStringText] = useState(null); const [sendTokenAddressError, setSendTokenAddressError] = useState(false); 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, getTokenStats } = useBCH(); // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // Fetch token stats if you do not have them and API did not return an error if (tokenStats === null) { getTokenStats(BCH, tokenId).then( result => { setTokenStats(result); }, err => { console.log(`Error getting token stats: ${err}`); }, ); } async function submit() { setFormData({ ...formData, dirty: false, }); if ( !formData.address || !formData.value || Number(formData.value <= 0) || sendTokenAmountError ) { return; } // Event("Category", "Action", "Label") // Track number of SLPA send transactions and // SLPA token IDs Event('SendToken.js', 'Send', tokenId); - setLoading(true); + passLoadingStatus(true); const { address, value } = formData; // Clear params from address let cleanAddress = address.split('?')[0]; // Convert to simpleledger prefix if etoken cleanAddress = convertEtokenToSimpleledger(cleanAddress); try { const link = await sendToken(BCH, wallet, slpBalancesAndUtxos, { tokenId: tokenId, tokenReceiverAddress: cleanAddress, amount: value, }); notification.success({ message: 'Success', description: ( Transaction successful. Click or tap here for more details ), duration: 5, }); } catch (e) { - setLoading(false); + 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 { 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 handleTokenAddressChange = e => { const { value, name } = e.target; // validate for token address // validate for parameters // show warning that query strings are not supported 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 (!isValidTokenPrefix(address)) { error = `Cashtab only supports sending to ${currency.tokenPrefixes[0]} prefixed addresses`; } setSendTokenAddressError(error); 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); + passLoadingStatus(false); }, [token]); return ( <> {!token && } {token && ( <> - -
+ handleTokenAddressChange({ + target: { + name: 'address', + value: result, + }, + }) + } + inputProps={{ + placeholder: `${currency.tokenTicker} Address`, + name: 'address', + onChange: e => + handleTokenAddressChange(e), + required: true, + value: formData.address, + }} + /> + + } + /> + ) : ( + {`identicon + ), + suffix: token.info.tokenTicker, + onChange: e => handleSlpAmountChange(e), + required: true, + value: formData.value, + }} + /> +
- - handleTokenAddressChange({ - target: { - name: 'address', - value: result, - }, - }) - } - inputProps={{ - placeholder: `${currency.tokenTicker} Address`, - name: 'address', - onChange: e => - handleTokenAddressChange(e), - required: true, - value: formData.address, - }} - /> - - } - /> - ) : ( - {`identicon - ), - suffix: token.info.tokenTicker, - onChange: e => - handleSlpAmountChange(e), - required: true, - value: formData.value, - }} + {apiError || + sendTokenAmountError || + sendTokenAddressError ? ( + <> + + Send {token.info.tokenName} + + {apiError && } + + ) : ( + submit()}> + Send {token.info.tokenName} + + )} +
+ + {queryStringText && ( + -
- {apiError || - sendTokenAmountError || - sendTokenAddressError ? ( - <> - - Send {token.info.tokenName} - - {apiError && } - - ) : ( - submit()} - > - Send {token.info.tokenName} - - )} -
- - {queryStringText && ( - - )} - {apiError && ( -

- - An error occured on our end. - Reconnecting... - -

- )} - - {tokenStats !== null && ( - - - {token.info.decimals} - - - {token.tokenId} - - {tokenStats && ( - <> - - {tokenStats.documentUri} - - - {new Date( - tokenStats.timestampUnix * - 1000, - ).toLocaleDateString()} - - - {tokenStats.containsBaton - ? 'No' - : 'Yes'} - - - {tokenStats.initialTokenQty.toLocaleString()} - - - {tokenStats.totalBurned.toLocaleString()} - - - {tokenStats.totalMinted.toLocaleString()} - - - {tokenStats.circulatingSupply.toLocaleString()} - - - )} - + + An error occured on our end. + Reconnecting... + +

)} -
+ + {tokenStats !== null && ( + + + {token.info.decimals} + + + {token.tokenId} + + {tokenStats && ( + <> + + {tokenStats.documentUri} + + + {new Date( + tokenStats.timestampUnix * + 1000, + ).toLocaleDateString()} + + + {tokenStats.containsBaton + ? 'No' + : 'Yes'} + + + {tokenStats.initialTokenQty.toLocaleString()} + + + {tokenStats.totalBurned.toLocaleString()} + + + {tokenStats.totalMinted.toLocaleString()} + + + {tokenStats.circulatingSupply.toLocaleString()} + + + )} + + )}
)} ); }; +/* +passLoadingStatus must receive a default prop that is a function +in order to pass the rendering unit test in SendToken.test.js + +status => {console.log(status)} is an arbitrary stub function +*/ + +SendToken.defaultProps = { + passLoadingStatus: status => { + console.log(status); + }, +}; + export default SendToken; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap index 34694ae11..8694f7742 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/Send.test.js.snap @@ -1,2127 +1,2079 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
- - - - -
-
- - - - - XEC - -
- - - - - -
+ value="" + /> + + XEC + +
+ + - max +
-
-
-
- -
-
+ max + +
-
-
- = - - $ NaN USD -
-
- +
+ +
+
- +
+
+
+ = + + $ NaN USD +
+
+
-
+
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
- - - - -
-
- - - - - XEC - -
- - - - - -
+ value="" + /> + + XEC + +
+ + - max +
-
-
-
- -
-
+ max + +
-
-
- = - - $ NaN USD -
-
- +
+ +
+
- +
+
+
+ = + + $ NaN USD +
+
+
-
+
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
- - - - -
-
- - - - - XEC - -
- - - - - -
+ value="" + /> + + XEC + +
+ + - max +
-
-
-
- -
-
+ max + +
-
-
- = - - $ NaN USD -
-
- +
+ +
+
- +
+
+
+ = + + $ NaN USD +
+
+
-
+
, ] `; exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
- - - - -
-
- - - - - XEC - -
- - - - - -
+ value="" + /> + + XEC + +
+ + - max +
-
-
-
- -
-
+ max + +
-
-
- = - - $ NaN USD -
-
- +
+ +
+
- +
+
+
+ = + + $ NaN USD +
+
+
-
+
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
- - - - -
-
- - - - - XEC - -
- - - - - -
+ value="" + /> + + XEC + +
+ + - max +
-
-
-
- -
-
+ max + +
-
-
- = - - $ NaN USD -
-
- +
+ +
+
- +
+
+
+ = + + $ NaN USD
-
+
+ +
+
, ] `; exports[`Without wallet defined 1`] = ` Array [
You currently have 0 XEC
Deposit some funds to use this feature
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
+ + + + +
+
- - - - -
-
- - - - - XEC - -
- - - - - -
+ value="" + /> + + XEC + +
+ + - max +
-
-
-
- -
-
+ max + +
-
-
- = - - $ NaN USD -
-
- +
+ +
+
- +
+
+
+ = + + $ NaN USD +
+
+
-
+
, ] `; diff --git a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap index 1c183c588..cad5c3345 100644 --- a/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap +++ b/web/cashtab/src/components/Send/__tests__/__snapshots__/SendToken.test.js.snap @@ -1,774 +1,750 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances and tokens 1`] = ` Array [
6.001 TBS
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
- - - identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba - - - - TBS - - - - - max - - + identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba + + + + TBS -
-
-
-
- -
-
+ + + max + + + +
-
-
- +
+ +
+
- +
-
+
+ +
+ , ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
6.001 TBS
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
- - - identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba - - - - TBS - - - - - max - - + identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba + + + + TBS -
-
-
-
- -
-
+ + + max + + + +
-
-
- +
+ +
+
- +
+
+
+
-
+ , ] `; exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` Array [
6.001 TBS
,
-
-
-
-
- - + + + + + + + - + + -
-
-
-
- -
-
+ + +
+
+
+
+
+
+
+
+
-
-
- - - identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba - - - - TBS - - - - - max - - + identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba + + + + TBS -
-
-
-
- -
-
+ + + max + + + +
-
-
- +
+ +
+
- +
+
+
+
-
+ , ] `; exports[`Without wallet defined 1`] = `null`;