diff --git a/web/cashtab-v2/extension/src/components/App.js b/web/cashtab-v2/extension/src/components/App.js index 18aaf7362..225f6dd21 100644 --- a/web/cashtab-v2/extension/src/components/App.js +++ b/web/cashtab-v2/extension/src/components/App.js @@ -1,358 +1,363 @@ import React, { useState } from 'react'; import 'antd/dist/antd.less'; +import PropTypes from 'prop-types'; import { Spin } from 'antd'; import { CashLoadingIcon, HomeIcon, SendIcon, ReceiveIcon, SettingsIcon, AirdropIcon, } from 'components/Common/CustomIcons'; import '../index.css'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { theme } from 'assets/styles/theme'; import Home from 'components/Home/Home'; import Receive from 'components/Receive/Receive'; import Tokens from 'components/Tokens/Tokens'; import Send from 'components/Send/Send'; import SendToken from 'components/Send/SendToken'; import Airdrop from 'components/Airdrop/Airdrop'; import Configure from 'components/Configure/Configure'; import NotFound from 'components/NotFound'; import CashTab from 'assets/cashtab_xec.png'; import './App.css'; import { WalletContext } from 'utils/context'; import { isValidStoredWallet } from 'utils/cashMethods'; import { Route, Redirect, Switch, useLocation, useHistory, } from 'react-router-dom'; // Extension-only import used for open in new tab link import PopOut from 'assets/popout.svg'; const GlobalStyle = createGlobalStyle` *::placeholder { color: ${props => props.theme.forms.placeholder} !important; } *::selection { background: ${props => props.theme.eCashBlue} !important; } .ant-modal-content, .ant-modal-header, .ant-modal-title { background-color: ${props => props.theme.modal.background} !important; color: ${props => props.theme.modal.color} !important; } .ant-modal-content svg { fill: ${props => props.theme.modal.color}; } .ant-modal-footer button { background-color: ${props => props.theme.modal.buttonBackground} !important; color: ${props => props.theme.modal.color} !important; border-color: ${props => props.theme.modal.border} !important; :hover { background-color: ${props => props.theme.eCashBlue} !important; } } .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, #cropControlsConfirm{ border-radius: 3px; border-radius: 3px; background-color: ${props => props.theme.modal.buttonBackground} !important; color: ${props => props.theme.modal.color} !important; border-color: ${props => props.theme.modal.border} !important; :hover { background-color: ${props => props.theme.eCashBlue} !important; } text-shadow: none !important; text-shadow: none !important; } .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, #cropControlsConfirm:hover { color: ${props => props.theme.contrast}; transition: all 0.3s; background-color: ${props => props.theme.eCashBlue}; border-color: ${props => props.theme.eCashBlue}; } .selectedCurrencyOption, .ant-select-dropdown { text-align: left; color: ${props => props.theme.contrast} !important; background-color: ${props => props.theme.collapses.expandedBackground} !important; } .cashLoadingIcon { color: ${props => props.theme.eCashBlue} !important; font-size: 48px !important; } .selectedCurrencyOption:hover { color: ${props => props.theme.contrast} !important; background-color: ${props => props.theme.eCashBlue} !important; } #addrSwitch, #cropSwitch { .ant-switch-checked { background-color: white !important; } } #addrSwitch.ant-switch-checked, #cropSwitch.ant-switch-checked { background-image: ${props => props.theme.buttons.primary.backgroundImage} !important; } .ant-slider-rail { background-color: ${props => props.theme.forms.border} !important; } .ant-slider-track { background-color: ${props => props.theme.eCashBlue} !important; } .ant-descriptions-bordered .ant-descriptions-row { background: ${props => props.theme.contrast}; } .ant-modal-confirm-content, .ant-modal-confirm-title { color: ${props => props.theme.contrast} !important; } `; const CustomApp = styled.div` text-align: center; font-family: 'Gilroy', sans-serif; font-family: 'Poppins', sans-serif; background-color: ${props => props.theme.backgroundColor}; background-size: 100px 171px; background-image: ${props => props.theme.backgroundImage}; background-attachment: fixed; `; const Footer = styled.div` z-index: 2; height: 80px; border-top: 1px solid rgba(255, 255, 255, 0.5); background-color: ${props => props.theme.footerBackground}; position: fixed; bottom: 0; width: 500px; display: flex; align-items: center; justify-content: space-between; padding: 0 50px; @media (max-width: 768px) { width: 100%; padding: 0 20px; } `; export const NavButton = styled.button` :focus, :active { outline: none; } cursor: pointer; padding: 0; background: none; border: none; font-size: 10px; svg { fill: ${props => props.theme.contrast}; width: 26px; height: auto; } ${({ active, ...props }) => active && ` color: ${props.theme.navActive}; svg { fill: ${props.theme.navActive}; } `} `; export const WalletBody = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; min-height: 100vh; `; export const WalletCtn = styled.div` position: relative; width: 500px; min-height: 100vh; padding: 0 0 100px; background: ${props => props.theme.walletBackground}; -webkit-box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow}; -moz-box-shadow: 0px 0px 24px 1px ${props => props.theme.shadow}; box-shadow: 0px 0px 24px 1px ${props => props.theme.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: space-between; width: 100%; padding: 15px; `; export const CashTabLogo = styled.img` width: 120px; @media (max-width: 768px) { width: 110px; } `; // Extension only styled components const OpenInTabBtn = styled.button` background: none; border: none; `; const ExtTabImg = styled.img` max-width: 20px; `; const App = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading } = ContextValue; const [loadingUtxosAfterSend, setLoadingUtxosAfterSend] = useState(false); // 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 const validWallet = isValidStoredWallet(wallet); const location = useLocation(); const history = useHistory(); const selectedKey = location && location.pathname ? location.pathname.substr(1) : ''; // openInTab is an extension-only method const openInTab = () => { window.open(`index.html#/${selectedKey}`); }; return ( {/*Begin extension-only components*/} openInTab()} > {/*End extension-only components*/} {/*Note that the extension does not support biometric security*/} {/*Hence is not pulled in*/} ( )} /> {wallet ? ( ) : null} ); }; +App.propTypes = { + match: PropTypes.string, +}; + export default App; diff --git a/web/cashtab-v2/src/components/Airdrop/Airdrop.js b/web/cashtab-v2/src/components/Airdrop/Airdrop.js index fb99599c7..58f1ec2a8 100644 --- a/web/cashtab-v2/src/components/Airdrop/Airdrop.js +++ b/web/cashtab-v2/src/components/Airdrop/Airdrop.js @@ -1,468 +1,475 @@ import React, { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import PropTypes from 'prop-types'; import BigNumber from 'bignumber.js'; import styled from 'styled-components'; import { WalletContext } from 'utils/context'; import { AntdFormWrapper } from 'components/Common/EnhancedInputs'; import { AdvancedCollapse } from 'components/Common/StyledCollapse'; import { Form, Alert, Collapse, Input, Modal, Spin, Progress } from 'antd'; const { Panel } = Collapse; const { TextArea } = Input; import { Row, Col } from 'antd'; import { SmartButton } from 'components/Common/PrimaryButton'; import useBCH from 'hooks/useBCH'; import { errorNotification, generalNotification, } from 'components/Common/Notifications'; import { currency } from 'components/Common/Ticker.js'; import BalanceHeader from 'components/Common/BalanceHeader'; import BalanceHeaderFiat from 'components/Common/BalanceHeaderFiat'; import { getWalletState, convertEtokenToEcashAddr, fromSmallestDenomination, } from 'utils/cashMethods'; import { isValidTokenId, isValidXecAirdrop, isValidAirdropOutputsArray, } from 'utils/validation'; import { CustomSpinner } from 'components/Common/CustomIcons'; import * as etokenList from 'etoken-list'; import { ZeroBalanceHeader, SidePaddingCtn, WalletInfoCtn, } from 'components/Common/Atoms'; import WalletLabel from 'components/Common/WalletLabel.js'; import { Link } from 'react-router-dom'; const AirdropActions = styled.div` text-align: center; width: 100%; padding: 10px; border-radius: 5px; a { color: ${props => props.theme.contrast}; margin: 0; font-size: 11px; border: 1px solid ${props => props.theme.contrast}; border-radius: 5px; padding: 2px 10px; opacity: 0.6; } a:hover { opacity: 1; border-color: ${props => props.theme.eCashBlue}; color: ${props => props.theme.contrast}; background: ${props => props.theme.eCashBlue}; } ${({ received, ...props }) => received && ` text-align: left; background: ${props.theme.receivedMessage}; `} `; // Note jestBCH is only used for unit tests; BCHJS must be mocked for jest const Airdrop = ({ jestBCH, passLoadingStatus }) => { const ContextValue = React.useContext(WalletContext); const { wallet, fiatPrice, cashtabSettings } = ContextValue; const location = useLocation(); const walletState = getWalletState(wallet); const { balances } = walletState; const [bchObj, setBchObj] = useState(false); const [isAirdropCalcModalVisible, setIsAirdropCalcModalVisible] = useState(false); const [airdropCalcModalProgress, setAirdropCalcModalProgress] = useState(0); // the dynamic % progress bar 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); if (location && location.state && location.state.airdropEtokenId) { setFormData({ ...formData, tokenId: location.state.airdropEtokenId, }); handleTokenIdInput({ target: { value: location.state.airdropEtokenId, }, }); } }, []); const [formData, setFormData] = useState({ tokenId: '', totalAirdrop: '', }); const [tokenIdIsValid, setTokenIdIsValid] = useState(null); const [totalAirdropIsValid, setTotalAirdropIsValid] = useState(null); const [airdropRecipients, setAirdropRecipients] = useState(''); const [airdropOutputIsValid, setAirdropOutputIsValid] = useState(true); const [etokenHolders, setEtokenHolders] = useState(new BigNumber(0)); const [showAirdropOutputs, setShowAirdropOutputs] = useState(false); const { getBCH } = useBCH(); const handleTokenIdInput = e => { const { name, value } = e.target; setTokenIdIsValid(isValidTokenId(value)); setFormData(p => ({ ...p, [name]: value, })); }; const handleTotalAirdropInput = e => { const { name, value } = e.target; setTotalAirdropIsValid(isValidXecAirdrop(value)); setFormData(p => ({ ...p, [name]: value, })); }; const calculateXecAirdrop = async () => { // display airdrop calculation message modal setIsAirdropCalcModalVisible(true); setShowAirdropOutputs(false); // hide any previous airdrop outputs passLoadingStatus(true); setAirdropCalcModalProgress(25); // updated progress bar to 25% let latestBlock; try { latestBlock = await bchObj.Blockchain.getBlockCount(); } catch (err) { errorNotification( err, 'Error retrieving latest block height', 'bchObj.Blockchain.getBlockCount() error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } setAirdropCalcModalProgress(50); etokenList.Config.SetUrl(currency.tokenDbUrl); let airdropList; try { airdropList = await etokenList.List.GetAddressListFor( formData.tokenId, latestBlock, true, ); } catch (err) { errorNotification( err, 'Error retrieving airdrop recipients', 'etokenList.List.GetAddressListFor() error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } if (!airdropList) { errorNotification( null, 'No recipients found for tokenId ' + formData.tokenId, 'Airdrop Calculation Error', ); setIsAirdropCalcModalVisible(false); passLoadingStatus(false); return; } setAirdropCalcModalProgress(75); let totalTokenAmongstRecipients = new BigNumber(0); let totalHolders = new BigNumber(airdropList.size); // amount of addresses that hold this eToken setEtokenHolders(totalHolders); // keep a cumulative total of each eToken holding in each address in airdropList airdropList.forEach( index => (totalTokenAmongstRecipients = totalTokenAmongstRecipients.plus( new BigNumber(index), )), ); let circToAirdropRatio = new BigNumber(formData.totalAirdrop).div( totalTokenAmongstRecipients, ); let resultString = ''; airdropList.forEach( (element, index) => (resultString += convertEtokenToEcashAddr(index) + ',' + new BigNumber(element) .multipliedBy(circToAirdropRatio) .decimalPlaces(currency.cashDecimals) + '\n'), ); resultString = resultString.substring(0, resultString.length - 1); // remove the final newline setAirdropRecipients(resultString); setAirdropCalcModalProgress(100); if (!resultString) { errorNotification( null, 'No holders found for eToken ID: ' + formData.tokenId, 'Airdrop Calculation Error', ); return; } // validate the airdrop values for each recipient // Note: addresses are not validated as they are retrieved directly from onchain setAirdropOutputIsValid(isValidAirdropOutputsArray(resultString)); setShowAirdropOutputs(true); // display the airdrop outputs TextArea setIsAirdropCalcModalVisible(false); passLoadingStatus(false); }; const handleAirdropCalcModalCancel = () => { setIsAirdropCalcModalVisible(false); passLoadingStatus(false); }; let airdropCalcInputIsValid = tokenIdIsValid && totalAirdropIsValid; return ( <> {!balances.totalBalance ? ( You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : ( <> {fiatPrice !== null && ( )} )}


handleTokenIdInput(e) } /> handleTotalAirdropInput(e) } /> calculateXecAirdrop() } disabled={ !airdropCalcInputIsValid || !tokenIdIsValid } > Calculate Airdrop {showAirdropOutputs && ( <> {!airdropOutputIsValid && etokenHolders > 0 && ( <>
)} One to Many Airdrop Payment Outputs