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 ? (
history.push('/wallet')}
>
history.push('/send')}
>
history.push('receive')}
>
history.push('/airdrop')}
>
history.push('/configure')}
>
) : 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
Copy to Send screen
{
navigator.clipboard.writeText(
airdropRecipients,
);
generalNotification(
'Airdrop recipients copied to clipboard',
'Copied',
);
}}
>
Copy to Clipboard
>
)}
>
);
};
+/*
+passLoadingStatus must receive a default prop that is a function
+in order to pass the rendering unit test in Airdrop.test.js
+
+status => {console.log(status)} is an arbitrary stub function
+*/
+
Airdrop.defaultProps = {
passLoadingStatus: status => {
console.log(status);
},
};
Airdrop.propTypes = {
jestBCH: PropTypes.object,
passLoadingStatus: PropTypes.func,
};
export default Airdrop;
diff --git a/web/cashtab-v2/src/components/App.js b/web/cashtab-v2/src/components/App.js
index 41b1fd4aa..4ab5db3b4 100644
--- a/web/cashtab-v2/src/components/App.js
+++ b/web/cashtab-v2/src/components/App.js
@@ -1,378 +1,383 @@
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';
// Easter egg imports not used in extension/src/components/App.js
import TabCash from 'assets/tabcash.png';
import { checkForTokenById } from 'utils/tokenMethods.js';
// Biometric security import not used in extension/src/components/App.js
import ProtectableComponentWrapper from './Authentication/ProtectableComponentWrapper';
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;
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;
}
.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: '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: center;
width: 100%;
padding: 15px 0;
`;
export const CashTabLogo = styled.img`
width: 120px;
@media (max-width: 768px) {
width: 110px;
}
`;
// AbcLogo styled component not included in extension, replaced by open in new tab link
export const AbcLogo = styled.img`
width: 150px;
@media (max-width: 768px) {
width: 120px;
}
`;
// Easter egg styled component not used in extension/src/components/App.js
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;
}
`;
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) : '';
// Easter egg boolean not used in extension/src/components/App.js
const hasTab = validWallet
? checkForTokenById(
wallet.state.tokens,
'50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e',
)
: false;
return (
{/*Begin component not included in extension as desktop only*/}
{hasTab && (
)}
{/*End component not included in extension as desktop only*/}
(
)}
/>
{wallet ? (
history.push('/wallet')}
>
history.push('/send')}
>
history.push('receive')}
>
history.push('/airdrop')}
>
history.push('/configure')}
>
) : null}
);
};
+App.propTypes = {
+ match: PropTypes.string,
+};
+
export default App;
diff --git a/web/cashtab-v2/src/components/Authentication/ProtectableComponentWrapper.js b/web/cashtab-v2/src/components/Authentication/ProtectableComponentWrapper.js
index 54a2603db..08d0daeae 100644
--- a/web/cashtab-v2/src/components/Authentication/ProtectableComponentWrapper.js
+++ b/web/cashtab-v2/src/components/Authentication/ProtectableComponentWrapper.js
@@ -1,32 +1,36 @@
import React, { useContext } from 'react';
import { AuthenticationContext } from 'utils/context';
+import PropTypes from 'prop-types';
import SignUp from './SignUp';
import SignIn from './SignIn';
const ProtectableComponentWrapper = ({ children }) => {
const authentication = useContext(AuthenticationContext);
-
if (authentication) {
const { loading, isAuthenticationRequired, isSignedIn } =
authentication;
if (loading) {
return
Loading authenticaion data...
;
}
// prompt if user would like to enable biometric lock when the app first run
if (isAuthenticationRequired === undefined) {
return ;
}
// prompt user to sign in
if (isAuthenticationRequired && !isSignedIn) {
return ;
}
}
// authentication = null => authentication is not supported
return <>{children}>;
};
+ProtectableComponentWrapper.propTypes = {
+ children: PropTypes.objectOf(PropTypes.node),
+};
+
export default ProtectableComponentWrapper;
diff --git a/web/cashtab-v2/src/components/Common/QRCode.js b/web/cashtab-v2/src/components/Common/QRCode.js
index 8cd7c6485..0907b3378 100644
--- a/web/cashtab-v2/src/components/Common/QRCode.js
+++ b/web/cashtab-v2/src/components/Common/QRCode.js
@@ -1,244 +1,251 @@
import React, { useState } from 'react';
+import PropTypes from 'prop-types';
import styled from 'styled-components';
import RawQRCode from 'qrcode.react';
import { currency } from 'components/Common/Ticker.js';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Event } from 'utils/GoogleAnalytics';
import { convertToEcashPrefix } from 'utils/cashMethods';
export const StyledRawQRCode = styled(RawQRCode)`
cursor: pointer;
border-radius: 10px;
background: ${props => props.theme.qr.background};
margin-bottom: 10px;
path:first-child {
fill: ${props => props.theme.qr.background};
}
:hover {
border-color: ${({ xec = 0, ...props }) =>
xec === 1 ? props.theme.eCashBlue : props.theme.eCashPurple};
}
@media (max-width: 768px) {
border-radius: 18px;
width: 170px;
height: 170px;
}
`;
const Copied = styled.div`
font-size: 18px;
font-weight: bold;
width: 100%;
text-align: center;
background-color: ${({ xec = 0, ...props }) =>
xec === 1 ? props.theme.eCashBlue : props.theme.eCashPurple};
border: 1px solid;
border-color: ${({ xec = 0, ...props }) =>
xec === 1 ? props.theme.eCashBlue : props.theme.eCashPurple};
color: ${props => props.theme.contrast};
position: absolute;
top: 65px;
padding: 30px 0;
@media (max-width: 768px) {
top: 52px;
padding: 20px 0;
}
`;
const PrefixLabel = styled.span`
text-align: right;
font-weight: bold;
color: ${({ xec = 0, ...props }) =>
xec === 1 ? props.theme.eCashBlue : props.theme.eCashPurple};
@media (max-width: 768px) {
font-size: 12px;
}
@media (max-width: 400px) {
font-size: 10px;
}
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
`;
const AddressHighlightTrim = styled.span`
font-weight: bold;
color: ${props => props.theme.contrast};
@media (max-width: 768px) {
font-size: 12px;
}
@media (max-width: 400px) {
font-size: 10px;
}
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
`;
const CustomInput = styled.div`
font-size: 14px;
color: ${props => props.theme.lightWhite};
text-align: center;
cursor: pointer;
margin-bottom: 10px;
padding: 6px 0;
font-family: 'Roboto Mono', monospace;
border-radius: 5px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
input {
border: none;
width: 100%;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: pointer;
color: ${props => props.theme.contrast};
padding: 10px 0;
background: transparent;
margin-bottom: 15px;
display: none;
}
input:focus {
outline: none;
}
input::selection {
background: transparent;
color: ${props => props.theme.contrast};
}
@media (max-width: 768px) {
font-size: 10px;
input {
font-size: 10px;
margin-bottom: 10px;
}
}
@media (max-width: 400px) {
font-size: 7px;
input {
font-size: 10px;
margin-bottom: 10px;
}
}
`;
export const QRCode = ({
address,
isCashAddress,
size = 210,
onClick = () => null,
- ...otherProps
}) => {
address = address ? convertToEcashPrefix(address) : '';
const [visible, setVisible] = useState(false);
const trimAmount = 8;
const address_trim = address ? address.length - trimAmount : '';
const addressSplit = address ? address.split(':') : [''];
const addressPrefix = addressSplit[0];
const prefixLength = addressPrefix.length + 1;
const txtRef = React.useRef(null);
const handleOnClick = evt => {
setVisible(true);
setTimeout(() => {
setVisible(false);
}, 1500);
onClick(evt);
};
const handleOnCopy = () => {
// Event.("Category", "Action", "Label")
// xec or etoken?
let eventLabel = currency.ticker;
if (address && !isCashAddress) {
eventLabel = currency.tokenTicker;
// Event('Category', 'Action', 'Label')
Event('Wallet', 'Copy Address', eventLabel);
}
setVisible(true);
setTimeout(() => {
txtRef.current.select();
}, 100);
};
return (
);
};
+
+QRCode.propTypes = {
+ address: PropTypes.string,
+ isCashAddress: PropTypes.func,
+ size: PropTypes.number,
+ onClick: PropTypes.func,
+};
diff --git a/web/cashtab-v2/src/components/Common/ScanQRCode.js b/web/cashtab-v2/src/components/Common/ScanQRCode.js
index d51d0e810..da1b25750 100644
--- a/web/cashtab-v2/src/components/Common/ScanQRCode.js
+++ b/web/cashtab-v2/src/components/Common/ScanQRCode.js
@@ -1,187 +1,186 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Alert, Modal } from 'antd';
import { ThemedQrcodeOutlined } from 'components/Common/CustomIcons';
import { errorNotification } from './Notifications';
import styled from 'styled-components';
import { BrowserQRCodeReader } from '@zxing/library';
import { currency, parseAddressForParams } from 'components/Common/Ticker.js';
import { Event } from 'utils/GoogleAnalytics';
import { isValidXecAddress, isValidEtokenAddress } from 'utils/validation';
const StyledScanQRCode = styled.span`
display: block;
`;
const StyledModal = styled(Modal)`
width: 400px !important;
height: 400px !important;
.ant-modal-close {
top: 0 !important;
right: 0 !important;
}
`;
const QRPreview = styled.video`
width: 100%;
`;
const ScanQRCode = ({
loadWithCameraOpen,
onScan = () => null,
...otherProps
}) => {
const [visible, setVisible] = useState(loadWithCameraOpen);
const [error, setError] = useState(false);
// Use these states to debug video errors on mobile
// Note: iOS chrome/brave/firefox does not support accessing camera, will throw error
// iOS users can use safari
// todo only show scanner with safari
//const [mobileError, setMobileError] = useState(false);
//const [mobileErrorMsg, setMobileErrorMsg] = useState(false);
const [activeCodeReader, setActiveCodeReader] = useState(null);
const teardownCodeReader = codeReader => {
if (codeReader !== null) {
codeReader.reset();
codeReader.stop();
codeReader = null;
setActiveCodeReader(codeReader);
}
};
const parseContent = content => {
let type = 'unknown';
let values = {};
const addressInfo = parseAddressForParams(content);
// If what scanner reads from QR code is a valid eCash or eToken address
if (
isValidXecAddress(addressInfo.address) ||
isValidEtokenAddress(content)
) {
type = 'address';
values = {
address: content,
};
// Event("Category", "Action", "Label")
// Track number of successful QR code scans
// BCH or slp?
let eventLabel = currency.ticker;
const isToken = content.split(currency.tokenPrefix).length > 1;
if (isToken) {
eventLabel = currency.tokenTicker;
}
Event('ScanQRCode.js', 'Address Scanned', eventLabel);
}
return { type, values };
};
const scanForQrCode = async () => {
const codeReader = new BrowserQRCodeReader();
setActiveCodeReader(codeReader);
try {
// Need to execute this before you can decode input
// eslint-disable-next-line no-unused-vars
const videoInputDevices = await codeReader.getVideoInputDevices();
//console.log(`videoInputDevices`, videoInputDevices);
//setMobileError(JSON.stringify(videoInputDevices));
// choose your media device (webcam, frontal camera, back camera, etc.)
// TODO implement if necessary
//const selectedDeviceId = videoInputDevices[0].deviceId;
//const previewElem = document.querySelector("#test-area-qr-code-webcam");
let result = { type: 'unknown', values: {} };
while (result.type !== 'address') {
const content = await codeReader.decodeFromInputVideoDevice(
undefined,
'test-area-qr-code-webcam',
);
result = parseContent(content.text);
if (result.type !== 'address') {
errorNotification(
content.text,
`${content.text} is not a valid eCash address`,
`${content.text} is not a valid eCash address`,
);
}
}
// When you scan a valid address, stop scanning and fill form
// Hide the scanner
setVisible(false);
onScan(result.values.address);
return teardownCodeReader(codeReader);
} catch (err) {
console.log(`Error in QR scanner:`);
console.log(err);
console.log(JSON.stringify(err.message));
//setMobileErrorMsg(JSON.stringify(err.message));
setError(err);
return teardownCodeReader(codeReader);
}
};
React.useEffect(() => {
if (!visible) {
setError(false);
// Stop the camera if user closes modal
if (activeCodeReader !== null) {
teardownCodeReader(activeCodeReader);
}
} else {
scanForQrCode();
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
return (
<>
setVisible(!visible)}
>
setVisible(false)}
footer={null}
>
{visible ? (
{error ? (
<>
{/*
{mobileError}
{mobileErrorMsg}
*/}
>
) : (
)}
) : null}
>
);
};
ScanQRCode.propTypes = {
loadWithCameraOpen: PropTypes.bool,
onScan: PropTypes.func,
};
export default ScanQRCode;
diff --git a/web/cashtab-v2/src/components/Configure/Configure.js b/web/cashtab-v2/src/components/Configure/Configure.js
index 662bbb49d..5411800e4 100644
--- a/web/cashtab-v2/src/components/Configure/Configure.js
+++ b/web/cashtab-v2/src/components/Configure/Configure.js
@@ -1,809 +1,808 @@
-/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { Collapse, Form, Input, Modal, Alert, Switch, Tag } from 'antd';
import {
PlusSquareOutlined,
WalletFilled,
ImportOutlined,
LockOutlined,
CheckOutlined,
CloseOutlined,
LockFilled,
ExclamationCircleFilled,
} from '@ant-design/icons';
import { WalletContext, AuthenticationContext } from 'utils/context';
import { SidePaddingCtn } from 'components/Common/Atoms';
import { StyledCollapse } from 'components/Common/StyledCollapse';
import {
AntdFormWrapper,
CurrencySelectDropdown,
} from 'components/Common/EnhancedInputs';
import PrimaryButton, {
SecondaryButton,
SmartButton,
} from 'components/Common/PrimaryButton';
import {
ThemedCopyOutlined,
ThemedWalletOutlined,
ThemedDollarOutlined,
ThemedSettingOutlined,
} from 'components/Common/CustomIcons';
import { ReactComponent as Trashcan } from 'assets/trashcan.svg';
import { ReactComponent as Edit } from 'assets/edit.svg';
import { Event } from 'utils/GoogleAnalytics';
import ApiError from 'components/Common/ApiError';
import { formatSavedBalance } from 'utils/formatting';
const { Panel } = Collapse;
const SettingsLink = styled.a`
text-decoration: underline;
color: ${props => props.theme.eCashBlue};
:visited {
text-decoration: underline;
color: ${props => props.theme.eCashBlue};
}
:hover {
color: ${props => props.theme.eCashPurple};
}
`;
const SWRow = styled.div`
border-radius: 3px;
padding: 10px 0;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 6px;
@media (max-width: 500px) {
flex-direction: column;
margin-bottom: 12px;
}
`;
const SWName = styled.div`
width: 50%;
display: flex;
align-items: center;
justify-content: space-between;
word-wrap: break-word;
hyphens: auto;
@media (max-width: 500px) {
width: 100%;
justify-content: center;
margin-bottom: 15px;
}
h3 {
font-size: 16px;
color: ${props => props.theme.darkBlue};
margin: 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
h3.overflow {
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
}
h3.overflow:hover {
background-color: ${props => props.theme.settings.background};
overflow: visible;
inline-size: 100px;
white-space: normal;
}
`;
const SWBalance = styled.div`
width: 50%;
display: flex;
align-items: center;
justify-content: space-between;
word-wrap: break-word;
hyphens: auto;
@media (max-width: 500px) {
width: 100%;
justify-content: center;
margin-bottom: 15px;
}
div {
font-size: 13px;
color: ${props => props.theme.darkBlue};
margin: 0;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
div.overflow {
width: 150px;
overflow: hidden;
text-overflow: ellipsis;
}
div.overflow:hover {
background-color: ${props => props.theme.settings.background};
overflow: visible;
inline-size: 150px;
white-space: normal;
}
`;
const SWButtonCtn = styled.div`
width: 50%;
display: flex;
align-items: center;
justify-content: flex-end;
@media (max-width: 500px) {
width: 100%;
justify-content: center;
}
button {
cursor: pointer;
background: transparent;
border: 1px solid #fff;
box-shadow: none;
color: #fff;
border-radius: 3px;
opacity: 0.6;
transition: all 200ms ease-in-out;
:hover {
opacity: 1;
background: ${props => props.theme.eCashBlue};
border-color: ${props => props.theme.eCashBlue};
}
@media (max-width: 768px) {
font-size: 14px;
}
}
svg {
stroke: ${props => props.theme.eCashBlue};
fill: ${props => props.theme.eCashBlue};
width: 25px;
height: 25px;
margin-right: 20px;
cursor: pointer;
:first-child:hover {
stroke: ${props => props.theme.eCashBlue};
fill: ${props => props.theme.eCashBlue};
}
:hover {
stroke: ${props => props.theme.settings.delete};
fill: ${props => props.theme.settings.delete};
}
}
`;
const AWRow = styled.div`
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
h3 {
font-size: 16px;
display: inline-block;
color: ${props => props.theme.darkBlue};
margin: 0;
text-align: left;
font-weight: bold;
@media (max-width: 500px) {
font-size: 14px;
}
}
h4 {
font-size: 16px;
display: inline-block;
color: ${props => props.theme.eCashBlue} !important;
margin: 0;
text-align: right;
}
@media (max-width: 500px) {
flex-direction: column;
margin-bottom: 12px;
}
`;
const StyledConfigure = styled.div`
h2 {
color: ${props => props.theme.contrast};
font-size: 25px;
}
svg {
fill: ${props => props.theme.eCashBlue};
}
p {
color: ${props => props.theme.darkBlue};
}
`;
const StyledSpacer = styled.div`
height: 1px;
width: 100%;
background-color: ${props => props.theme.lightWhite};
margin: 60px 0 50px;
`;
const GeneralSettingsItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
.ant-switch svg {
fill: #717171;
}
.title {
color: ${props => props.theme.contrast};
}
.anticon {
color: ${props => props.theme.contrast};
}
.ant-switch {
background-color: #bdbdbd;
}
.ant-switch-checked {
background-color: ${props => props.theme.eCashBlue};
svg {
fill: ${props => props.theme.contrast};
}
}
.SendConfirm {
color: ${props => props.theme.lightWhite};
}
`;
const Configure = () => {
const ContextValue = React.useContext(WalletContext);
const authentication = React.useContext(AuthenticationContext);
const { wallet, apiError } = ContextValue;
const {
addNewSavedWallet,
activateWallet,
renameWallet,
deleteWallet,
validateMnemonic,
getSavedWallets,
cashtabSettings,
changeCashtabSettings,
} = ContextValue;
const [savedWallets, setSavedWallets] = useState([]);
const [formData, setFormData] = useState({
dirty: true,
mnemonic: '',
});
const [showRenameWalletModal, setShowRenameWalletModal] = useState(false);
const [showDeleteWalletModal, setShowDeleteWalletModal] = useState(false);
const [walletToBeRenamed, setWalletToBeRenamed] = useState(null);
const [walletToBeDeleted, setWalletToBeDeleted] = useState(null);
const [newWalletName, setNewWalletName] = useState('');
const [
confirmationOfWalletToBeDeleted,
setConfirmationOfWalletToBeDeleted,
] = useState('');
const [newWalletNameIsValid, setNewWalletNameIsValid] = useState(null);
const [walletDeleteValid, setWalletDeleteValid] = useState(null);
const [seedInput, openSeedInput] = useState(false);
const [showTranslationWarning, setShowTranslationWarning] = useState(false);
const showPopulatedDeleteWalletModal = walletInfo => {
setWalletToBeDeleted(walletInfo);
setShowDeleteWalletModal(true);
};
const showPopulatedRenameWalletModal = walletInfo => {
setWalletToBeRenamed(walletInfo);
setShowRenameWalletModal(true);
};
const cancelRenameWallet = () => {
// Delete form value
setNewWalletName('');
setShowRenameWalletModal(false);
};
const cancelDeleteWallet = () => {
setWalletToBeDeleted(null);
setConfirmationOfWalletToBeDeleted('');
setShowDeleteWalletModal(false);
};
const updateSavedWallets = async activeWallet => {
if (activeWallet) {
let savedWallets;
try {
savedWallets = await getSavedWallets(activeWallet);
setSavedWallets(savedWallets);
} catch (err) {
console.log(`Error in getSavedWallets()`);
console.log(err);
}
}
};
const [isValidMnemonic, setIsValidMnemonic] = useState(null);
useEffect(() => {
// Update savedWallets every time the active wallet changes
updateSavedWallets(wallet);
}, [wallet]);
useEffect(() => {
const detectedBrowserLang = navigator.language;
if (!detectedBrowserLang.includes('en-')) {
setShowTranslationWarning(true);
}
}, []);
// Need this function to ensure that savedWallets are updated on new wallet creation
const updateSavedWalletsOnCreate = async importMnemonic => {
// Event("Category", "Action", "Label")
// Track number of times a different wallet is activated
Event('Configure.js', 'Create Wallet', 'New');
const walletAdded = await addNewSavedWallet(importMnemonic);
if (!walletAdded) {
Modal.error({
title: 'This wallet already exists!',
content: 'Wallet not added',
});
} else {
Modal.success({
content: 'Wallet added to your saved wallets',
});
}
await updateSavedWallets(wallet);
};
// Same here
// TODO you need to lock UI here until this is complete
// Otherwise user may try to load an already-loading wallet, wreak havoc with indexedDB
const updateSavedWalletsOnLoad = async walletToActivate => {
// Event("Category", "Action", "Label")
// Track number of times a different wallet is activated
Event('Configure.js', 'Activate', '');
await activateWallet(walletToActivate);
};
async function submit() {
setFormData({
...formData,
dirty: false,
});
// Exit if no user input
if (!formData.mnemonic) {
return;
}
// Exit if mnemonic is invalid
if (!isValidMnemonic) {
return;
}
// Event("Category", "Action", "Label")
// Track number of times a different wallet is activated
Event('Configure.js', 'Create Wallet', 'Imported');
updateSavedWalletsOnCreate(formData.mnemonic);
}
const handleChange = e => {
const { value, name } = e.target;
// Validate mnemonic on change
// Import button should be disabled unless mnemonic is valid
setIsValidMnemonic(validateMnemonic(value));
setFormData(p => ({ ...p, [name]: value }));
};
const changeWalletName = async () => {
if (newWalletName === '' || newWalletName.length > 24) {
setNewWalletNameIsValid(false);
return;
}
// Hide modal
setShowRenameWalletModal(false);
// Change wallet name
console.log(
`Changing wallet ${walletToBeRenamed.name} name to ${newWalletName}`,
);
const renameSuccess = await renameWallet(
walletToBeRenamed.name,
newWalletName,
);
if (renameSuccess) {
Modal.success({
content: `Wallet "${walletToBeRenamed.name}" renamed to "${newWalletName}"`,
});
} else {
Modal.error({
content: `Rename failed. All wallets must have a unique name.`,
});
}
await updateSavedWallets(wallet);
// Clear wallet name for form
setNewWalletName('');
};
const deleteSelectedWallet = async () => {
if (!walletDeleteValid && walletDeleteValid !== null) {
return;
}
if (
confirmationOfWalletToBeDeleted !==
`delete ${walletToBeDeleted.name}`
) {
setWalletDeleteValid(false);
return;
}
// Hide modal
setShowDeleteWalletModal(false);
// Change wallet name
console.log(`Deleting wallet "${walletToBeDeleted.name}"`);
const walletDeletedSuccess = await deleteWallet(walletToBeDeleted);
if (walletDeletedSuccess) {
Modal.success({
content: `Wallet "${walletToBeDeleted.name}" successfully deleted`,
});
} else {
Modal.error({
content: `Error deleting ${walletToBeDeleted.name}.`,
});
}
await updateSavedWallets(wallet);
// Clear wallet delete confirmation from form
setConfirmationOfWalletToBeDeleted('');
};
const handleWalletNameInput = e => {
const { value } = e.target;
// validation
if (value && value.length && value.length < 24) {
setNewWalletNameIsValid(true);
} else {
setNewWalletNameIsValid(false);
}
setNewWalletName(value);
};
const handleWalletToDeleteInput = e => {
const { value } = e.target;
if (value && value === `delete ${walletToBeDeleted.name}`) {
setWalletDeleteValid(true);
} else {
setWalletDeleteValid(false);
}
setConfirmationOfWalletToBeDeleted(value);
};
- const handleAppLockToggle = (checked, e) => {
+ const handleAppLockToggle = checked => {
if (checked) {
// if there is an existing credential, that means user has registered
// simply turn on the Authentication Required flag
if (authentication.credentialId) {
authentication.turnOnAuthentication();
} else {
// there is no existing credential, that means user has not registered
// user need to register
authentication.signUp();
}
} else {
authentication.turnOffAuthentication();
}
};
const handleSendModalToggle = checkedState => {
changeCashtabSettings('sendModal', checkedState);
};
return (
{walletToBeRenamed !== null && (
cancelRenameWallet()}
>
}
placeholder="Enter new wallet name"
name="newName"
value={newWalletName}
onChange={e => handleWalletNameInput(e)}
/>
)}
{walletToBeDeleted !== null && (
cancelDeleteWallet()}
>
}
placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`}
name="walletToBeDeletedInput"
value={confirmationOfWalletToBeDeleted}
onChange={e =>
handleWalletToDeleteInput(e)
}
/>
)}
Backup your wallet
{showTranslationWarning && (
)}
{wallet && wallet.mnemonic && (
{wallet && wallet.mnemonic
? wallet.mnemonic
: ''}
)}
Manage Wallets
{apiError ? (
) : (
<>
updateSavedWalletsOnCreate()}
>
New Wallet
openSeedInput(!seedInput)}
>
Import Wallet
{seedInput && (
<>
Copy and paste your mnemonic seed phrase
below to import an existing wallet
}
type="email"
placeholder="mnemonic (seed phrase)"
name="mnemonic"
autoComplete="off"
onChange={e => handleChange(e)}
required
/>
submit()}
>
Import
>
)}
>
)}
{savedWallets && savedWallets.length > 0 && (
<>
{wallet.name}
Currently active
{savedWallets.map(sw => (
{sw.name}
[
{sw && sw.state
? formatSavedBalance(
sw.state.balances
.totalBalance,
)
: 'N/A'}{' '}
XEC]
showPopulatedRenameWalletModal(
sw,
)
}
/>
showPopulatedDeleteWalletModal(
sw,
)
}
/>
updateSavedWalletsOnLoad(
sw,
)
}
>
Activate
))}
>
)}
Fiat Currency
changeCashtabSettings('fiatCurrency', fiatCode)
}
/>
General Settings
Lock App
{authentication ? (
}
unCheckedChildren={ }
checked={
authentication.isAuthenticationRequired &&
authentication.credentialId
? true
: false
}
// checked={false}
onChange={handleAppLockToggle}
/>
) : (
}>
Not Supported
)}
Send Confirmations
}
unCheckedChildren={ }
checked={
cashtabSettings ? cashtabSettings.sendModal : false
}
onChange={handleSendModalToggle}
/>
[
Documentation
]
);
};
export default Configure;
diff --git a/web/cashtab-v2/src/components/Home/Tx.js b/web/cashtab-v2/src/components/Home/Tx.js
index b23f4e60d..73677769d 100644
--- a/web/cashtab-v2/src/components/Home/Tx.js
+++ b/web/cashtab-v2/src/components/Home/Tx.js
@@ -1,708 +1,687 @@
import React from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
-import styled, { css } from 'styled-components';
+import styled from 'styled-components';
import {
SendIcon,
ReceiveIcon,
GenesisIcon,
UnparsedIcon,
} from 'components/Common/CustomIcons';
import { currency } from 'components/Common/Ticker';
import { fromLegacyDecimals } from 'utils/cashMethods';
import { formatBalance, formatDate } from 'utils/formatting';
import TokenIcon from 'components/Tokens/TokenIcon';
import { Collapse } from 'antd';
import { AntdContextCollapseWrapper } from 'components/Common/StyledCollapse';
import { generalNotification } from 'components/Common/Notifications';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import {
ThemedCopySolid,
ThemedLinkSolid,
} from 'components/Common/CustomIcons';
const TxIcon = styled.div`
svg {
width: 20px;
height: 20px;
}
height: 40px;
width: 40px;
border: 1px solid #fff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100px;
`;
const SentTx = styled(TxIcon)`
svg {
margin-right: -3px;
}
fill: ${props => props.theme.contrast};
`;
const ReceivedTx = styled(TxIcon)`
svg {
fill: ${props => props.theme.eCashBlue};
}
border-color: ${props => props.theme.eCashBlue};
`;
const GenesisTx = styled(TxIcon)`
border-color: ${props => props.theme.genesisGreen};
svg {
fill: ${props => props.theme.genesisGreen};
}
`;
const UnparsedTx = styled(TxIcon)`
color: ${props => props.theme.eCashBlue} !important;
`;
const DateType = styled.div`
text-align: left;
padding: 12px;
@media screen and (max-width: 500px) {
font-size: 0.8rem;
}
`;
const LeftTextCtn = styled.div`
text-align: left;
display: flex;
align-items: left;
flex-direction: column;
margin-left: 10px;
h3 {
color: ${props => props.theme.contrast};
font-size: 14px;
font-weight: 700;
margin: 0;
}
.genesis {
color: ${props => props.theme.genesisGreen};
}
.received {
color: ${props => props.theme.eCashBlue};
}
h4 {
font-size: 12px;
color: ${props => props.theme.lightWhite};
margin: 0;
}
`;
const RightTextCtn = styled.div`
text-align: right;
display: flex;
align-items: left;
flex-direction: column;
margin-left: 10px;
h3 {
color: ${props => props.theme.contrast};
font-size: 14px;
font-weight: 700;
margin: 0;
}
.genesis {
color: ${props => props.theme.genesisGreen};
}
.received {
color: ${props => props.theme.eCashBlue};
}
h4 {
font-size: 12px;
color: ${props => props.theme.lightWhite};
margin: 0;
}
`;
const OpReturnType = styled.div`
text-align: right;
width: 100%;
padding: 10px;
border-radius: 5px;
background: ${props => props.theme.sentMessage};
margin-top: 15px;
h4 {
color: ${props => props.theme.lightWhite};
margin: 0;
font-size: 12px;
display: inline-block;
}
p {
color: ${props => props.theme.contrast};
margin: 0;
font-size: 14px;
margin-bottom: 10px;
overflow-wrap: break-word;
}
a {
color: ${props => props.theme.contrast};
margin: 0;
font-size: 10px;
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};
`}
`;
-const SentLabel = styled.span`
- font-weight: bold;
- color: ${props => props.theme.secondary} !important;
-`;
+
const ReceivedLabel = styled.span`
font-weight: bold;
color: ${props => props.theme.eCashBlue} !important;
`;
-const GenesisLabel = styled.span`
- font-weight: bold;
- color: ${props => props.theme.genesisGreen} !important;
-`;
-const CashtabMessageLabel = styled.span`
- text-align: left;
- font-weight: bold;
- color: ${props => props.theme.eCashBlue} !important;
- white-space: nowrap;
-`;
+
const EncryptionMessageLabel = styled.span`
font-weight: bold;
font-size: 12px;
color: ${props => props.theme.encryptionRed};
white-space: nowrap;
`;
const UnauthorizedDecryptionMessage = styled.span`
text-align: left;
color: ${props => props.theme.encryptionRed};
white-space: nowrap;
font-style: italic;
`;
-const MessageLabel = styled.span`
- text-align: left;
- font-weight: bold;
- color: ${props => props.theme.secondary} !important;
- white-space: nowrap;
-`;
-const ReplyMessageLabel = styled.span`
- color: ${props => props.theme.eCashBlue} !important;
-`;
const TxInfo = styled.div`
text-align: right;
display: flex;
align-items: left;
flex-direction: column;
margin-left: 10px;
flex-grow: 2;
h3 {
color: ${props => props.theme.contrast};
font-size: 14px;
font-weight: 700;
margin: 0;
}
.genesis {
color: ${props => props.theme.genesisGreen};
}
.received {
color: ${props => props.theme.eCashBlue};
}
h4 {
font-size: 12px;
color: ${props => props.theme.lightWhite};
margin: 0;
}
@media screen and (max-width: 500px) {
font-size: 0.8rem;
}
`;
const TokenInfo = styled.div`
display: flex;
flex-grow: 1;
justify-content: flex-end;
color: ${props =>
props.outgoing ? props.theme.secondary : props.theme.eCashBlue};
@media screen and (max-width: 500px) {
font-size: 0.8rem;
grid-template-columns: 16px auto;
}
`;
const TxTokenIcon = styled.div`
img {
height: 24px;
width: 24px;
}
@media screen and (max-width: 500px) {
img {
height: 16px;
width: 16px;
}
}
grid-column-start: 1;
grid-column-end: span 1;
grid-row-start: 1;
grid-row-end: span 2;
align-self: center;
`;
const TokenTxAmt = styled.h3`
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const TokenName = styled.h4`
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const TxWrapper = styled.div`
display: flex;
align-items: center;
border-top: 1px solid rgba(255, 255, 255, 0.12);
color: ${props => props.theme.contrast};
padding: 10px 0;
flex-wrap: wrap;
width: 100%;
`;
const Panel = Collapse.Panel;
const DropdownIconWrapper = styled.div`
display: flex;
align-items: center;
gap: 4px;
`;
const TextLayer = styled.div`
font-size: 12px;
color: ${props => props.theme.contrast};
`;
const DropdownButton = styled.button`
display: flex;
justify-content: flex-end;
background-color: ${props => props.theme.walletBackground};
border: none;
cursor: pointer;
padding: 0;
&:hover {
div {
color: ${props => props.theme.eCashBlue}!important;
}
svg {
fill: ${props => props.theme.eCashBlue}!important;
}
}
`;
const PanelCtn = styled.div`
display: flex;
justify-content: flex-end;
right: 0;
gap: 8px;
`;
export const TxLink = styled.a`
color: ${props => props.theme.primary};
`;
const Tx = ({ data, fiatPrice, fiatCurrency }) => {
const txDate =
typeof data.blocktime === 'undefined'
? formatDate()
: formatDate(data.blocktime, navigator.language);
// if data only includes height and txid, then the tx could not be parsed by cashtab
// render as such but keep link to block explorer
let unparsedTx = false;
if (!Object.keys(data).includes('outgoingTx')) {
unparsedTx = true;
}
return (
<>
{unparsedTx ? (
Unparsed
{txDate}
Open in Explorer
) : (
{data.outgoingTx ? (
<>
{data.tokenTx &&
data.tokenInfo
.transactionType ===
'GENESIS' ? (
) : (
)}
>
) : (
)}
{data.outgoingTx ? (
<>
{data.tokenTx &&
data.tokenInfo
.transactionType ===
'GENESIS' ? (
Genesis
) : (
Sent
)}
>
) : (
Received
)}
{txDate}
{data.tokenTx ? (
{data.tokenTx &&
data.tokenInfo ? (
<>
{data.outgoingTx ? (
{data.tokenInfo
.transactionType ===
'GENESIS' ? (
<>
+{' '}
{data.tokenInfo.qtyReceived.toString()}
{
data
.tokenInfo
.tokenTicker
}
{
data
.tokenInfo
.tokenName
}
>
) : (
<>
-{' '}
{data.tokenInfo.qtySent.toString()}
{
data
.tokenInfo
.tokenTicker
}
{
data
.tokenInfo
.tokenName
}
>
)}
) : (
+{' '}
{data.tokenInfo.qtyReceived.toString()}
{
data
.tokenInfo
.tokenTicker
}
{
data
.tokenInfo
.tokenName
}
)}
>
) : (
Token Tx
)}
) : (
<>
{data.outgoingTx ? (
<>
-
{formatBalance(
fromLegacyDecimals(
data.amountSent,
),
)}{' '}
{
currency.ticker
}
{fiatPrice !==
null &&
!isNaN(
data.amountSent,
) && (
-
{
currency
.fiatCurrencies[
fiatCurrency
]
.symbol
}
{(
fromLegacyDecimals(
data.amountSent,
) *
fiatPrice
).toFixed(
2,
)}{' '}
{
currency
.fiatCurrencies
.fiatCurrency
}
)}
>
) : (
<>
+
{formatBalance(
fromLegacyDecimals(
data.amountReceived,
),
)}{' '}
{
currency.ticker
}
{fiatPrice !==
null &&
!isNaN(
data.amountReceived,
) && (
+
{
currency
.fiatCurrencies[
fiatCurrency
]
.symbol
}
{(
fromLegacyDecimals(
data.amountReceived,
) *
fiatPrice
).toFixed(
2,
)}{' '}
{
currency
.fiatCurrencies
.fiatCurrency
}
)}
>
)}
>
)}
{data.opReturnMessage && (
<>
{data.isCashtabMessage ? (
Cashtab Message
) : (
External Message
)}
{data.isEncryptedMessage ? (
- Encrypted
) : (
''
)}
{/*unencrypted OP_RETURN Message*/}
{data.opReturnMessage &&
!data.isEncryptedMessage ? (
{
data.opReturnMessage
}
) : (
''
)}
{/*encrypted and wallet is authorized to view OP_RETURN Message*/}
{data.opReturnMessage &&
data.isEncryptedMessage &&
data.decryptionSuccess ? (
{
data.opReturnMessage
}
) : (
''
)}
{/*encrypted but wallet is not authorized to view OP_RETURN Message*/}
{data.opReturnMessage &&
data.isEncryptedMessage &&
!data.decryptionSuccess ? (
{
data.opReturnMessage
}
) : (
''
)}
{!data.outgoingTx &&
data.replyAddress ? (
Reply To Message
) : (
''
)}
>
)}
>
}
>
{
generalNotification(
data.txid,
'Tx ID copied to clipboard',
);
}}
>
Copy Tx ID
View on be.cash
)}
>
);
};
Tx.propTypes = {
data: PropTypes.object,
fiatPrice: PropTypes.number,
fiatCurrency: PropTypes.string,
};
export default Tx;
diff --git a/web/cashtab-v2/src/components/Home/TxHistory.js b/web/cashtab-v2/src/components/Home/TxHistory.js
index 21364d613..ad30fb662 100644
--- a/web/cashtab-v2/src/components/Home/TxHistory.js
+++ b/web/cashtab-v2/src/components/Home/TxHistory.js
@@ -1,27 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
-import styled from 'styled-components';
import Tx from './Tx';
const TxHistory = ({ txs, fiatPrice, fiatCurrency }) => {
return (
{txs.map(tx => (
))}
);
};
TxHistory.propTypes = {
txs: PropTypes.array,
fiatPrice: PropTypes.number,
fiatCurrency: PropTypes.string,
};
export default TxHistory;
diff --git a/web/cashtab-v2/src/components/Home/__tests__/__snapshots__/Home.test.js.snap b/web/cashtab-v2/src/components/Home/__tests__/__snapshots__/Home.test.js.snap
index 75257b8bb..2a62a6c87 100644
--- a/web/cashtab-v2/src/components/Home/__tests__/__snapshots__/Home.test.js.snap
+++ b/web/cashtab-v2/src/components/Home/__tests__/__snapshots__/Home.test.js.snap
@@ -1,449 +1,449 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP @generated
exports[`Wallet with BCH balances 1`] = `
Array [
,
Transactions
eTokens
🎉
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 [
,
Transactions
eTokens
🎉
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
,
Transactions
eTokens
🎉
Congratulations on your new wallet!
🎉
Start using the wallet immediately to receive
XEC
payments, or load it up with
XEC
to send to others
,
]
`;
exports[`Wallet without BCH balance 1`] = `
Array [
,
Transactions
eTokens
🎉
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`] = `
`;
diff --git a/web/cashtab-v2/src/components/Send/Send.js b/web/cashtab-v2/src/components/Send/Send.js
index a0ce230be..001e1c985 100644
--- a/web/cashtab-v2/src/components/Send/Send.js
+++ b/web/cashtab-v2/src/components/Send/Send.js
@@ -1,1093 +1,1087 @@
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';
+import { Form, message, Modal, Alert, Collapse, Input } from 'antd';
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 { Panel } = Collapse;
const { TextArea } = Input;
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 checkForConfirmationBeforeSendXec = () => {
if (txInfoFromUrl) {
setIsModalVisible(true);
} else if (cashtabSettings.sendModal) {
setIsModalVisible(cashtabSettings.sendModal);
} else {
// if the user does not have the send confirmation enabled in settings then send directly
send();
}
};
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)}`,
});
}
// if this was routed from the Airdrop screen's Airdrop Calculator then
// switch to multiple recipient mode and prepopulate the recipients field
if (location && location.state && location.state.airdropRecipients) {
setIsOneToManyXECSend(true);
setFormData({
address: location.state.airdropRecipients,
});
// validate the airdrop outputs from the calculator
handleMultiAddressChange({
target: {
value: location.state.airdropRecipients,
},
});
}
// 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;
// parse address for parameters
const addressInfo = parseAddressForParams(addressString);
// validate address
const isValid = isValidXecAddress(addressInfo.address);
/*
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