diff --git a/web/cashtab/src/components/Common/BalanceHeaderFiat.js b/web/cashtab/src/components/Common/BalanceHeaderFiat.js index cdfc5b220..a48cd96fe 100644 --- a/web/cashtab/src/components/Common/BalanceHeaderFiat.js +++ b/web/cashtab/src/components/Common/BalanceHeaderFiat.js @@ -1,42 +1,49 @@ import * as React from 'react'; import styled from 'styled-components'; import PropTypes from 'prop-types'; import { BalanceHeaderFiatWrap } from '@components/Common/Atoms'; import { currency } from '@components/Common/Ticker.js'; const FiatCurrencyToXEC = styled.p` margin: 0 auto; padding: 0; overflow: hidden; text-overflow: ellipsis; color: ${props => props.theme.lightWhite}; `; const BalanceHeaderFiat = ({ balance, settings, fiatPrice }) => { return ( - - {settings - ? `${currency.fiatCurrencies[settings.fiatCurrency].symbol}` - : '$'} - {parseFloat((balance * fiatPrice).toFixed(2)).toLocaleString()}{' '} - {settings - ? `${currency.fiatCurrencies[ - settings.fiatCurrency - ].slug.toUpperCase()} ` - : 'USD'} + <> {fiatPrice && ( - - 1 {currency.ticker} ={' '} - {fiatPrice.toFixed(9).toLocaleString()}{' '} - {settings.fiatCurrency.toUpperCase()} - + + {settings + ? `${ + currency.fiatCurrencies[settings.fiatCurrency] + .symbol + }` + : '$'} + {parseFloat( + (balance * fiatPrice).toFixed(2), + ).toLocaleString()}{' '} + {settings + ? `${currency.fiatCurrencies[ + settings.fiatCurrency + ].slug.toUpperCase()} ` + : 'USD'} + + 1 {currency.ticker} ={' '} + {fiatPrice.toFixed(9).toLocaleString()}{' '} + {settings.fiatCurrency.toUpperCase()} + + )} - + ); }; BalanceHeaderFiat.propTypes = { balance: PropTypes.number, settings: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), fiatPrice: PropTypes.number, }; export default BalanceHeaderFiat; diff --git a/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap b/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap index 134f03375..2cbda2fe3 100644 --- a/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap +++ b/web/cashtab/src/components/Home/__tests__/__snapshots__/Home.test.js.snap @@ -1,481 +1,449 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [

MigrationTestAlpha

0 XEC
-
- $ - NaN - - USD -
,
πŸŽ‰ Congratulations on your new wallet! πŸŽ‰
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [

MigrationTestAlpha

0 XEC
-
- $ - NaN - - USD -
,
πŸŽ‰ Congratulations on your new wallet! πŸŽ‰
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [

MigrationTestAlpha

0.06 XEC
-
- $ - NaN - - USD -
,
πŸŽ‰ Congratulations on your new wallet! πŸŽ‰
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [

MigrationTestAlpha

0 XEC
-
- $ - NaN - - USD -
,
πŸŽ‰ Congratulations on your new wallet! πŸŽ‰
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
Create eToken

Tokens sent to your eToken address will appear here

, ] `; exports[`Without wallet defined 1`] = `

Welcome to Cashtab!

Cashtab is an open source, non-custodial web wallet for eCash .

Want to learn more? Check out the Cashtab documentation.

`; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index 8599a37fa..bf70bbb3e 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,1063 +1,1062 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import { WalletContext } from '@utils/context'; import { AntdFormWrapper, SendBchInput, DestinationAddressSingle, DestinationAddressMulti, } from '@components/Common/EnhancedInputs'; import { AdvancedCollapse } from '@components/Common/StyledCollapse'; import { Form, message, Modal, Alert, Collapse, Input, Button } from 'antd'; const { Panel } = Collapse; const { TextArea } = Input; import { Row, Col, Switch } from 'antd'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import useBCH from '@hooks/useBCH'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { sendXecNotification, errorNotification, messageSignedNotification, } from '@components/Common/Notifications'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { currency, parseAddressForParams } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput, isValidXecAddress, isValidEtokenAddress, isValidXecSendAmount, } from '@utils/validation'; import BalanceHeader from '@components/Common/BalanceHeader'; import BalanceHeaderFiat from '@components/Common/BalanceHeaderFiat'; import { ZeroBalanceHeader, ConvertAmount, AlertMsg, WalletInfoCtn, SidePaddingCtn, FormLabel, } from '@components/Common/Atoms'; import { getWalletState, convertToEcashPrefix, toLegacyCash, toLegacyCashArray, fromSmallestDenomination, } from '@utils/cashMethods'; import ApiError from '@components/Common/ApiError'; import { formatFiatBalance, formatBalance } from '@utils/formatting'; import { TokenParamLabel } from '@components/Common/Atoms'; import { PlusSquareOutlined } from '@ant-design/icons'; import styled from 'styled-components'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import WalletLabel from '@components/Common/WalletLabel.js'; const SignMessageLabel = styled.div` text-align: left; color: ${props => props.theme.forms.text}; `; const TextAreaLabel = styled.div` text-align: left; color: ${props => props.theme.forms.text}; padding-left: 1px; `; const AmountPreviewCtn = styled.div` margin-top: -30px; `; const SendInputCtn = styled.div` .ant-form-item-with-help { margin-bottom: 32px; } `; const LocaleFormattedValue = styled.h3` color: ${props => props.theme.contrast}; font-weight: bold; margin-bottom: 0; `; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const SendBCH = ({ jestBCH, passLoadingStatus }) => { // 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 location = useLocation(); const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; const walletState = getWalletState(wallet); const { balances, slpBalancesAndUtxos } = walletState; // Modal settings const [showConfirmMsgToSign, setShowConfirmMsgToSign] = useState(false); const [msgToSign, setMsgToSign] = useState(''); const [signMessageIsValid, setSignMessageIsValid] = useState(null); const [isOneToManyXECSend, setIsOneToManyXECSend] = useState(false); const [opReturnMsg, setOpReturnMsg] = useState(false); const [isEncryptedOptionalOpReturnMsg, setIsEncryptedOptionalOpReturnMsg] = useState(false); const [bchObj, setBchObj] = 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({ value: '', address: '', }); 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 [messageSignature, setMessageSignature] = useState(''); const [sigCopySuccess, setSigCopySuccess] = useState(''); const userLocale = navigator.language; const clearInputForms = () => { setFormData({ value: '', address: '', }); setOpReturnMsg(''); // OP_RETURN message has its own state field }; const showModal = () => { setIsModalVisible(true); }; const handleOk = () => { setIsModalVisible(false); send(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getBCH, getRestUrl, sendXec, calcFee, signPkMessage } = useBCH(); // 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(() => { passLoadingStatus(false); }, [balances.totalBalance]); useEffect(() => { // jestBCH is only ever specified for unit tests, otherwise app will use getBCH(); const BCH = jestBCH ? jestBCH : getBCH(); // set the BCH instance to state, for other functions to reference setBchObj(BCH); // Manually parse for txInfo object on page load when Send.js is loaded with a query string // if this was routed from Wallet screen's Reply to message link then prepopulate the address and value field if (location && location.state && location.state.replyAddress) { setFormData({ address: location.state.replyAddress, value: `${fromSmallestDenomination(currency.dustSats)}`, }); } // 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, }); } } 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({ ...formData, }); 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 } = 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 = toLegacyCashArray(addressAndValueArray); const link = await sendXec( bchObj, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, opReturnMsg, true, // indicate send mode is one to many cleanAddressAndValueArray, ); sendXecNotification(link); clearInputForms(); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } else { // standard one to one XEC send transaction 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); passLoadingStatus(true); const { address, value } = formData; // Get the param-free address let cleanAddress = address.split('?')[0]; // Ensure address has bitcoincash: prefix and checksum cleanAddress = toLegacyCash(cleanAddress); // Calculate the amount in BCH let bchValue = value; if (selectedCurrency !== 'XEC') { bchValue = fiatToCrypto(value, fiatPrice); } // encrypted message limit truncation let optionalOpReturnMsg; if (isEncryptedOptionalOpReturnMsg) { optionalOpReturnMsg = opReturnMsg.substring( 0, currency.opReturn.encryptedMsgCharLimit, ); } else { optionalOpReturnMsg = opReturnMsg; } try { const link = await sendXec( bchObj, wallet, slpBalancesAndUtxos.nonSlpUtxos, currency.defaultFee, optionalOpReturnMsg, false, // sendToMany boolean flag null, // address array not applicable for one to many tx cleanAddress, bchValue, isEncryptedOptionalOpReturnMsg, ); sendXecNotification(link); clearInputForms(); } catch (e) { handleSendXecError(e, isOneToManyXECSend); } } } const handleAddressChange = e => { const { value, name } = e.target; let error = false; let addressString = value; // validate address const isValid = isValidXecAddress(addressString); // parse address for parameters const addressInfo = parseAddressForParams(addressString); /* Model addressInfo = { address: '', queryString: '', amount: null, }; */ const { address, 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 (isValidEtokenAddress(address)) { error = `eToken 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 handleMultiAddressChange = e => { const { value, name } = e.target; let error; if (!value) { error = 'Input must not be blank'; setSendBchAddressError(error); return setFormData(p => ({ ...p, [name]: value, })); } //convert each line from the