diff --git a/web/cashtab/src/components/Common/Atoms.js b/web/cashtab/src/components/Common/Atoms.js index 3de4810ea..77fe28186 100644 --- a/web/cashtab/src/components/Common/Atoms.js +++ b/web/cashtab/src/components/Common/Atoms.js @@ -1,63 +1,63 @@ import styled from 'styled-components'; export const LoadingCtn = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; height: 400px; flex-direction: column; svg { width: 50px; height: 50px; fill: ${props => props.theme.primary}; } `; -export const BalanceHeader = styled.div` +export const BalanceHeaderWrap = styled.div` color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 30px; font-weight: bold; @media (max-width: 768px) { font-size: 23px; } `; export const BalanceHeaderFiat = styled.div` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 18px; margin-bottom: 20px; font-weight: bold; @media (max-width: 768px) { font-size: 16px; } `; export const ZeroBalanceHeader = styled.div` color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 14px; margin-bottom: 5px; `; export const TokenParamLabel = styled.span` font-weight: bold; `; export const AlertMsg = styled.p` color: ${props => props.theme.forms.error}; `; export const ConvertAmount = styled.div` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 14px; margin-bottom: 10px; font-weight: bold; @media (max-width: 768px) { font-size: 12px; } `; diff --git a/web/cashtab/src/components/Common/BalanceHeader.js b/web/cashtab/src/components/Common/BalanceHeader.js new file mode 100644 index 000000000..a59db76db --- /dev/null +++ b/web/cashtab/src/components/Common/BalanceHeader.js @@ -0,0 +1,11 @@ +import * as React from 'react'; +import { formatBalance } from '@utils/cashMethods'; +import { BalanceHeaderWrap } from '@components/Common/Atoms'; + +export const BalanceHeader = ({ balance, ticker }) => { + return ( + + {formatBalance(balance)} {ticker} + + ); +}; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index 784e8ce72..f8497a54b 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,575 +1,575 @@ 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 { 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 { formatBalance } from '@utils/cashMethods'; +import { BalanceHeader } from '@components/Common/BalanceHeader'; import { - BalanceHeader, BalanceHeaderFiat, 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 }) => { // 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); }, [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); 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); 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); 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
) : ( <> - - {formatBalance(balances.totalBalance)} {currency.ticker} - + {fiatPrice !== null && ( {cashtabSettings ? `${ currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].symbol } ` : '$ '} {(balances.totalBalance * fiatPrice).toFixed(2)}{' '} {cashtabSettings ? `${currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].slug.toUpperCase()} ` : 'USD'} )} )}
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 && ( <>

An error occured on our end. Reconnecting...

)}
); }; export default SendBCH; diff --git a/web/cashtab/src/components/Send/SendToken.js b/web/cashtab/src/components/Send/SendToken.js index 44697ad77..1c8d7c019 100644 --- a/web/cashtab/src/components/Send/SendToken.js +++ b/web/cashtab/src/components/Send/SendToken.js @@ -1,467 +1,467 @@ 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 { FormItemWithMaxAddon, FormItemWithQRCodeAddon, } from '@components/Common/EnhancedInputs'; import useBCH from '@hooks/useBCH'; -import { BalanceHeader } from '@components/Common/Atoms'; +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 { - formatBalance, isValidStoredWallet, convertEtokenToSimpleledger, } from '@utils/cashMethods'; const SendToken = ({ tokenId, jestBCH }) => { 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); 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); 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); }, [token]); return ( <> {!token && } {token && ( <> - - {formatBalance(token.balance)} {token.info.tokenTicker} - +
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 && (

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()} )} )}
)} ); }; export default SendToken; diff --git a/web/cashtab/src/components/Tokens/Tokens.js b/web/cashtab/src/components/Tokens/Tokens.js index 309c02eca..9bf437e7c 100644 --- a/web/cashtab/src/components/Tokens/Tokens.js +++ b/web/cashtab/src/components/Tokens/Tokens.js @@ -1,171 +1,173 @@ import React from 'react'; import { LoadingOutlined } from '@ant-design/icons'; import { CashLoader } from '@components/Common/CustomIcons'; import { WalletContext } from '@utils/context'; import { - formatBalance, isValidStoredWallet, fromSmallestDenomination, } from '@utils/cashMethods'; import CreateTokenForm from '@components/Tokens/CreateTokenForm'; import { currency } from '@components/Common/Ticker.js'; import TokenList from '@components/Wallet/TokenList'; import useBCH from '@hooks/useBCH'; +import { BalanceHeader } from '@components/Common/BalanceHeader'; import { LoadingCtn, - BalanceHeader, BalanceHeaderFiat, ZeroBalanceHeader, AlertMsg, } from '@components/Common/Atoms'; const Tokens = ({ jestBCH }) => { /* Dev note This is the first new page created after the wallet migration to include state in storage As such, it will only load this type of wallet If any user is still migrating at this point, this page will display a loading spinner until their wallet has updated (ETA within 10 seconds) Going forward, this approach will be the model for Wallet, Send, and SendToken, as the legacy wallet state parameters not stored in the wallet object are deprecated */ const { loading, wallet, apiError, fiatPrice, cashtabSettings, } = React.useContext(WalletContext); // If wallet is unmigrated, do not show page until it has migrated // An invalid wallet will be validated/populated after the next API call, ETA 10s let validWallet = isValidStoredWallet(wallet); // Get wallet state variables let balances, tokens; if (validWallet) { balances = wallet.state.balances; tokens = wallet.state.tokens; } const { getBCH, getRestUrl, createToken } = useBCH(); // Support using locally installed bchjs for unit tests const BCH = jestBCH ? jestBCH : getBCH(); return ( <> {loading || !validWallet ? ( ) : ( <> {!balances.totalBalance ? ( <> You need some {currency.ticker} in your wallet to create tokens. - 0 {currency.ticker} + ) : ( <> - - {formatBalance(balances.totalBalance)}{' '} - {currency.ticker} - + {fiatPrice !== null && !isNaN(balances.totalBalance) && ( {cashtabSettings ? `${ currency.fiatCurrencies[ cashtabSettings .fiatCurrency ].symbol } ` : '$ '} {( balances.totalBalance * fiatPrice ).toFixed(2)}{' '} {cashtabSettings ? `${currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].slug.toUpperCase()} ` : 'USD'} )} )} {apiError && ( <>

An error occurred on our end.

Re-establishing connection...

)} {balances.totalBalanceInSatoshis < currency.dustSats && ( You need at least{' '} {fromSmallestDenomination( currency.dustSats, ).toString()}{' '} {currency.ticker} ( {cashtabSettings ? `${ currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].symbol } ` : '$ '} {( fromSmallestDenomination( currency.dustSats, ).toString() * fiatPrice ).toFixed(4)}{' '} {cashtabSettings ? `${currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].slug.toUpperCase()} ` : 'USD'} ) to create a token )} {tokens && tokens.length > 0 ? ( <> ) : ( <>No {currency.tokenTicker} tokens in this wallet )} )} ); }; export default Tokens; diff --git a/web/cashtab/src/components/Wallet/Wallet.js b/web/cashtab/src/components/Wallet/Wallet.js index f660d1eb9..049e865c2 100644 --- a/web/cashtab/src/components/Wallet/Wallet.js +++ b/web/cashtab/src/components/Wallet/Wallet.js @@ -1,414 +1,417 @@ import React from 'react'; import styled from 'styled-components'; import { Switch } from 'antd'; import { LinkOutlined, LoadingOutlined } from '@ant-design/icons'; import { WalletContext } from '@utils/context'; import { OnBoarding } from '@components/OnBoarding/OnBoarding'; import { QRCode } from '@components/Common/QRCode'; import { currency } from '@components/Common/Ticker.js'; import { Link } from 'react-router-dom'; import TokenList from './TokenList'; import TxHistory from './TxHistory'; import { CashLoader } from '@components/Common/CustomIcons'; -import { formatBalance } from '@utils/cashMethods'; +import { BalanceHeader } from '@components/Common/BalanceHeader'; import { LoadingCtn, - BalanceHeader, BalanceHeaderFiat, ZeroBalanceHeader, } from '@components/Common/Atoms'; export const Tabs = styled.div` margin: auto; margin-bottom: 12px; display: inline-block; text-align: center; `; export const TabLabel = styled.button` :focus, :active { outline: none; } border: none; background: none; font-size: 20px; cursor: pointer; @media (max-width: 400px) { font-size: 16px; } ${({ active, ...props }) => active && ` color: ${props.theme.primary}; `} `; export const TabLine = styled.div` margin: auto; transition: margin-left 0.5s ease-in-out, width 0.5s 0.1s; height: 4px; border-radius: 5px; background-color: ${props => props.theme.primary}; pointer-events: none; margin-left: 72%; width: 28%; ${({ left, ...props }) => left && ` margin-left: 1% width: 69%; `} `; export const TabPane = styled.div` ${({ active }) => !active && ` display: none; `} `; export const SwitchBtnCtn = styled.div` display: flex; align-items: center; justify-content: center; align-content: space-between; margin-bottom: 15px; .nonactiveBtn { color: ${props => props.theme.wallet.text.secondary}; background: ${props => props.theme.wallet.switch.inactive.background} !important; box-shadow: none !important; } .slpActive { background: ${props => props.theme.wallet.switch.activeToken.background} !important; box-shadow: ${props => props.theme.wallet.switch.activeToken.shadow} !important; } `; export const SwitchBtn = styled.div` font-weight: bold; display: inline-block; cursor: pointer; color: ${props => props.theme.contrast}; font-size: 14px; padding: 6px 0; width: 100px; margin: 0 1px; text-decoration: none; background: ${props => props.theme.primary}; box-shadow: ${props => props.theme.wallet.switch.activeCash.shadow}; user-select: none; :first-child { border-radius: 100px 0 0 100px; } :nth-child(2) { border-radius: 0 100px 100px 0; } `; export const Links = styled(Link)` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin: 10px 0 20px 0; border: 1px solid ${props => props.theme.wallet.text.secondary}; padding: 14px 0; display: inline-block; border-radius: 3px; transition: all 200ms ease-in-out; svg { fill: ${props => props.theme.wallet.text.secondary}; } :hover { color: ${props => props.theme.primary}; border-color: ${props => props.theme.primary}; svg { fill: ${props => props.theme.primary}; } } @media (max-width: 768px) { padding: 10px 0; font-size: 14px; } `; export const ExternalLink = styled.a` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin: 0 0 20px 0; border: 1px solid ${props => props.theme.wallet.text.secondary}; padding: 14px 0; display: inline-block; border-radius: 3px; transition: all 200ms ease-in-out; svg { fill: ${props => props.theme.wallet.text.secondary}; transition: all 200ms ease-in-out; } :hover { color: ${props => props.theme.primary}; border-color: ${props => props.theme.primary}; svg { fill: ${props => props.theme.primary}; } } @media (max-width: 768px) { padding: 10px 0; font-size: 14px; } `; export const AddrSwitchContainer = styled.div` text-align: center; padding: 6px 0 12px 0; `; export const AddrPrefixSwitch = styled(Switch)``; export const AddrPrefixLabel = styled.span` color: ${props => props.theme.wallet.text.primary} margin-right: 4px; `; const WalletInfo = () => { const ContextValue = React.useContext(WalletContext); const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; let balances; let parsedTxHistory; let tokens; // use parameters from wallet.state object and not legacy separate parameters, if they are in state // handle 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 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') && paramsInWalletState.includes('parsedTxHistory') && paramsInWalletState.includes('tokens') ) { balances = wallet.state.balances; parsedTxHistory = wallet.state.parsedTxHistory; tokens = wallet.state.tokens; } 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; parsedTxHistory = ContextValue.parsedTxHistory; tokens = ContextValue.tokens; } const [address, setAddress] = React.useState('cashAddress'); const [addressPrefix, setAddressPrefix] = React.useState('eCash'); const [activeTab, setActiveTab] = React.useState('txHistory'); const hasHistory = parsedTxHistory && parsedTxHistory.length > 0; const handleChangeAddress = () => { setAddress(address === 'cashAddress' ? 'slpAddress' : 'cashAddress'); }; const onAddressPrefixChange = () => { setAddressPrefix(addressPrefix === 'eCash' ? 'bitcoincash' : 'eCash'); }; return ( <> {!balances.totalBalance && !apiError && !hasHistory ? ( <> 🎉 Congratulations on your new wallet!{' '} 🎉
Start using the wallet immediately to receive{' '} {currency.ticker} payments, or load it up with{' '} {currency.ticker} to send to others
- 0 {currency.ticker} + ) : ( <> - - {formatBalance(balances.totalBalance)} {currency.ticker} - + {fiatPrice !== null && !isNaN(balances.totalBalance) && ( {cashtabSettings ? `${ currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].symbol } ` : '$ '} {(balances.totalBalance * fiatPrice).toFixed(2)}{' '} {cashtabSettings ? `${currency.fiatCurrencies[ cashtabSettings.fiatCurrency ].slug.toUpperCase()} ` : 'USD'} )} )} {apiError && ( <>

An error occurred on our end.

Re-establishing connection...

)} {wallet && ((wallet.Path245 && wallet.Path145) || wallet.Path1899) && ( <> {wallet.Path1899 ? ( <> Address Format: ) : ( <> )} )} handleChangeAddress()} className={ address !== 'cashAddress' ? 'nonactiveBtn' : null } > {currency.ticker} handleChangeAddress()} className={ address === 'cashAddress' ? 'nonactiveBtn' : 'slpActive' } > {currency.tokenTicker} {hasHistory && parsedTxHistory && ( <> setActiveTab('txHistory')} > Transaction History setActiveTab('tokens')} > Tokens {tokens && tokens.length > 0 ? ( ) : (

Tokens sent to your {currency.tokenTicker}{' '} address will appear here

)}
)} ); }; const Wallet = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading } = ContextValue; return ( <> {loading ? ( ) : ( <>{wallet.Path1899 ? : } )} ); }; export default Wallet; diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap index cab22a3f8..269c561f3 100644 --- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap +++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap @@ -1,794 +1,795 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP @generated exports[`Wallet with BCH balances 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
Address Format:
,
XEC
eToken
, ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
Address Format:
,
XEC
eToken
, ] `; exports[`Wallet with BCH balances and tokens and state field 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
Address Format:
,
XEC
eToken
, ] `; exports[`Wallet with BCH balances and tokens and state field, but no params in state 1`] = ` Array [
0.06047469 XEC
,
$ NaN USD
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
Address Format:
,
XEC
eToken
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive XEC payments, or load it up with XEC to send to others
,
- 0 + 0 + XEC
,
Copied
ecash:qzagy47mvh6qxkvcn3acjnz73rkhkc6y7ccxkrr6zd
qzagy47m vh6qxkvcn3acjnz73rkhkc6y7c cxkrr6zd
,
Address Format:
,
XEC
eToken
, ] `; exports[`Without wallet defined 1`] = ` Array [

Welcome to Cashtab!

,

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

Want to learn more? Check out the Cashtab documentation.

, , , ] `;