Page MenuHomePhabricator

D15776.diff
No OneTemporary

D15776.diff

diff --git a/cashtab/extension/public/manifest.json b/cashtab/extension/public/manifest.json
--- a/cashtab/extension/public/manifest.json
+++ b/cashtab/extension/public/manifest.json
@@ -3,7 +3,7 @@
"name": "Cashtab",
"description": "A browser-integrated eCash wallet from Bitcoin ABC",
- "version": "3.7.1",
+ "version": "3.8.0",
"content_scripts": [
{
"matches": ["file://*/*", "http://*/*", "https://*/*"],
diff --git a/cashtab/package-lock.json b/cashtab/package-lock.json
--- a/cashtab/package-lock.json
+++ b/cashtab/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "cashtab",
- "version": "2.7.3",
+ "version": "2.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cashtab",
- "version": "2.7.3",
+ "version": "2.8.0",
"dependencies": {
"@ant-design/icons": "^5.3.0",
"@bitgo/utxo-lib": "^9.33.0",
diff --git a/cashtab/package.json b/cashtab/package.json
--- a/cashtab/package.json
+++ b/cashtab/package.json
@@ -1,6 +1,6 @@
{
"name": "cashtab",
- "version": "2.7.3",
+ "version": "2.8.0",
"private": true,
"scripts": {
"start": "node scripts/start.js",
diff --git a/cashtab/src/assets/qrcode.svg b/cashtab/src/assets/qrcode.svg
new file mode 100644
--- /dev/null
+++ b/cashtab/src/assets/qrcode.svg
@@ -0,0 +1,19 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="32" height="32" viewBox="0 0 2000 2000" x="0" y="0" shape-rendering="crispEdges"><defs></defs><rect x="0" y="0" width="2000" height="2000" fill="none"></rect><rect x="815" y="223" width="74" height="74" fill="#fff"></rect><rect x="963" y="223" width="74" height="74" fill="#fff"></rect><rect x="1037" y="223" width="74" height="74" fill="#fff"></rect><rect x="1111" y="223" width="74" height="74" fill="#fff"></rect><rect x="963" y="297" width="74" height="74" fill="#fff"></rect><rect x="1037" y="297" width="74" height="74" fill="#fff"></rect><rect x="815" y="371" width="74" height="74" fill="#fff"></rect><rect x="889" y="371" width="74" height="74" fill="#fff"></rect><rect x="1037" y="371" width="74" height="74" fill="#fff"></rect><rect x="815" y="445" width="74" height="74" fill="#fff"></rect><rect x="889" y="445" width="74" height="74" fill="#fff"></rect><rect x="1111" y="445" width="74" height="74" fill="#fff"></rect><rect x="815" y="519" width="74" height="74" fill="#fff"></rect><rect x="1037" y="519" width="74" height="74" fill="#fff"></rect><rect x="889" y="593" width="74" height="74" fill="#fff"></rect><rect x="963" y="593" width="74" height="74" fill="#fff"></rect><rect x="1037" y="593" width="74" height="74" fill="#fff"></rect><rect x="1111" y="593" width="74" height="74" fill="#fff"></rect><rect x="815" y="667" width="74" height="74" fill="#fff"></rect><rect x="963" y="667" width="74" height="74" fill="#fff"></rect><rect x="1111" y="667" width="74" height="74" fill="#fff"></rect><rect x="1037" y="741" width="74" height="74" fill="#fff"></rect><rect x="1111" y="741" width="74" height="74" fill="#fff"></rect><rect x="223" y="815" width="74" height="74" fill="#fff"></rect><rect x="297" y="815" width="74" height="74" fill="#fff"></rect><rect x="371" y="815" width="74" height="74" fill="#fff"></rect><rect x="445" y="815" width="74" height="74" fill="#fff"></rect><rect x="667" y="815" width="74" height="74" fill="#fff"></rect><rect x="815" y="815" width="74" height="74" fill="#fff"></rect><rect x="889" y="815" width="74" height="74" fill="#fff"></rect><rect x="963" y="815" width="74" height="74" fill="#fff"></rect><rect x="1037" y="815" width="74" height="74" fill="#fff"></rect><rect x="1111" y="815" width="74" height="74" fill="#fff"></rect><rect x="1185" y="815" width="74" height="74" fill="#fff"></rect><rect x="1407" y="815" width="74" height="74" fill="#fff"></rect><rect x="1481" y="815" width="74" height="74" fill="#fff"></rect><rect x="1555" y="815" width="74" height="74" fill="#fff"></rect><rect x="1703" y="815" width="74" height="74" fill="#fff"></rect><rect x="223" y="889" width="74" height="74" fill="#fff"></rect><rect x="371" y="889" width="74" height="74" fill="#fff"></rect><rect x="593" y="889" width="74" height="74" fill="#fff"></rect><rect x="741" y="889" width="74" height="74" fill="#fff"></rect><rect x="1037" y="889" width="74" height="74" fill="#fff"></rect><rect x="1111" y="889" width="74" height="74" fill="#fff"></rect><rect x="1185" y="889" width="74" height="74" fill="#fff"></rect><rect x="1259" y="889" width="74" height="74" fill="#fff"></rect><rect x="1333" y="889" width="74" height="74" fill="#fff"></rect><rect x="1481" y="889" width="74" height="74" fill="#fff"></rect><rect x="1555" y="889" width="74" height="74" fill="#fff"></rect><rect x="1629" y="889" width="74" height="74" fill="#fff"></rect><rect x="297" y="963" width="74" height="74" fill="#fff"></rect><rect x="445" y="963" width="74" height="74" fill="#fff"></rect><rect x="667" y="963" width="74" height="74" fill="#fff"></rect><rect x="741" y="963" width="74" height="74" fill="#fff"></rect><rect x="815" y="963" width="74" height="74" fill="#fff"></rect><rect x="889" y="963" width="74" height="74" fill="#fff"></rect><rect x="1037" y="963" width="74" height="74" fill="#fff"></rect><rect x="1481" y="963" width="74" height="74" fill="#fff"></rect><rect x="1555" y="963" width="74" height="74" fill="#fff"></rect><rect x="1629" y="963" width="74" height="74" fill="#fff"></rect><rect x="1703" y="963" width="74" height="74" fill="#fff"></rect><rect x="223" y="1037" width="74" height="74" fill="#fff"></rect><rect x="371" y="1037" width="74" height="74" fill="#fff"></rect><rect x="445" y="1037" width="74" height="74" fill="#fff"></rect><rect x="593" y="1037" width="74" height="74" fill="#fff"></rect><rect x="741" y="1037" width="74" height="74" fill="#fff"></rect><rect x="815" y="1037" width="74" height="74" fill="#fff"></rect><rect x="889" y="1037" width="74" height="74" fill="#fff"></rect><rect x="963" y="1037" width="74" height="74" fill="#fff"></rect><rect x="1037" y="1037" width="74" height="74" fill="#fff"></rect><rect x="1259" y="1037" width="74" height="74" fill="#fff"></rect><rect x="1407" y="1037" width="74" height="74" fill="#fff"></rect><rect x="1481" y="1037" width="74" height="74" fill="#fff"></rect><rect x="1629" y="1037" width="74" height="74" fill="#fff"></rect><rect x="371" y="1111" width="74" height="74" fill="#fff"></rect><rect x="445" y="1111" width="74" height="74" fill="#fff"></rect><rect x="519" y="1111" width="74" height="74" fill="#fff"></rect><rect x="593" y="1111" width="74" height="74" fill="#fff"></rect><rect x="667" y="1111" width="74" height="74" fill="#fff"></rect><rect x="815" y="1111" width="74" height="74" fill="#fff"></rect><rect x="889" y="1111" width="74" height="74" fill="#fff"></rect><rect x="963" y="1111" width="74" height="74" fill="#fff"></rect><rect x="1111" y="1111" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1111" width="74" height="74" fill="#fff"></rect><rect x="1481" y="1111" width="74" height="74" fill="#fff"></rect><rect x="1555" y="1111" width="74" height="74" fill="#fff"></rect><rect x="815" y="1185" width="74" height="74" fill="#fff"></rect><rect x="1037" y="1185" width="74" height="74" fill="#fff"></rect><rect x="1259" y="1185" width="74" height="74" fill="#fff"></rect><rect x="1703" y="1185" width="74" height="74" fill="#fff"></rect><rect x="963" y="1259" width="74" height="74" fill="#fff"></rect><rect x="1037" y="1259" width="74" height="74" fill="#fff"></rect><rect x="1111" y="1259" width="74" height="74" fill="#fff"></rect><rect x="889" y="1333" width="74" height="74" fill="#fff"></rect><rect x="963" y="1333" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1333" width="74" height="74" fill="#fff"></rect><rect x="1407" y="1333" width="74" height="74" fill="#fff"></rect><rect x="1555" y="1333" width="74" height="74" fill="#fff"></rect><rect x="1703" y="1333" width="74" height="74" fill="#fff"></rect><rect x="889" y="1407" width="74" height="74" fill="#fff"></rect><rect x="1111" y="1407" width="74" height="74" fill="#fff"></rect><rect x="1185" y="1407" width="74" height="74" fill="#fff"></rect><rect x="1259" y="1407" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1407" width="74" height="74" fill="#fff"></rect><rect x="1555" y="1407" width="74" height="74" fill="#fff"></rect><rect x="815" y="1481" width="74" height="74" fill="#fff"></rect><rect x="889" y="1481" width="74" height="74" fill="#fff"></rect><rect x="963" y="1481" width="74" height="74" fill="#fff"></rect><rect x="1037" y="1481" width="74" height="74" fill="#fff"></rect><rect x="1259" y="1481" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1481" width="74" height="74" fill="#fff"></rect><rect x="1481" y="1481" width="74" height="74" fill="#fff"></rect><rect x="815" y="1555" width="74" height="74" fill="#fff"></rect><rect x="963" y="1555" width="74" height="74" fill="#fff"></rect><rect x="1111" y="1555" width="74" height="74" fill="#fff"></rect><rect x="1259" y="1555" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1555" width="74" height="74" fill="#fff"></rect><rect x="815" y="1629" width="74" height="74" fill="#fff"></rect><rect x="889" y="1629" width="74" height="74" fill="#fff"></rect><rect x="1185" y="1629" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1629" width="74" height="74" fill="#fff"></rect><rect x="1407" y="1629" width="74" height="74" fill="#fff"></rect><rect x="1481" y="1629" width="74" height="74" fill="#fff"></rect><rect x="1629" y="1629" width="74" height="74" fill="#fff"></rect><rect x="815" y="1703" width="74" height="74" fill="#fff"></rect><rect x="1185" y="1703" width="74" height="74" fill="#fff"></rect><rect x="1333" y="1703" width="74" height="74" fill="#fff"></rect><rect x="1407" y="1703" width="74" height="74" fill="#fff"></rect><rect x="1481" y="1703" width="74" height="74" fill="#fff"></rect><svg version="1.1" id="Ebene_1" x="223" y="223" width="518" height="518" viewBox="0 0 699.988 699.986" enable-background="new 0 0 699.988 699.986" xml:space="preserve" shape-rendering="auto">
+<path fill="#fff" d="M600.99,0h-100h-99.997h-0.001h-99.997h-99.998h-99.998H1v99.998v99.998v99.998v99.999v99.997v99.998v99.998 h99.999h99.998h99.998h99.997h0.001h99.997h100h99.998v-99.998V499.99v-99.997v-99.999v-99.998V99.998V0H600.99z M600.99,199.996 v99.998v99.999v99.997v99.998h-100h-99.997h-0.001h-99.997h-99.998h-99.998V499.99v-99.997v-99.999v-99.998V99.998h99.998h99.998 h99.997h0.001h99.997h100V199.996z"></path>
+</svg>
+<svg version="1.0" id="Ebene_1" x="223" y="223" width="518" height="518" viewBox="0 0 699.988 699.988" enable-background="new 0 0 699.988 699.988" xml:space="preserve" shape-rendering="auto">
+<polygon fill="#fff" points="399.994,199.997 399.992,199.997 299.996,199.997 199.998,199.997 199.998,299.994 199.998,399.994 199.998,499.991 299.996,499.991 399.992,499.991 399.994,499.991 499.99,499.991 499.99,399.994 499.99,299.994 499.99,199.997 "></polygon>
+</svg>
+<svg version="1.1" id="Ebene_1" x="1259" y="223" width="518" height="518" viewBox="0 0 699.988 699.986" enable-background="new 0 0 699.988 699.986" xml:space="preserve" shape-rendering="auto">
+<path fill="#fff" d="M600.99,0h-100h-99.997h-0.001h-99.997h-99.998h-99.998H1v99.998v99.998v99.998v99.999v99.997v99.998v99.998 h99.999h99.998h99.998h99.997h0.001h99.997h100h99.998v-99.998V499.99v-99.997v-99.999v-99.998V99.998V0H600.99z M600.99,199.996 v99.998v99.999v99.997v99.998h-100h-99.997h-0.001h-99.997h-99.998h-99.998V499.99v-99.997v-99.999v-99.998V99.998h99.998h99.998 h99.997h0.001h99.997h100V199.996z"></path>
+</svg>
+<svg version="1.0" id="Ebene_1" x="1259" y="223" width="518" height="518" viewBox="0 0 699.988 699.988" enable-background="new 0 0 699.988 699.988" xml:space="preserve" shape-rendering="auto">
+<polygon fill="#fff" points="399.994,199.997 399.992,199.997 299.996,199.997 199.998,199.997 199.998,299.994 199.998,399.994 199.998,499.991 299.996,499.991 399.992,499.991 399.994,499.991 499.99,499.991 499.99,399.994 499.99,299.994 499.99,199.997 "></polygon>
+</svg>
+<svg version="1.1" id="Ebene_1" x="223" y="1259" width="518" height="518" viewBox="0 0 699.988 699.986" enable-background="new 0 0 699.988 699.986" xml:space="preserve" shape-rendering="auto">
+<path fill="#fff" d="M600.99,0h-100h-99.997h-0.001h-99.997h-99.998h-99.998H1v99.998v99.998v99.998v99.999v99.997v99.998v99.998 h99.999h99.998h99.998h99.997h0.001h99.997h100h99.998v-99.998V499.99v-99.997v-99.999v-99.998V99.998V0H600.99z M600.99,199.996 v99.998v99.999v99.997v99.998h-100h-99.997h-0.001h-99.997h-99.998h-99.998V499.99v-99.997v-99.999v-99.998V99.998h99.998h99.998 h99.997h0.001h99.997h100V199.996z"></path>
+</svg>
+<svg version="1.0" id="Ebene_1" x="223" y="1259" width="518" height="518" viewBox="0 0 699.988 699.988" enable-background="new 0 0 699.988 699.988" xml:space="preserve" shape-rendering="auto">
+<polygon fill="#fff" points="399.994,199.997 399.992,199.997 299.996,199.997 199.998,199.997 199.998,299.994 199.998,399.994 199.998,499.991 299.996,499.991 399.992,499.991 399.994,499.991 499.99,499.991 499.99,399.994 499.99,299.994 499.99,199.997 "></polygon>
+</svg>
+</svg>
\ No newline at end of file
diff --git a/cashtab/src/components/Common/CopyToClipboard.js b/cashtab/src/components/Common/CopyToClipboard.js
--- a/cashtab/src/components/Common/CopyToClipboard.js
+++ b/cashtab/src/components/Common/CopyToClipboard.js
@@ -5,6 +5,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { toast } from 'react-toastify';
+import styled from 'styled-components';
+
+const CopyWrapper = styled.div`
+ cursor: pointer;
+`;
const CopyToClipboard = ({
data,
@@ -13,7 +18,7 @@
children,
}) => {
return (
- <div
+ <CopyWrapper
onClick={() => {
if (navigator.clipboard) {
navigator.clipboard.writeText(data);
@@ -27,14 +32,14 @@
}}
>
{children}
- </div>
+ </CopyWrapper>
);
};
CopyToClipboard.propTypes = {
data: PropTypes.string,
showToast: PropTypes.bool,
- customMsg: PropTypes.oneOf(PropTypes.false, PropTypes.string),
+ customMsg: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
children: PropTypes.node,
};
diff --git a/cashtab/src/components/Common/CustomIcons.js b/cashtab/src/components/Common/CustomIcons.js
--- a/cashtab/src/components/Common/CustomIcons.js
+++ b/cashtab/src/components/Common/CustomIcons.js
@@ -9,7 +9,6 @@
DollarOutlined,
LoadingOutlined,
WalletOutlined,
- QrcodeOutlined,
SettingOutlined,
LockOutlined,
ContactsOutlined,
@@ -21,6 +20,7 @@
GithubOutlined,
} from '@ant-design/icons';
import { Image } from 'antd';
+import { ReactComponent as QRCode } from 'assets/qrcode.svg';
import { ReactComponent as Send } from 'assets/send.svg';
import { ReactComponent as Receive } from 'assets/receive.svg';
import { ReactComponent as Genesis } from 'assets/flask.svg';
@@ -73,9 +73,6 @@
export const ThemedWalletOutlined = styled(WalletOutlined)`
color: ${props => props.theme.icons.outlined} !important;
`;
-export const ThemedQrcodeOutlined = styled(QrcodeOutlined)`
- color: ${props => props.theme.walletBackground} !important;
-`;
export const ThemedSettingOutlined = styled(SettingOutlined)`
color: ${props => props.theme.icons.outlined} !important;
`;
@@ -245,6 +242,7 @@
);
export const ReceiveIcon = () => <Receive />;
+export const QRCodeIcon = () => <QRCode />;
export const GenesisIcon = () => <Genesis />;
export const UnparsedIcon = () => <Unparsed />;
export const HomeIcon = () => <Home />;
diff --git a/cashtab/src/components/Common/Inputs.js b/cashtab/src/components/Common/Inputs.js
new file mode 100644
--- /dev/null
+++ b/cashtab/src/components/Common/Inputs.js
@@ -0,0 +1,364 @@
+// Copyright (c) 2024 The Bitcoin developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import ScanQRCode from './ScanQRCode';
+import appConfig from 'config/app';
+
+const CashtabInputWrapper = styled.div`
+ box-sizing: border-box;
+ *,
+ *:before,
+ *:after {
+ box-sizing: inherit;
+ }
+ width: 100%;
+`;
+
+const InputRow = styled.div`
+ display: flex;
+ align-items: stretch;
+ input,
+ button,
+ select {
+ border: ${props =>
+ props.invalid
+ ? `1px solid ${props.theme.forms.error}`
+ : `1px solid ${props.theme.forms.border}`};
+ }
+ button,
+ select {
+ color: ${props =>
+ props.invalid ? props.theme.forms.error : props.theme.contrast};
+ }
+`;
+
+const CashtabInput = styled.input`
+ background-color: ${props => props.theme.forms.selectionBackground};
+ font-size: 18px;
+ padding: 16px 12px;
+ border-radius: 9px;
+ width: 100%;
+ color: ${props => props.theme.forms.text};
+ :focus-visible {
+ outline: none;
+ }
+`;
+
+const ModalInputField = styled(CashtabInput)`
+ background-color: transparent;
+ border: ${props =>
+ props.invalid
+ ? `1px solid ${props.theme.forms.error}`
+ : `1px solid ${props.theme.eCashBlue} !important`};
+`;
+
+const CashtabTextArea = styled.textarea`
+ background-color: ${props => props.theme.forms.selectionBackground};
+ font-size: 12px;
+ padding: 16px 12px;
+ border-radius: 9px;
+ width: 100%;
+ color: ${props => props.theme.forms.text};
+ :focus-visible {
+ outline: none;
+ }
+ height: 142px;
+`;
+
+const LeftInput = styled(CashtabInput)`
+ border-radius: 9px 0 0 9px;
+`;
+
+const OnMaxBtn = styled.button`
+ cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')};
+ color: ${props =>
+ props.invalid ? props.theme.forms.error : props.theme.contrast};
+ border-radius 0 9px 9px 0;
+ background-color: ${props => props.theme.forms.selectionBackground};
+ border-left: none !important;
+ font-size: 18px;
+ padding: 16px;
+`;
+
+const OnMaxBtnToken = styled(OnMaxBtn)`
+ padding: 12px;
+ min-width: 59px;
+`;
+
+const CurrencyDropdown = styled.select`
+ width: 100px;
+ cursor: pointer;
+ font-size: 18px;
+ padding: 6px;
+ color: ${props =>
+ props.invalid ? props.theme.forms.error : props.theme.contrast};
+ border-left: none !important;
+ background-color: ${props => props.theme.forms.selectionBackground};
+ :focus-visible {
+ outline: none;
+ }
+`;
+const CurrencyOption = styled.option`
+ text-align: left;
+ background-color: ${props => props.theme.forms.selectionBackground};
+ :hover {
+ background-color: ${props => props.theme.forms.selectionBackground};
+ }
+`;
+
+const ErrorMsg = styled.div`
+ font-size: 14px;
+ color: ${props => props.theme.forms.error};
+`;
+
+export const InputFlex = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ gap: 12px;
+`;
+
+export const Input = ({
+ placeholder = '',
+ name = '',
+ value = '',
+ handleInput,
+ error = false,
+}) => {
+ return (
+ <CashtabInputWrapper>
+ <InputRow invalid={typeof error === 'string'}>
+ <CashtabInput
+ name={name}
+ value={value}
+ placeholder={placeholder}
+ invalid={typeof error === 'string'}
+ onChange={e => handleInput(e)}
+ />
+ </InputRow>
+ <ErrorMsg>{typeof error === 'string' ? error : ''}</ErrorMsg>
+ </CashtabInputWrapper>
+ );
+};
+
+Input.propTypes = {
+ placeholder: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.string,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ handleInput: PropTypes.func,
+};
+
+export const ModalInput = ({
+ placeholder = '',
+ name = '',
+ value = '',
+ handleInput,
+ error = false,
+}) => {
+ return (
+ <CashtabInputWrapper>
+ <InputRow invalid={typeof error === 'string'}>
+ <ModalInputField
+ name={name}
+ value={value}
+ placeholder={placeholder}
+ invalid={typeof error === 'string'}
+ onChange={e => handleInput(e)}
+ />
+ </InputRow>
+ <ErrorMsg>{typeof error === 'string' ? error : ''}</ErrorMsg>
+ </CashtabInputWrapper>
+ );
+};
+
+ModalInput.propTypes = {
+ placeholder: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.string,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ handleInput: PropTypes.func,
+};
+
+export const TextArea = ({
+ placeholder = '',
+ name = '',
+ value = '',
+ handleInput,
+ error = false,
+}) => {
+ return (
+ <CashtabInputWrapper>
+ <CashtabTextArea
+ placeholder={placeholder}
+ name={name}
+ value={value}
+ onChange={e => handleInput(e)}
+ invalid={typeof error === 'string'}
+ />
+ <ErrorMsg>{typeof error === 'string' ? error : ''}</ErrorMsg>
+ </CashtabInputWrapper>
+ );
+};
+
+TextArea.propTypes = {
+ placeholder: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.string,
+ handleInput: PropTypes.func,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+};
+
+export const InputWithScanner = ({
+ placeholder = '',
+ name = '',
+ value = '',
+ disabled = false,
+ handleInput,
+ error = false,
+ loadWithScannerOpen,
+}) => {
+ return (
+ <CashtabInputWrapper>
+ <InputRow invalid={typeof error === 'string'}>
+ <LeftInput
+ name={name}
+ value={value}
+ disabled={disabled}
+ placeholder={placeholder}
+ invalid={typeof error === 'string'}
+ onChange={e => handleInput(e)}
+ />
+ <ScanQRCode
+ loadWithScannerOpen={loadWithScannerOpen}
+ onScan={result =>
+ handleInput({
+ target: {
+ name: 'address',
+ value: result,
+ },
+ })
+ }
+ />
+ </InputRow>
+ <ErrorMsg>{typeof error === 'string' ? error : ''}</ErrorMsg>
+ </CashtabInputWrapper>
+ );
+};
+
+InputWithScanner.propTypes = {
+ placeholder: PropTypes.string,
+ name: PropTypes.string,
+ value: PropTypes.string,
+ disabled: PropTypes.bool,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ handleInput: PropTypes.func,
+ loadWithScannerOpen: PropTypes.bool,
+};
+
+export const SendXecInput = ({
+ name = '',
+ value = 0,
+ inputDisabled = false,
+ selectValue = '',
+ selectDisabled = false,
+ fiatCode = 'USD',
+ error = false,
+ handleInput,
+ handleSelect,
+ handleOnMax,
+}) => {
+ return (
+ <CashtabInputWrapper>
+ <InputRow invalid={typeof error === 'string'}>
+ <LeftInput
+ placeholder="Amount"
+ type="number"
+ step="0.01"
+ name={name}
+ value={value}
+ onChange={e => handleInput(e)}
+ disabled={inputDisabled}
+ />
+ <CurrencyDropdown
+ data-testid="currency-select-dropdown"
+ value={selectValue}
+ onChange={e => handleSelect(e)}
+ disabled={selectDisabled}
+ >
+ <CurrencyOption data-testid="xec-option" value="XEC">
+ XEC
+ </CurrencyOption>
+ <CurrencyOption data-testid="fiat-option" value={fiatCode}>
+ {fiatCode}
+ </CurrencyOption>
+ </CurrencyDropdown>
+ <OnMaxBtn
+ onClick={handleOnMax}
+ // Disable the onMax button if the user has fiat selected
+ disabled={selectValue !== appConfig.ticker}
+ >
+ max
+ </OnMaxBtn>
+ </InputRow>
+ <ErrorMsg>{typeof error === 'string' ? error : ''}</ErrorMsg>
+ </CashtabInputWrapper>
+ );
+};
+
+SendXecInput.propTypes = {
+ name: PropTypes.string,
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ inputDisabled: PropTypes.bool,
+ selectValue: PropTypes.string,
+ selectDisabled: PropTypes.bool,
+ fiatCode: PropTypes.string,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ handleInput: PropTypes.func,
+ handleSelect: PropTypes.func,
+ handleOnMax: PropTypes.func,
+};
+
+export const SendTokenInput = ({
+ name = '',
+ placeholder = '',
+ value = 0,
+ inputDisabled = false,
+ decimals = 0,
+ error = false,
+ handleInput,
+ handleOnMax,
+}) => {
+ return (
+ <CashtabInputWrapper>
+ <InputRow invalid={typeof error === 'string'}>
+ <LeftInput
+ placeholder={placeholder}
+ type="number"
+ step={1 / 10 ** decimals}
+ name={name}
+ value={value}
+ onChange={e => handleInput(e)}
+ disabled={inputDisabled}
+ />
+ <OnMaxBtnToken onClick={handleOnMax}>max</OnMaxBtnToken>
+ </InputRow>
+ <ErrorMsg>{typeof error === 'string' ? error : ''}</ErrorMsg>
+ </CashtabInputWrapper>
+ );
+};
+
+SendTokenInput.propTypes = {
+ name: PropTypes.string,
+ placeholder: PropTypes.string,
+ value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ decimals: PropTypes.number,
+ inputDisabled: PropTypes.bool,
+ error: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+ handleInput: PropTypes.func,
+ handleOnMax: PropTypes.func,
+};
diff --git a/cashtab/src/components/Common/Modal.js b/cashtab/src/components/Common/Modal.js
--- a/cashtab/src/components/Common/Modal.js
+++ b/cashtab/src/components/Common/Modal.js
@@ -108,7 +108,7 @@
right: 5px;
top: 5px;
background: none;
- border: none;
+ border: none !important;
color: ${props => props.theme.contrast};
font-weight: bold;
cursor: pointer;
@@ -126,6 +126,7 @@
children,
width = 320,
height = 210,
+ showButtons = true,
}) => {
return (
<ModalContainer width={width} height={height}>
@@ -139,12 +140,16 @@
<ModalDescription>{description}</ModalDescription>
)}
{children}
- <ButtonHolder>
- <ModalConfirm onClick={handleOk}>OK</ModalConfirm>
- {showCancelButton && (
- <ModalCancel onClick={handleCancel}>Cancel</ModalCancel>
- )}
- </ButtonHolder>
+ {showButtons && (
+ <ButtonHolder>
+ <ModalConfirm onClick={handleOk}>OK</ModalConfirm>
+ {showCancelButton && (
+ <ModalCancel onClick={handleCancel}>
+ Cancel
+ </ModalCancel>
+ )}
+ </ButtonHolder>
+ )}
</ModalBody>
</ModalContainer>
);
@@ -153,11 +158,12 @@
Modal.propTypes = {
title: PropTypes.string,
description: PropTypes.string,
- handleOk: PropTypes.func.isRequired,
+ handleOk: PropTypes.func,
handleCancel: PropTypes.func.isRequired,
showCancelButton: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
+ showButtons: PropTypes.bool,
children: PropTypes.node,
};
diff --git a/cashtab/src/components/Common/ScanQRCode.js b/cashtab/src/components/Common/ScanQRCode.js
--- a/cashtab/src/components/Common/ScanQRCode.js
+++ b/cashtab/src/components/Common/ScanQRCode.js
@@ -4,8 +4,8 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
-import { Alert, Modal } from 'antd';
-import { ThemedQrcodeOutlined } from 'components/Common/CustomIcons';
+import Modal from 'components/Common/Modal';
+import { QRCodeIcon } from 'components/Common/CustomIcons';
import styled from 'styled-components';
import { BrowserQRCodeReader } from '@zxing/browser';
import {
@@ -14,31 +14,32 @@
ChecksumException,
} from '@zxing/library';
-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 StyledScanQRCode = styled.button`
+ cursor: pointer;
+ border-radius 0 9px 9px 0;
+ background-color: ${props => props.theme.forms.selectionBackground};
+ border-left: none !important;
+ padding: 0 12px;
`;
const QRPreview = styled.video`
width: 100%;
`;
+const Alert = styled.div`
+ background-color: #fff2f0;
+ border-radius: 12px;
+ color: red;
+ padding: 12px;
+`;
+
const ScanQRCode = ({
- loadWithCameraOpen,
+ loadWithScannerOpen,
onScan = () => null,
...otherProps
}) => {
const [codeReaderControls, setCodeReaderControls] = useState(null);
- const [visible, setVisible] = useState(loadWithCameraOpen);
+ const [visible, setVisible] = useState(loadWithScannerOpen);
const [error, setError] = useState(false);
const codeReader = new BrowserQRCodeReader();
@@ -127,39 +128,38 @@
{...otherProps}
onClick={() => setVisible(!visible)}
>
- <ThemedQrcodeOutlined />
+ <QRCodeIcon />
</StyledScanQRCode>
- <StyledModal
- data-testid="scan-qr-code-modal"
- title="Scan QR code"
- open={visible === true}
- onCancel={() => setVisible(false)}
- footer={null}
- >
- {visible ? (
- <div>
- {error ? (
- <>
- <Alert
- message="Error"
- description={`Error in QR scanner: ${error}.\n\nPlease ensure your camera is not in use.`}
- type="error"
- showIcon
- style={{ textAlign: 'left' }}
- />
- </>
- ) : (
- <QRPreview id="test-area-qr-code-webcam"></QRPreview>
- )}
- </div>
- ) : null}
- </StyledModal>
+ {visible === true && (
+ <Modal
+ handleCancel={() => setVisible(false)}
+ showButtons={false}
+ height={250}
+ >
+ {error ? (
+ <>
+ <Alert
+ message="Error"
+ description={`Error in QR scanner: ${error}.\n\nPlease ensure your camera is not in use.`}
+ type="error"
+ showIcon
+ style={{ textAlign: 'left' }}
+ />
+ </>
+ ) : (
+ <QRPreview
+ data-testid="video"
+ id="test-area-qr-code-webcam"
+ ></QRPreview>
+ )}
+ </Modal>
+ )}
</>
);
};
ScanQRCode.propTypes = {
- loadWithCameraOpen: PropTypes.bool,
+ loadWithScannerOpen: PropTypes.bool,
onScan: PropTypes.func,
};
diff --git a/cashtab/src/components/Common/__tests__/ScanQRCode.test.js b/cashtab/src/components/Common/__tests__/ScanQRCode.test.js
--- a/cashtab/src/components/Common/__tests__/ScanQRCode.test.js
+++ b/cashtab/src/components/Common/__tests__/ScanQRCode.test.js
@@ -7,6 +7,8 @@
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';
import ScanQRCode from 'components/Common/ScanQRCode';
+import { ThemeProvider } from 'styled-components';
+import { theme } from 'assets/styles/theme';
// https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function
Object.defineProperty(window, 'matchMedia', {
@@ -41,52 +43,44 @@
describe('<ScanQRCode />', () => {
it('Renders the modal on load if loadWithCameraOpen is true', async () => {
const user = userEvent.setup();
- render(<ScanQRCode loadWithCameraOpen={true} />);
+ render(
+ <ThemeProvider theme={theme}>
+ <ScanQRCode loadWithScannerOpen={true} />
+ </ThemeProvider>,
+ );
// Button to open modal is rendered
const StartScanningButton = screen.queryByTestId('scan-qr-code');
expect(StartScanningButton).toBeInTheDocument();
- // The modal root component is rendered
- expect(screen.getByTestId('scan-qr-code-modal')).toBeInTheDocument();
-
- // The modal is displayed
- expect(screen.getByTestId('scan-qr-code-modal').firstChild).toHaveStyle(
- `display: block`,
- );
+ // The video component inside the modal is rendered
+ expect(await screen.findByTestId('video')).toBeInTheDocument();
// Click the close button
- await user.click(
- screen.getByRole('button', { class: /ant-modal-close/i }),
- );
+ await user.click(screen.getByRole('button', { name: /X/ }));
// Expect modal to be closed
- expect(
- screen.queryByTestId('scan-qr-code-modal').firstChild,
- ).toHaveStyle(`display: none`);
+ expect(screen.queryByTestId('video')).not.toBeInTheDocument();
});
it('Does not render the modal on load if loadWithCameraOpen is false', async () => {
const user = userEvent.setup();
- render(<ScanQRCode loadWithCameraOpen={false} />);
+ render(
+ <ThemeProvider theme={theme}>
+ <ScanQRCode loadWithScannerOpen={false} />
+ </ThemeProvider>,
+ );
// Button to open modal is rendered
const StartScanningButton = screen.queryByTestId('scan-qr-code');
expect(StartScanningButton).toBeInTheDocument();
// The modal root component is not rendered
- expect(
- screen.queryByTestId('scan-qr-code-modal'),
- ).not.toBeInTheDocument();
+ expect(screen.queryByTestId('video')).not.toBeInTheDocument();
// Click the open modal button
await user.click(StartScanningButton);
- // The modal root component is rendered
- expect(screen.getByTestId('scan-qr-code-modal')).toBeInTheDocument();
-
- // Expect modal to be open
- expect(screen.getByTestId('scan-qr-code-modal').firstChild).toHaveStyle(
- `display: block`,
- );
+ // The modal is rendered
+ expect(await screen.findByTestId('video')).toBeInTheDocument();
});
});
diff --git a/cashtab/src/components/Configure/Configure.js b/cashtab/src/components/Configure/Configure.js
--- a/cashtab/src/components/Configure/Configure.js
+++ b/cashtab/src/components/Configure/Configure.js
@@ -5,17 +5,11 @@
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { useLocation, Link } from 'react-router-dom';
-import { Collapse, Form, Input, Alert, Switch, Tooltip, Checkbox } from 'antd';
+import { Collapse, Form, Alert, Switch, Tooltip, Checkbox } from 'antd';
import { Row, Col } from 'antd';
-import {
- WalletFilled,
- LockOutlined,
- CheckOutlined,
- CloseOutlined,
- LockFilled,
-} from '@ant-design/icons';
+import { CheckOutlined, CloseOutlined, LockFilled } from '@ant-design/icons';
import { WalletContext } from 'wallet/context';
-import { SidePaddingCtn, FormLabel } from 'components/Common/Atoms';
+import { SidePaddingCtn } from 'components/Common/Atoms';
import { StyledCollapse } from 'components/Common/StyledCollapse';
import {
AntdFormWrapper,
@@ -64,6 +58,7 @@
} from 'wallet';
import CustomModal from 'components/Common/Modal';
import { toast } from 'react-toastify';
+import { Input, ModalInput, InputFlex } from 'components/Common/Inputs';
const { Panel } = Collapse;
@@ -480,7 +475,8 @@
setConfirmationOfWalletToBeDeleted,
] = useState('');
const [newWalletNameIsValid, setNewWalletNameIsValid] = useState(null);
- const [walletDeleteValid, setWalletDeleteValid] = useState(null);
+ const [walletDeleteConfirmationError, setWalletDeleteConfirmationError] =
+ useState(false);
const [seedInput, openSeedInput] = useState(false);
const [revealSeed, setRevealSeed] = useState(false);
const [showTranslationWarning, setShowTranslationWarning] = useState(false);
@@ -519,7 +515,8 @@
const [showDeleteContactModal, setShowDeleteContactModal] = useState(false);
const [contactAddressToDelete, setContactAddressToDelete] = useState(null);
- const [contactDeleteValid, setContactDeleteValid] = useState(null);
+ const [contactDeleteConfirmationError, setContactDeleteConfirmationError] =
+ useState(false);
const [
confirmationOfContactToBeDeleted,
setConfirmationOfContactToBeDeleted,
@@ -529,10 +526,9 @@
useState(false);
const [manualContactName, setManualContactName] = useState('');
const [manualContactAddress, setManualContactAddress] = useState('');
- const [manualContactNameIsValid, setManualContactNameIsValid] =
- useState(null);
- const [manualContactAddressIsValid, setManualContactAddressIsValid] =
- useState(null);
+ const [manualContactNameError, setManualContactNameError] = useState(false);
+ const [manualContactAddressError, setManualContactAddressError] =
+ useState(false);
const handleContactListRouting = async () => {
// if this was routed from Home screen's Add to Contact link
@@ -759,7 +755,7 @@
* @param {object} walletToBeDeleted
*/
const deleteWallet = async walletToBeDeleted => {
- if (!walletDeleteValid && walletDeleteValid !== null) {
+ if (walletDeleteConfirmationError) {
return;
}
@@ -767,7 +763,9 @@
confirmationOfWalletToBeDeleted !==
`delete ${walletToBeDeleted.name}`
) {
- setWalletDeleteValid(false);
+ setWalletDeleteConfirmationError(
+ 'Your confirmation phrase must match exactly',
+ );
return;
}
@@ -817,9 +815,11 @@
const { value } = e.target;
if (value && value === `delete ${walletToBeDeleted.name}`) {
- setWalletDeleteValid(true);
+ setWalletDeleteConfirmationError(false);
} else {
- setWalletDeleteValid(false);
+ setWalletDeleteConfirmationError(
+ 'Your confirmation phrase must match exactly',
+ );
}
setConfirmationOfWalletToBeDeleted(value);
};
@@ -965,26 +965,24 @@
};
const handleDeleteContactModalOk = () => {
- if (
- !contactDeleteValid ||
- contactDeleteValid === null ||
- !contactAddressToDelete
- ) {
+ if (contactDeleteConfirmationError || !contactAddressToDelete) {
return;
}
setShowDeleteContactModal(false);
deleteContactByAddress(contactAddressToDelete);
// Reset validation input
- setConfirmationOfContactToBeDeleted(null);
+ setConfirmationOfContactToBeDeleted(false);
};
const handleContactToDeleteInput = e => {
const { value } = e.target;
const contactName = getContactNameByAddress(contactAddressToDelete);
if (value && value === 'delete ' + contactName) {
- setContactDeleteValid(true);
+ setContactDeleteConfirmationError(false);
} else {
- setContactDeleteValid(false);
+ setContactDeleteConfirmationError(
+ `Input must exactly match "delete ${contactName}"`,
+ );
}
setConfirmationOfContactToBeDeleted(value);
};
@@ -1062,7 +1060,7 @@
const handleManualAddContactModalOk = async () => {
// if either inputs are invalid then go no further
- if (!manualContactNameIsValid || !manualContactAddressIsValid) {
+ if (manualContactNameError || manualContactAddressError) {
return;
}
@@ -1102,9 +1100,11 @@
const { value } = e.target;
if (value && value.length && value.length < 24) {
- setManualContactNameIsValid(true);
+ setManualContactNameError(false);
} else {
- setManualContactNameIsValid(false);
+ setManualContactNameError(
+ 'Contact name must be a string between 1 and 24 characters long',
+ );
}
setManualContactName(value);
};
@@ -1112,7 +1112,13 @@
const handleManualContactAddressInput = async e => {
const { value } = e.target;
setManualContactAddress(value);
- setManualContactAddressIsValid(await isValidRecipient(value));
+ const validContactAddress = await isValidRecipient(value);
+
+ setManualContactAddressError(
+ validContactAddress === true
+ ? false
+ : 'Invalid eCash address or alias',
+ );
};
return (
@@ -1126,75 +1132,39 @@
handleCancel={() =>
handleAddSavedWalletAsContactCancel()
}
+ showCancelButton
/>
)}
{showManualAddContactModal && (
<CustomModal
data-testid="confirm-add-contact-modal"
- height={308}
+ height={305}
title={`Add new contact`}
handleOk={() => handleManualAddContactModalOk()}
handleCancel={() => handleManualAddContactModalCancel()}
showCancelButton
>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <FormLabel>Name:</FormLabel>
- <Form.Item
- validateStatus={
- manualContactNameIsValid === null ||
- manualContactNameIsValid
- ? ''
- : 'error'
- }
- help={
- manualContactNameIsValid === null ||
- manualContactNameIsValid
- ? ''
- : 'Contact name must be a string between 1 and 24 characters long'
- }
- >
- <Input
- placeholder="Enter new contact name"
- name="manualContactName"
- value={manualContactName}
- onChange={e =>
- handleManualContactNameInput(e)
- }
- />
- </Form.Item>
- <FormLabel>eCash Address:</FormLabel>
- <Form.Item
- validateStatus={
- manualContactAddressIsValid === null ||
- manualContactAddressIsValid
- ? ''
- : 'error'
- }
- help={
- manualContactAddressIsValid === null ||
- manualContactAddressIsValid
- ? ''
- : 'Invalid eCash address or alias'
- }
- >
- <Input
- placeholder="Enter new eCash address or alias"
- name="manualContactAddress"
- value={manualContactAddress}
- onChange={e =>
- handleManualContactAddressInput(e)
- }
- />
- </Form.Item>
- </Form>
- </AntdFormWrapper>
+ <InputFlex>
+ <ModalInput
+ placeholder="Enter new contact name"
+ name="manualContactName"
+ error={manualContactNameError}
+ value={manualContactName}
+ handleInput={handleManualContactNameInput}
+ />
+ <ModalInput
+ placeholder="Enter new eCash address or alias"
+ name="manualContactAddress"
+ value={manualContactAddress}
+ error={manualContactAddressError}
+ handleInput={handleManualContactAddressInput}
+ />
+ </InputFlex>
</CustomModal>
)}
{showDeleteContactModal && (
<CustomModal
- data-testid="confirm-delete-contact-modal"
- height={242}
+ height={290}
title="Confirm Delete Contact"
description={`Delete
"${getContactNameByAddress(
@@ -1204,82 +1174,42 @@
handleCancel={() => handleDeleteContactModalCancel()}
showCancelButton
>
- <p></p>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- contactDeleteValid === null ||
- contactDeleteValid
- ? ''
- : 'error'
- }
- help={
- contactDeleteValid === null ||
- contactDeleteValid
- ? ''
- : 'Your confirmation phrase must match exactly'
- }
- >
- <Input
- data-testid="confirm-delete-contact"
- prefix={<ThemedContactsOutlined />}
- placeholder={`Type "delete ${getContactNameByAddress(
- contactAddressToDelete,
- )}" to confirm`}
- name="contactToBeDeletedInput"
- value={confirmationOfContactToBeDeleted}
- onChange={e =>
- handleContactToDeleteInput(e)
- }
- />
- </Form.Item>
- </Form>
- </AntdFormWrapper>
+ <ModalInput
+ placeholder={`Type "delete ${getContactNameByAddress(
+ contactAddressToDelete,
+ )}" to confirm`}
+ name="contactToBeDeletedInput"
+ value={confirmationOfContactToBeDeleted}
+ handleInput={handleContactToDeleteInput}
+ error={contactDeleteConfirmationError}
+ />
</CustomModal>
)}
{showRenameContactModal && (
<CustomModal
- height={242}
+ height={290}
title={`Rename contact?`}
description={`Editing name for contact ${contactToBeRenamed.name}`}
handleOk={() => handleRenameContactModalOk()}
handleCancel={() => handleRenameContactCancel()}
showCancelButton
>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- newContactNameIsValid === null ||
- newContactNameIsValid
- ? ''
- : 'error'
- }
- help={
- newContactNameIsValid === null ||
- newContactNameIsValid
- ? ''
- : 'Contact name must be a string between 1 and 24 characters long'
- }
- >
- <Input
- prefix={<WalletFilled />}
- placeholder="Enter new contact name"
- name="newContactName"
- value={confirmationOfContactToBeRenamed}
- onChange={e =>
- handleContactNameInput(e)
- }
- />
- </Form.Item>
- </Form>
- </AntdFormWrapper>
+ <ModalInput
+ placeholder="Enter new contact name"
+ name="newContactName"
+ value={confirmationOfContactToBeRenamed}
+ error={
+ newContactNameIsValid
+ ? false
+ : 'Contact name must be a string between 1 and 24 characters long'
+ }
+ handleInput={handleContactNameInput}
+ />
</CustomModal>
)}
{walletToBeRenamed !== null && showRenameWalletModal && (
<CustomModal
- height={260}
+ height={290}
title={`Rename Wallet?`}
description={`Editing name for wallet "${walletToBeRenamed.name}"`}
handleOk={() =>
@@ -1288,71 +1218,35 @@
handleCancel={() => cancelRenameWallet()}
showCancelButton
>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- newWalletNameIsValid === null ||
- newWalletNameIsValid
- ? ''
- : 'error'
- }
- help={
- newWalletNameIsValid === null ||
- newWalletNameIsValid
- ? ''
- : 'Wallet name must be a string between 1 and 24 characters long'
- }
- >
- <Input
- prefix={<WalletFilled />}
- placeholder="Enter new wallet name"
- name="newName"
- value={newWalletName}
- onChange={e => handleWalletNameInput(e)}
- />
- </Form.Item>
- </Form>
- </AntdFormWrapper>
+ <ModalInput
+ placeholder="Enter new wallet name"
+ name="newName"
+ value={newWalletName}
+ error={
+ newWalletNameIsValid
+ ? false
+ : 'Wallet name must be a string between 1 and 24 characters long'
+ }
+ handleInput={handleWalletNameInput}
+ />
</CustomModal>
)}
{walletToBeDeleted !== null && showDeleteWalletModal && (
<CustomModal
- height={311}
+ height={340}
title={`Delete Wallet?`}
description={`Delete wallet "${walletToBeDeleted.name}"?. This cannot be undone. Make sure you have backed up your wallet.`}
handleOk={() => deleteWallet(walletToBeDeleted)}
handleCancel={() => cancelDeleteWallet()}
showCancelButton
>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- walletDeleteValid === null ||
- walletDeleteValid
- ? ''
- : 'error'
- }
- help={
- walletDeleteValid === null ||
- walletDeleteValid
- ? ''
- : 'Your confirmation phrase must match exactly'
- }
- >
- <Input
- prefix={<WalletFilled />}
- placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`}
- name="walletToBeDeletedInput"
- value={confirmationOfWalletToBeDeleted}
- onChange={e =>
- handleWalletToDeleteInput(e)
- }
- />
- </Form.Item>
- </Form>
- </AntdFormWrapper>
+ <ModalInput
+ placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`}
+ name="walletToBeDeletedInput"
+ value={confirmationOfWalletToBeDeleted}
+ handleInput={handleWalletToDeleteInput}
+ error={walletDeleteConfirmationError}
+ />
</CustomModal>
)}
<h2>
@@ -1435,54 +1329,34 @@
Import Wallet
</SecondaryButton>
{seedInput && (
- <>
+ <InputFlex>
<p style={{ color: '#fff' }}>
Copy and paste your mnemonic seed phrase
below to import an existing wallet
</p>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- isValidMnemonic === null ||
- isValidMnemonic
- ? ''
- : 'error'
- }
- help={
- isValidMnemonic === null ||
- isValidMnemonic
- ? ''
- : 'Valid mnemonic seed phrase required'
- }
- >
- <Input
- prefix={<LockOutlined />}
- type="email"
- placeholder="mnemonic (seed phrase)"
- name="mnemonic"
- value={formData.mnemonic}
- autoComplete="off"
- onChange={e =>
- handleImportMnemonicInput(e)
- }
- required
- title=""
- />
- </Form.Item>
- <SecondaryButton
- disabled={isValidMnemonic !== true}
- onClick={() =>
- importNewWallet(
- formData.mnemonic,
- )
- }
- >
- Import
- </SecondaryButton>
- </Form>
- </AntdFormWrapper>
- </>
+
+ <Input
+ type="email"
+ placeholder="mnemonic (seed phrase)"
+ name="mnemonic"
+ error={
+ isValidMnemonic
+ ? false
+ : 'Valid mnemonic seed phrase required'
+ }
+ value={formData.mnemonic}
+ autoComplete="off"
+ handleInput={handleImportMnemonicInput}
+ />
+ <SecondaryButton
+ disabled={isValidMnemonic !== true}
+ onClick={() =>
+ importNewWallet(formData.mnemonic)
+ }
+ >
+ Import
+ </SecondaryButton>
+ </InputFlex>
)}
</>
)}
diff --git a/cashtab/src/components/OnBoarding/OnBoarding.js b/cashtab/src/components/OnBoarding/OnBoarding.js
--- a/cashtab/src/components/OnBoarding/OnBoarding.js
+++ b/cashtab/src/components/OnBoarding/OnBoarding.js
@@ -5,9 +5,7 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { WalletContext } from 'wallet/context';
-import { Input, Form } from 'antd';
-import { AntdFormWrapper } from 'components/Common/EnhancedInputs';
-import { LockOutlined } from '@ant-design/icons';
+import { Input, InputFlex } from 'components/Common/Inputs';
import PrimaryButton, {
SecondaryButton,
} from 'components/Common/PrimaryButton';
@@ -104,40 +102,26 @@
Import Wallet
</SecondaryButton>
{seedInput && (
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- !isValidMnemonic && formData.mnemonic.length > 0
- ? 'error'
- : ''
- }
- help={
- !isValidMnemonic && formData.mnemonic.length > 0
- ? 'invalid 12-word mnemonic'
- : ''
- }
- >
- <Input
- prefix={<LockOutlined />}
- type="email"
- placeholder="mnemonic (seed phrase)"
- name="mnemonic"
- autoComplete="off"
- onChange={e => handleChange(e)}
- required
- title=""
- />
- </Form.Item>
-
- <SecondaryButton
- disabled={!isValidMnemonic}
- onClick={() => importWallet()}
- >
- Import
- </SecondaryButton>
- </Form>
- </AntdFormWrapper>
+ <InputFlex>
+ <Input
+ type="email"
+ placeholder="mnemonic (seed phrase)"
+ name="mnemonic"
+ value={formData.mnemonic}
+ error={
+ isValidMnemonic ? false : 'invalid 12-word mnemonic'
+ }
+ autoComplete="off"
+ handleInput={handleChange}
+ />
+
+ <SecondaryButton
+ disabled={!isValidMnemonic}
+ onClick={() => importWallet()}
+ >
+ Import
+ </SecondaryButton>
+ </InputFlex>
)}
</WelcomeCtn>
);
diff --git a/cashtab/src/components/Send/SendToken.js b/cashtab/src/components/Send/SendToken.js
--- a/cashtab/src/components/Send/SendToken.js
+++ b/cashtab/src/components/Send/SendToken.js
@@ -5,14 +5,10 @@
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import { WalletContext } from 'wallet/context';
-import { Form, message, Row, Col, Descriptions, Button, Input } from 'antd';
-import { SecondaryButton } from 'components/Common/PrimaryButton';
-import { FireTwoTone } from '@ant-design/icons';
-import {
- DestinationAmount,
- DestinationAddressSingle,
- AntdFormWrapper,
-} from 'components/Common/EnhancedInputs';
+import { message, Button } from 'antd';
+import PrimaryButton, {
+ SecondaryButton,
+} from 'components/Common/PrimaryButton';
import { SidePaddingCtn, TxLink } from 'components/Common/Atoms';
import BalanceHeaderToken from 'components/Common/BalanceHeaderToken';
import { useNavigate } from 'react-router-dom';
@@ -23,9 +19,8 @@
import { isValidEtokenBurnAmount, parseAddressInput } from 'validation';
import { getTokenStats } from 'chronik';
import { formatDate } from 'utils/formatting';
-import styled, { css } from 'styled-components';
+import styled from 'styled-components';
import TokenIcon from 'components/Etokens/TokenIcon';
-import { token as tokenConfig } from 'config/token';
import { explorer } from 'config/explorer';
import { queryAliasServer } from 'alias';
import aliasSettings from 'config/alias';
@@ -41,18 +36,31 @@
import { hasEnoughToken } from 'wallet';
import Modal from 'components/Common/Modal';
import { toast } from 'react-toastify';
-
-const AntdDescriptionsCss = css`
- .ant-descriptions-item-label,
- .ant-input-number,
- .ant-descriptions-item-content {
- background-color: ${props => props.theme.contrast} !important;
- color: ${props => props.theme.dropdownText};
- }
- .ant-descriptions-title {
- color: ${props => props.theme.lightWhite};
- }
+import {
+ InputWithScanner,
+ SendTokenInput,
+ ModalInput,
+ InputFlex,
+} from 'components/Common/Inputs';
+import CopyToClipboard from 'components/Common/CopyToClipboard';
+import { ThemedCopySolid } from 'components/Common/CustomIcons';
+
+const TokenStatsTable = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ color: ${props => props.theme.contrast};
+`;
+const TokenStatsRow = styled.div`
+ width: 100%;
+ display: flex;
+ text-align: center;
+ justify-content: center;
+ gap: 3px;
`;
+const TokenStatsCol = styled.div``;
const TokenSentLink = styled.a`
color: ${props => props.theme.walletBackground};
@@ -65,37 +73,6 @@
padding-left: 1px;
white-space: nowrap;
`;
-const AntdDescriptionsWrapper = styled.div`
- ${AntdDescriptionsCss}
-`;
-const AirdropButton = styled.div`
- text-align: center;
- width: 100%;
- padding: 10px;
- border-radius: 5px;
- background: ${props => props.theme.sentMessage};
- a {
- color: ${props => props.theme.darkBlue};
- margin: 0;
- font-size: 11px;
- border: 1px solid ${props => props.theme.darkBlue};
- 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 SendToken = () => {
let navigate = useNavigate();
@@ -114,10 +91,9 @@
const [tokenStats, setTokenStats] = useState(null);
const [sendTokenAddressError, setSendTokenAddressError] = useState(false);
const [sendTokenAmountError, setSendTokenAmountError] = useState(false);
- const [eTokenBurnAmount, setETokenBurnAmount] = useState(new BN(1));
const [showConfirmBurnEtoken, setShowConfirmBurnEtoken] = useState(false);
const [burnTokenAmountError, setBurnTokenAmountError] = useState(false);
- const [burnConfirmationValid, setBurnConfirmationValid] = useState(null);
+ const [burnConfirmationError, setBurnConfirmationError] = useState(false);
const [confirmationOfEtokenToBeBurnt, setConfirmationOfEtokenToBeBurnt] =
useState('');
const [aliasInputAddress, setAliasInputAddress] = useState(false);
@@ -128,8 +104,9 @@
const [isModalVisible, setIsModalVisible] = useState(false);
const [formData, setFormData] = useState({
- value: '',
+ amount: '',
address: '',
+ burnAmount: '',
});
const userLocale = getUserLocale(navigator);
@@ -159,21 +136,22 @@
// Clears address and amount fields following a send token notification
const clearInputForms = () => {
setFormData({
- value: '',
+ amount: '',
address: '',
+ burnAmount: '',
});
setAliasInputAddress(false); // clear alias address preview
};
- async function submit() {
+ async function sendToken() {
setFormData({
...formData,
});
if (
!formData.address ||
- !formData.value ||
- Number(formData.value <= 0) ||
+ !formData.amount ||
+ Number(formData.amount <= 0) ||
sendTokenAmountError
) {
return;
@@ -183,7 +161,7 @@
// SLPA token IDs
Event('SendToken.js', 'Send', tokenId);
- const { address, value } = formData;
+ const { address, amount } = formData;
let cleanAddress;
// check state on whether this is an alias or ecash address
@@ -199,7 +177,7 @@
const tokenInputInfo = getSendTokenInputs(
wallet.state.slpUtxos,
tokenId,
- value,
+ amount,
token.info.decimals,
);
@@ -344,11 +322,11 @@
// Clear this error before updating field
setSendTokenAmountError(false);
try {
- let value = token.balance;
+ let amount = token.balance;
setFormData({
...formData,
- value,
+ amount,
});
} catch (err) {
console.log(`Error in onMax:`);
@@ -364,13 +342,13 @@
setIsModalVisible(settings.sendModal);
} else {
// if the user does not have the send confirmation enabled in settings then send directly
- submit();
+ sendToken();
}
};
const handleOk = () => {
setIsModalVisible(false);
- submit();
+ sendToken();
};
const handleCancel = () => {
@@ -378,35 +356,36 @@
};
const handleEtokenBurnAmountChange = e => {
- const { value } = e.target;
- const burnAmount = new BN(value);
- setETokenBurnAmount(burnAmount);
+ console.log(`handleEtokenBurnAmountChange`);
+ const { name, value } = e.target;
+ console.log(`name`, name);
+ console.log(`value`, value);
let error = false;
- if (!isValidEtokenBurnAmount(burnAmount, token.balance)) {
+ if (!isValidEtokenBurnAmount(new BN(value), token.balance)) {
error = 'Burn amount must be between 1 and ' + token.balance;
}
setBurnTokenAmountError(error);
+
+ setFormData(p => ({
+ ...p,
+ [name]: value,
+ }));
};
const onMaxBurn = () => {
- setETokenBurnAmount(token.balance);
-
// trigger validation on the inserted max value
handleEtokenBurnAmountChange({
target: {
+ name: 'burnAmount',
value: token.balance,
},
});
};
async function burn() {
- if (
- !burnConfirmationValid ||
- burnConfirmationValid === null ||
- !eTokenBurnAmount
- ) {
+ if (burnConfirmationError || formData.burnAmount === '') {
return;
}
@@ -418,7 +397,7 @@
const tokenInputInfo = getSendTokenInputs(
wallet.state.slpUtxos,
tokenId,
- eTokenBurnAmount,
+ formData.burnAmount,
token.info.decimals,
);
@@ -470,9 +449,11 @@
const { value } = e.target;
if (value && value === `burn ${token.info.tokenTicker}`) {
- setBurnConfirmationValid(true);
+ setBurnConfirmationError(false);
} else {
- setBurnConfirmationValid(false);
+ setBurnConfirmationError(
+ `Input must exactly match "burn ${token.info.tokenTicker}"`,
+ );
}
setConfirmationOfEtokenToBeBurnt(value);
};
@@ -488,7 +469,7 @@
{isModalVisible && (
<Modal
title="Confirm Send"
- description={`Send ${formData.value}${' '}
+ description={`Send ${formData.amount}${' '}
${token.info.tokenTicker} to ${formData.address}?`}
handleOk={handleOk}
handleCancel={handleCancel}
@@ -497,7 +478,7 @@
<p>
{token && token.info && formData
? `Are you sure you want to send ${
- formData.value
+ formData.amount
}${' '}
${token.info.tokenTicker} to ${formData.address}?`
: ''}
@@ -510,44 +491,19 @@
{showConfirmBurnEtoken && (
<Modal
title={`Confirm ${token.info.tokenTicker} burn`}
- description={`Burn ${eTokenBurnAmount.toString()} ${
- token.info.tokenTicker
- }?`}
+ description={`Burn ${formData.burnAmount} ${token.info.tokenTicker}?`}
handleOk={burn}
handleCancel={() => setShowConfirmBurnEtoken(false)}
showCancelButton
- height={230}
+ height={250}
>
- <AntdFormWrapper>
- <Form style={{ width: 'auto' }}>
- <Form.Item
- validateStatus={
- burnConfirmationValid === null ||
- burnConfirmationValid
- ? ''
- : 'error'
- }
- help={
- burnConfirmationValid === null ||
- burnConfirmationValid
- ? ''
- : 'Your confirmation phrase must match exactly'
- }
- >
- <Input
- prefix={<FireTwoTone />}
- placeholder={`Type "burn ${token.info.tokenTicker}" to confirm`}
- name="etokenToBeBurnt"
- value={
- confirmationOfEtokenToBeBurnt
- }
- onChange={e =>
- handleBurnConfirmationInput(e)
- }
- />
- </Form.Item>
- </Form>
- </AntdFormWrapper>
+ <ModalInput
+ placeholder={`Type "burn ${token.info.tokenTicker}" to confirm`}
+ name="etokenToBeBurnt"
+ value={confirmationOfEtokenToBeBurnt}
+ error={burnConfirmationError}
+ handleInput={handleBurnConfirmationInput}
+ />
</Modal>
)}
<BalanceHeaderToken
@@ -555,207 +511,141 @@
ticker={token.info.tokenTicker}
tokenDecimals={token.info.decimals}
/>
- <Row type="flex">
- <Col span={24}>
- <Form
- style={{
- width: 'auto',
+ <TokenStatsTable
+ title={`Token info for "${token.info.tokenName}"`}
+ >
+ <TokenStatsRow>
+ <TokenStatsCol colSpan={2}>
+ <CopyToClipboard data={token.tokenId} showToast>
+ <TokenIcon size={128} tokenId={tokenId} />
+ </CopyToClipboard>
+ </TokenStatsCol>
+ </TokenStatsRow>
+ <TokenStatsRow>
+ <TokenStatsCol>
+ Token Id: {token.tokenId.slice(0, 3)}...
+ {token.tokenId.slice(-3)}
+ </TokenStatsCol>
+ <TokenStatsCol>
+ <CopyToClipboard data={token.tokenId} showToast>
+ <ThemedCopySolid />
+ </CopyToClipboard>
+ </TokenStatsCol>
+ </TokenStatsRow>
+ <TokenStatsRow>
+ <TokenStatsCol>
+ {token.info.decimals} decimal places
+ </TokenStatsCol>
+ </TokenStatsRow>
+
+ {tokenStats && (
+ <>
+ <TokenStatsRow>
+ {tokenStats.genesisInfo.url}
+ </TokenStatsRow>
+ <TokenStatsRow>
+ Minted{' '}
+ {tokenStats.block &&
+ tokenStats.block.timestamp !== null
+ ? formatDate(
+ tokenStats.block.timestamp,
+ navigator.language,
+ )
+ : 'Just now (Genesis tx confirming)'}
+ </TokenStatsRow>
+ </>
+ )}
+ </TokenStatsTable>
+ <InputWithScanner
+ placeholder={
+ aliasSettings.aliasEnabled
+ ? `Address or Alias`
+ : `Address`
+ }
+ name="address"
+ value={formData.address}
+ handleInput={handleTokenAddressChange}
+ error={sendTokenAddressError}
+ loadWithScannerOpen={openWithScanner}
+ />
+ <AliasAddressPreviewLabel>
+ <TxLink
+ key={aliasInputAddress}
+ href={`${explorer.blockExplorerUrl}/address/${aliasInputAddress}`}
+ target="_blank"
+ rel="noreferrer"
+ >
+ {aliasInputAddress &&
+ `${aliasInputAddress.slice(
+ 0,
+ 10,
+ )}...${aliasInputAddress.slice(-5)}`}
+ </TxLink>
+ </AliasAddressPreviewLabel>
+ <br />
+ <SendTokenInput
+ name="amount"
+ value={formData.amount}
+ error={sendTokenAmountError}
+ placeholder="Amount"
+ decimals={token.info.decimals}
+ handleInput={handleSlpAmountChange}
+ handleOnMax={onMax}
+ />
+
+ <SecondaryButton
+ style={{ marginTop: '24px' }}
+ disabled={
+ apiError ||
+ sendTokenAmountError ||
+ sendTokenAddressError
+ }
+ onClick={() => checkForConfirmationBeforeSendEtoken()}
+ >
+ Send {token.info.tokenName}
+ </SecondaryButton>
+
+ {apiError && <ApiError />}
+
+ <TokenStatsTable
+ title={`Token info for "${token.info.tokenName}"`}
+ >
+ <TokenStatsRow>
+ <Link
+ style={{ width: '100%' }}
+ to="/airdrop"
+ state={{
+ airdropEtokenId: token.tokenId,
}}
>
- <DestinationAddressSingle
- loadWithCameraOpen={openWithScanner}
- validateStatus={
- sendTokenAddressError ? 'error' : ''
- }
- help={
- sendTokenAddressError
- ? sendTokenAddressError
- : ''
- }
- onScan={result =>
- handleTokenAddressChange({
- target: {
- name: 'address',
- value: result,
- },
- })
- }
- inputProps={{
- placeholder: aliasSettings.aliasEnabled
- ? `Address or Alias`
- : `Address`,
- name: 'address',
- onChange: e =>
- handleTokenAddressChange(e),
- required: true,
- value: formData.address,
- }}
- style={{ marginBottom: '0px' }}
- />
- <AliasAddressPreviewLabel>
- <TxLink
- key={aliasInputAddress}
- href={`${explorer.blockExplorerUrl}/address/${aliasInputAddress}`}
- target="_blank"
- rel="noreferrer"
- >
- {aliasInputAddress &&
- `${aliasInputAddress.slice(
- 0,
- 10,
- )}...${aliasInputAddress.slice(
- -5,
- )}`}
- </TxLink>
- </AliasAddressPreviewLabel>
- <br />
- <DestinationAmount
- validateStatus={
- sendTokenAmountError ? 'error' : ''
- }
- help={
- sendTokenAmountError
- ? sendTokenAmountError
- : ''
- }
- onMax={onMax}
- inputProps={{
- name: 'value',
- step: 1 / 10 ** token.info.decimals,
- placeholder: 'Amount',
- prefix: (
- <img
- src={`${tokenConfig.tokenIconsUrl}/32/${tokenId}.png`}
- width={16}
- height={16}
- />
- ),
- suffix: token.info.tokenTicker,
- onChange: e => handleSlpAmountChange(e),
- required: true,
- value: formData.value,
- }}
+ <PrimaryButton style={{ marginTop: '12px' }}>
+ Airdrop
+ </PrimaryButton>
+ </Link>
+ </TokenStatsRow>
+ <TokenStatsRow>
+ <InputFlex>
+ <SendTokenInput
+ name="burnAmount"
+ value={formData.burnAmount}
+ error={burnTokenAmountError}
+ placeholder="Burn Amount"
+ decimals={token.info.decimals}
+ handleInput={handleEtokenBurnAmountChange}
+ handleOnMax={onMaxBurn}
/>
- <div
- style={{
- paddingTop: '12px',
- }}
+
+ <Button
+ type="primary"
+ onClick={handleBurnAmountInput}
+ danger
>
- <SecondaryButton
- disabled={
- apiError ||
- sendTokenAmountError ||
- sendTokenAddressError
- }
- onClick={() =>
- checkForConfirmationBeforeSendEtoken()
- }
- >
- Send {token.info.tokenName}
- </SecondaryButton>
- </div>
-
- {apiError && <ApiError />}
- </Form>
- {tokenStats !== null && (
- <AntdDescriptionsWrapper>
- <Descriptions
- column={1}
- bordered
- title={`Token info for "${token.info.tokenName}"`}
- >
- <Descriptions.Item label="Icon">
- <TokenIcon
- size={64}
- tokenId={tokenId}
- />
- </Descriptions.Item>
- <Descriptions.Item label="Decimals">
- {token.info.decimals}
- </Descriptions.Item>
- <Descriptions.Item label="Token ID">
- {token.tokenId}
- <br />
- <AirdropButton>
- <Link
- to="/airdrop"
- state={{
- airdropEtokenId:
- token.tokenId,
- }}
- >
- Airdrop XEC to holders
- </Link>
- </AirdropButton>
- </Descriptions.Item>
- {tokenStats && (
- <>
- <Descriptions.Item label="Document URI">
- {tokenStats.genesisInfo.url}
- </Descriptions.Item>
- <Descriptions.Item label="Genesis Date">
- {tokenStats.block &&
- tokenStats.block
- .timestamp !== null
- ? formatDate(
- tokenStats.block
- .timestamp,
- navigator.language,
- )
- : 'Just now (Genesis tx confirming)'}
- </Descriptions.Item>
- <Descriptions.Item label="Burn eToken">
- <DestinationAmount
- validateStatus={
- burnTokenAmountError
- ? 'error'
- : ''
- }
- help={
- burnTokenAmountError
- ? burnTokenAmountError
- : ''
- }
- onMax={onMaxBurn}
- inputProps={{
- placeholder:
- 'Burn Amount',
- suffix: token.info
- .tokenTicker,
- onChange: e =>
- handleEtokenBurnAmountChange(
- e,
- ),
- initialvalue: 1,
- value: eTokenBurnAmount,
- prefix: (
- <TokenIcon
- size={32}
- tokenId={
- tokenId
- }
- />
- ),
- }}
- />
- <Button
- type="primary"
- onClick={
- handleBurnAmountInput
- }
- danger
- >
- Burn&nbsp;
- {token.info.tokenTicker}
- </Button>
- </Descriptions.Item>
- </>
- )}
- </Descriptions>
- </AntdDescriptionsWrapper>
- )}
- </Col>
- </Row>
+ Burn&nbsp;
+ {token.info.tokenTicker}
+ </Button>
+ </InputFlex>
+ </TokenStatsRow>
+ </TokenStatsTable>
</SidePaddingCtn>
)}
</>
diff --git a/cashtab/src/components/Send/SendXec.js b/cashtab/src/components/Send/SendXec.js
--- a/cashtab/src/components/Send/SendXec.js
+++ b/cashtab/src/components/Send/SendXec.js
@@ -5,20 +5,13 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { WalletContext } from 'wallet/context';
-import {
- AntdFormWrapper,
- SendXecInput,
- DestinationAddressSingle,
- DestinationAddressMulti,
-} from 'components/Common/EnhancedInputs';
import {
ThemedMailOutlined,
CashReceivedNotificationIcon,
} from 'components/Common/CustomIcons';
import { CustomCollapseCtn } from 'components/Common/StyledCollapse';
-import { Form, Alert, Input } from 'antd';
+import { Alert, Switch } from 'antd';
import Modal from 'components/Common/Modal';
-import { Row, Col, Switch } from 'antd';
import PrimaryButton from 'components/Common/PrimaryButton';
import { toSatoshis, toXec } from 'wallet';
import { getMaxSendAmountSatoshis } from 'ecash-coinselect';
@@ -62,13 +55,19 @@
import { isMobile, getUserLocale } from 'helpers';
import { hasEnoughToken, fiatToSatoshis } from 'wallet';
import { toast } from 'react-toastify';
-const { TextArea } = Input;
+import {
+ InputWithScanner,
+ SendXecInput,
+ TextArea,
+} from 'components/Common/Inputs';
-const TextAreaLabel = styled.div`
- text-align: left;
+const SwitchContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
color: ${props => props.theme.forms.text};
- padding-left: 1px;
white-space: nowrap;
+ margin: 12px;
`;
const SentLink = styled.a`
@@ -91,10 +90,10 @@
`;
const AmountPreviewCtn = styled.div`
+ margin: 12px;
display: flex;
flex-direction: column;
- justify-content: top;
- max-height: 1rem;
+ justify-content: center;
`;
const SendInputCtn = styled.div`
@@ -112,10 +111,6 @@
margin-bottom: 0;
`;
-const SendAddressHeader = styled.div`
- display: flex;
- align-items: center;
-`;
const DestinationAddressSingleCtn = styled.div``;
const DestinationAddressMultiCtn = styled.div``;
@@ -181,11 +176,13 @@
settings && settings.autoCameraOn === true && isMobile(navigator);
const [formData, setFormData] = useState({
- value: '',
+ amount: '',
address: '',
+ multiAddressInput: '',
airdropTokenId: '',
});
const [sendAddressError, setSendAddressError] = useState(false);
+ const [multiSendAddressError, setMultiSendAddressError] = useState(false);
const [sendAmountError, setSendAmountError] = useState(false);
const [isMsgError, setIsMsgError] = useState(false);
const [aliasInputAddress, setAliasInputAddress] = useState(false);
@@ -216,12 +213,17 @@
const userLocale = getUserLocale(navigator);
const clearInputForms = () => {
setFormData({
- value: '',
+ amount: '',
address: '',
+ multiAddressInput: '',
+ airdropTokenId: '',
});
setOpReturnMsg(''); // OP_RETURN message has its own state field
setAliasInputAddress(false); // clear alias address preview
setParsedAddressInput(parseAddressInput(''));
+ // Reset to XEC
+ // Note, this ensures we never are in fiat send mode for multi-send
+ setSelectedCurrency(appConfig.ticker);
};
const checkForConfirmationBeforeSendXec = () => {
@@ -249,7 +251,7 @@
if (location && location.state && location.state.replyAddress) {
setFormData({
address: location.state.replyAddress,
- value: `${toXec(appConfig.dustSats).toString()}`,
+ amount: `${toXec(appConfig.dustSats)}`,
});
}
@@ -278,7 +280,7 @@
) {
setIsOneToManyXECSend(true);
setFormData({
- address: location.state.airdropRecipients,
+ multiAddressInput: location.state.airdropRecipients,
airdropTokenId: location.state.airdropTokenId,
});
@@ -386,7 +388,7 @@
// TODO deprecate this support once PayButton and cashtab-components do not require it
handleAmountChange({
target: {
- name: 'value',
+ name: 'amount',
value: txInfoFromUrl.value,
},
});
@@ -448,7 +450,7 @@
if (isOneToManyXECSend) {
// Handle XEC send to multiple addresses
targetOutputs = targetOutputs.concat(
- getMultisendTargetOutputs(formData.address),
+ getMultisendTargetOutputs(formData.multiAddressInput),
);
Event('Send.js', 'SendToMany', selectedCurrency);
@@ -464,8 +466,8 @@
}
const satoshisToSend =
selectedCurrency === 'XEC'
- ? toSatoshis(formData.value)
- : fiatToSatoshis(formData.value, fiatPrice);
+ ? toSatoshis(formData.amount)
+ : fiatToSatoshis(formData.amount, fiatPrice);
targetOutputs.push({
address: cleanAddress,
@@ -502,7 +504,6 @@
</SentLink>,
{
icon: CashReceivedNotificationIcon,
- autoClose: false,
},
);
@@ -584,15 +585,11 @@
// Use this object to mimic user input and get validation for the value
let amountObj = {
target: {
- name: 'value',
+ name: 'amount',
value: parsedAddressInput.amount.value,
},
};
handleAmountChange(amountObj);
- setFormData({
- ...formData,
- value: parsedAddressInput.amount.value,
- });
}
// Set address field to user input
@@ -611,7 +608,7 @@
);
// If you get an error msg, set it. If validation is good, clear error msg.
- setSendAddressError(
+ setMultiSendAddressError(
typeof errorOrIsValid === 'string' ? errorOrIsValid : false,
);
@@ -623,11 +620,11 @@
};
const handleSelectedCurrencyChange = e => {
- setSelectedCurrency(e);
+ setSelectedCurrency(e.target.value);
// Clear input field to prevent accidentally sending 1 XEC instead of 1 USD
setFormData(p => ({
...p,
- value: '',
+ amount: '',
}));
};
@@ -669,7 +666,7 @@
setOpReturnMsg(e.target.value);
};
- const onMax = async () => {
+ const onMax = () => {
// Clear amt error
setSendAmountError(false);
@@ -711,43 +708,59 @@
// Note, if we are updating it to 0, we will get a 'dust' error
handleAmountChange({
target: {
- name: 'value',
+ name: 'amount',
value: maxSendXec,
},
});
};
// Display price in USD below input field for send amount, if it can be calculated
let fiatPriceString = '';
- if (fiatPrice !== null && !isNaN(formData.value)) {
+ let multiSendTotal =
+ typeof formData.multiAddressInput === 'string'
+ ? sumOneToManyXec(formData.multiAddressInput.split('\n'))
+ : 0;
+ if (isNaN(multiSendTotal)) {
+ multiSendTotal = 0;
+ }
+ if (fiatPrice !== null && !isNaN(formData.amount)) {
if (selectedCurrency === appConfig.ticker) {
- // calculate conversion to fiatPrice
- fiatPriceString = `${(fiatPrice * Number(formData.value)).toFixed(
- 2,
- )}`;
-
- // formats to fiat locale style
- fiatPriceString = formatFiatBalance(
- Number(fiatPriceString),
- userLocale,
- );
-
// insert symbol and currency before/after the locale formatted fiat balance
- fiatPriceString = `${
- settings
- ? `${
- supportedFiatCurrencies[settings.fiatCurrency].symbol
- } `
- : '$ '
- } ${fiatPriceString} ${
- settings && settings.fiatCurrency
- ? settings.fiatCurrency.toUpperCase()
- : 'USD'
- }`;
+ fiatPriceString = isOneToManyXECSend
+ ? `${
+ settings
+ ? `${
+ supportedFiatCurrencies[settings.fiatCurrency]
+ .symbol
+ } `
+ : '$ '
+ } ${(fiatPrice * multiSendTotal).toLocaleString(userLocale, {
+ minimumFractionDigits: appConfig.cashDecimals,
+ maximumFractionDigits: appConfig.cashDecimals,
+ })} ${
+ settings && settings.fiatCurrency
+ ? settings.fiatCurrency.toUpperCase()
+ : 'USD'
+ }`
+ : `${
+ settings
+ ? `${
+ supportedFiatCurrencies[settings.fiatCurrency]
+ .symbol
+ } `
+ : '$ '
+ } ${(fiatPrice * formData.amount).toLocaleString(userLocale, {
+ minimumFractionDigits: appConfig.cashDecimals,
+ maximumFractionDigits: appConfig.cashDecimals,
+ })} ${
+ settings && settings.fiatCurrency
+ ? settings.fiatCurrency.toUpperCase()
+ : 'USD'
+ }`;
} else {
fiatPriceString = `${
- formData.value
+ formData.amount !== 0
? formatFiatBalance(
- toXec(fiatToSatoshis(formData.value, fiatPrice)),
+ toXec(fiatToSatoshis(formData.amount, fiatPrice)),
userLocale,
)
: formatFiatBalance(0, userLocale)
@@ -775,13 +788,11 @@
description={
isOneToManyXECSend
? `Send
- ${sumOneToManyXec(
- formData.address.split('\n'),
- ).toLocaleString(userLocale, {
+ ${multiSendTotal.toLocaleString(userLocale, {
maximumFractionDigits: 2,
})}
XEC to multiple recipients?`
- : `Send ${formData.value}${' '}
+ : `Send ${formData.amount}${' '}
${selectedCurrency} to ${parsedAddressInput.address.value}`
}
handleOk={handleOk}
@@ -790,305 +801,211 @@
/>
)}
<SidePaddingCtn data-testid="send-xec-ctn">
- <Row type="flex">
- <Col span={24}>
- <Form
- style={{
- width: 'auto',
- marginTop: '40px',
+ {txInfoFromUrl && (
+ <AppCreatedTxSummary data-testid="app-created-tx">
+ Webapp Tx Request
+ </AppCreatedTxSummary>
+ )}
+
+ {!txInfoFromUrl && !('queryString' in parsedAddressInput) && (
+ <SwitchContainer>
+ Multiple Recipients:&nbsp;&nbsp;
+ <Switch
+ data-testid="multiple-recipients-switch"
+ defaultunchecked="true"
+ checked={isOneToManyXECSend}
+ onChange={() => {
+ setIsOneToManyXECSend(!isOneToManyXECSend);
+ // Do not persist multisend input to single send and vice versa
+ clearInputForms();
}}
- >
- {txInfoFromUrl && (
- <AppCreatedTxSummary data-testid="app-created-tx">
- Webapp Tx Request
- </AppCreatedTxSummary>
- )}
-
- <SendAddressHeader>
- {' '}
- <FormLabel>Send to</FormLabel>
- {!txInfoFromUrl &&
- !('queryString' in parsedAddressInput) && (
- <TextAreaLabel>
- Multiple Recipients:&nbsp;&nbsp;
- <Switch
- data-testid="multiple-recipients-switch"
- defaultunchecked="true"
- checked={isOneToManyXECSend}
- onChange={() => {
- setIsOneToManyXECSend(
- !isOneToManyXECSend,
- );
- // Do not persist multisend input to single send and vice versa
- clearInputForms();
- }}
- style={{
- marginBottom: '7px',
- }}
- />
- </TextAreaLabel>
- )}
- </SendAddressHeader>
- <ExpandingAddressInputCtn open={isOneToManyXECSend}>
- <SendInputCtn>
- <DestinationAddressSingleCtn>
- <DestinationAddressSingle
- style={{
- marginBottom: '0px',
- }}
- loadWithCameraOpen={
- location &&
- location.state &&
- location.state.replyAddress
- ? false
- : openWithScanner
- }
- validateStatus={
- sendAddressError ? 'error' : ''
- }
- help={
- sendAddressError
- ? sendAddressError
- : ''
- }
- onScan={result =>
- handleAddressChange({
- target: {
- name: 'address',
- value: result,
- },
- })
- }
- inputProps={{
- disabled: txInfoFromUrl,
- placeholder:
- aliasSettings.aliasEnabled
- ? `Address or Alias`
- : `Address`,
- name: 'address',
- onChange: e =>
- handleAddressChange(e),
- value: formData.address,
- }}
- ></DestinationAddressSingle>
- <AliasAddressPreviewLabel>
- <TxLink
- key={aliasInputAddress}
- data-testid="alias-address-preview"
- href={`${explorer.blockExplorerUrl}/address/${aliasInputAddress}`}
- target="_blank"
- rel="noreferrer"
- >
- {aliasInputAddress &&
- `${aliasInputAddress.slice(
- 0,
- 10,
- )}...${aliasInputAddress.slice(
- -5,
- )}`}
- </TxLink>
- </AliasAddressPreviewLabel>
- <FormLabel>
- Amount{' '}
- {'amount' in parsedAddressInput &&
- parsedAddressInput.amount
- .value !== null && (
- <AmountSetByBip21Alert data-testid="bip-alert">
- {' '}
- (set by BIP21 query
- string)
- </AmountSetByBip21Alert>
- )}
- </FormLabel>
- <SendXecInput
- activeFiatCode={
- settings &&
- settings.fiatCurrency
- ? settings.fiatCurrency.toUpperCase()
- : 'USD'
- }
- validateStatus={
- sendAmountError ? 'error' : ''
- }
- help={
- sendAmountError
- ? sendAmountError
- : ''
- }
- onMax={onMax}
- inputProps={{
- name: 'value',
- dollar:
- selectedCurrency === 'USD'
- ? 1
- : 0,
- placeholder: 'Amount',
- onChange: e =>
- handleAmountChange(e),
- value: formData.value,
- disabled:
- priceApiError ||
- (txInfoFromUrl !== false &&
- 'value' in
- txInfoFromUrl &&
- txInfoFromUrl.value !==
- 'null' &&
- txInfoFromUrl.value !==
- 'undefined') ||
- 'amount' in
- parsedAddressInput,
- }}
- selectProps={{
- value: selectedCurrency,
- disabled:
- 'amount' in
- parsedAddressInput ||
- txInfoFromUrl,
- onChange: e =>
- handleSelectedCurrencyChange(
- e,
- ),
- }}
- ></SendXecInput>
- </DestinationAddressSingleCtn>
- {priceApiError && (
- <AlertMsg>
- Error fetching fiat price. Setting
- send by{' '}
- {supportedFiatCurrencies[
- settings.fiatCurrency
- ].slug.toUpperCase()}{' '}
- disabled
- </AlertMsg>
- )}
- </SendInputCtn>
-
- <>
- <DestinationAddressMultiCtn>
- <DestinationAddressMulti
- validateStatus={
- sendAddressError ? 'error' : ''
- }
- help={
- sendAddressError
- ? sendAddressError
- : ''
- }
- inputProps={{
- placeholder: `One address & value per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`,
- name: 'address',
- onChange: e =>
- handleMultiAddressChange(e),
- value: formData.address,
- }}
- ></DestinationAddressMulti>
- </DestinationAddressMultiCtn>
- </>
- <AmountPreviewCtn>
- {!priceApiError && !isOneToManyXECSend && (
- <>
- <LocaleFormattedValue>
- {!isNaN(formData.value)
- ? formatBalance(
- formData.value,
- userLocale,
- ) +
- ' ' +
- selectedCurrency
- : ''}
- </LocaleFormattedValue>
- <ConvertAmount>
- {fiatPriceString !== '' && '='}{' '}
- {fiatPriceString}
- </ConvertAmount>
- </>
- )}
- </AmountPreviewCtn>
- </ExpandingAddressInputCtn>
- {'op_return_raw' in parsedAddressInput && (
- <OpReturnRawSetByBip21Alert data-testid="op-return-raw-set-alert">
- Hex OP_RETURN &quot;
- {parsedAddressInput.op_return_raw.value}
- &quot; set by BIP21
- </OpReturnRawSetByBip21Alert>
- )}
- <div
- style={{
- paddingTop: '1rem',
- }}
- >
- <PrimaryButton
- data-testid="send-it"
- disabled={disableSendButton}
- onClick={() => {
- checkForConfirmationBeforeSendXec();
- }}
- >
- Send
- </PrimaryButton>
- </div>
- {!('op_return_raw' in parsedAddressInput) && (
- <CustomCollapseCtn
- data-testid="cashtab-msg-collapse"
- panelHeader={
- <PanelHeaderCtn>
- <ThemedMailOutlined /> Message
- </PanelHeaderCtn>
- }
- optionalDefaultActiveKey={
- location &&
- location.state &&
- location.state.replyAddress
- ? ['1']
- : ['0']
- }
- optionalKey="1"
+ />
+ </SwitchContainer>
+ )}
+ <ExpandingAddressInputCtn open={isOneToManyXECSend}>
+ <SendInputCtn>
+ <DestinationAddressSingleCtn>
+ <InputWithScanner
+ placeholder={
+ aliasSettings.aliasEnabled
+ ? `Address or Alias`
+ : `Address`
+ }
+ name="address"
+ value={formData.address}
+ disabled={txInfoFromUrl !== false}
+ handleInput={handleAddressChange}
+ error={sendAddressError}
+ loadWithScannerOpen={openWithScanner}
+ />
+ <AliasAddressPreviewLabel>
+ <TxLink
+ key={aliasInputAddress}
+ data-testid="alias-address-preview"
+ href={`${explorer.blockExplorerUrl}/address/${aliasInputAddress}`}
+ target="_blank"
+ rel="noreferrer"
>
- <AntdFormWrapper
- style={{
- marginBottom: '20px',
- }}
- >
- <Alert
- style={{
- marginBottom: '10px',
- }}
- description="Please note this message will be public."
- type="warning"
- showIcon
- />
- <TextArea
- name="opReturnMsg"
- placeholder={
- location &&
- location.state &&
- location.state.airdropTokenId
- ? `(max ${
- opreturnConfig.cashtabMsgByteLimit -
- localAirdropTxAddedBytes
- } bytes)`
- : `(max ${opreturnConfig.cashtabMsgByteLimit} bytes)`
- }
- value={
- opReturnMsg ? opReturnMsg : ''
- }
- onChange={e => handleMsgChange(e)}
- onKeyDown={e =>
- e.keyCode == 13
- ? e.preventDefault()
- : ''
- }
- />
- <MsgBytesizeError>
- {isMsgError ? isMsgError : ''}
- </MsgBytesizeError>
- </AntdFormWrapper>
- </CustomCollapseCtn>
- )}
- {apiError && <ApiError />}
- </Form>
- </Col>
- </Row>
+ {aliasInputAddress &&
+ `${aliasInputAddress.slice(
+ 0,
+ 10,
+ )}...${aliasInputAddress.slice(-5)}`}
+ </TxLink>
+ </AliasAddressPreviewLabel>
+ <FormLabel>
+ {'amount' in parsedAddressInput &&
+ parsedAddressInput.amount.value !==
+ null && (
+ <AmountSetByBip21Alert data-testid="bip-alert">
+ {' '}
+ (set by BIP21 query string)
+ </AmountSetByBip21Alert>
+ )}
+ </FormLabel>
+ <SendXecInput
+ name="amount"
+ value={formData.amount}
+ selectValue={selectedCurrency}
+ selectDisabled={
+ 'amount' in parsedAddressInput ||
+ txInfoFromUrl
+ }
+ inputDisabled={
+ priceApiError ||
+ (txInfoFromUrl !== false &&
+ 'value' in txInfoFromUrl &&
+ txInfoFromUrl.value !== 'null' &&
+ txInfoFromUrl.value !== 'undefined') ||
+ 'amount' in parsedAddressInput
+ }
+ fiatCode={settings.fiatCurrency.toUpperCase()}
+ error={sendAmountError}
+ handleInput={handleAmountChange}
+ handleSelect={handleSelectedCurrencyChange}
+ handleOnMax={onMax}
+ />
+ </DestinationAddressSingleCtn>
+ {priceApiError && (
+ <AlertMsg>
+ Error fetching fiat price. Setting send by{' '}
+ {supportedFiatCurrencies[
+ settings.fiatCurrency
+ ].slug.toUpperCase()}{' '}
+ disabled
+ </AlertMsg>
+ )}
+ </SendInputCtn>
+
+ <>
+ <DestinationAddressMultiCtn>
+ <TextArea
+ placeholder={`One address & value per line, separated by comma \ne.g. \necash:qpatql05s9jfavnu0tv6lkjjk25n6tmj9gkpyrlwu8,500 \necash:qzvydd4n3lm3xv62cx078nu9rg0e3srmqq0knykfed,700`}
+ name="multiAddressInput"
+ handleInput={e => handleMultiAddressChange(e)}
+ value={formData.multiAddressInput}
+ error={multiSendAddressError}
+ />
+ </DestinationAddressMultiCtn>
+ </>
+ <AmountPreviewCtn>
+ {!priceApiError && (
+ <>
+ {isOneToManyXECSend ? (
+ <LocaleFormattedValue>
+ {formatBalance(
+ multiSendTotal,
+ userLocale,
+ ) +
+ ' ' +
+ selectedCurrency}
+ </LocaleFormattedValue>
+ ) : (
+ <LocaleFormattedValue>
+ {!isNaN(formData.amount)
+ ? formatBalance(
+ formData.amount,
+ userLocale,
+ ) +
+ ' ' +
+ selectedCurrency
+ : ''}
+ </LocaleFormattedValue>
+ )}
+ <ConvertAmount>
+ {fiatPriceString !== '' && '='}{' '}
+ {fiatPriceString}
+ </ConvertAmount>
+ </>
+ )}
+ </AmountPreviewCtn>
+ </ExpandingAddressInputCtn>
+ {'op_return_raw' in parsedAddressInput && (
+ <OpReturnRawSetByBip21Alert data-testid="op-return-raw-set-alert">
+ Hex OP_RETURN &quot;
+ {parsedAddressInput.op_return_raw.value}
+ &quot; set by BIP21
+ </OpReturnRawSetByBip21Alert>
+ )}
+ <PrimaryButton
+ style={{ marginTop: '12px' }}
+ data-testid="send-it"
+ disabled={disableSendButton}
+ onClick={() => {
+ checkForConfirmationBeforeSendXec();
+ }}
+ >
+ Send
+ </PrimaryButton>
+ {!('op_return_raw' in parsedAddressInput) && (
+ <CustomCollapseCtn
+ data-testid="cashtab-msg-collapse"
+ panelHeader={
+ <PanelHeaderCtn>
+ <ThemedMailOutlined /> Message
+ </PanelHeaderCtn>
+ }
+ optionalDefaultActiveKey={
+ location &&
+ location.state &&
+ location.state.replyAddress
+ ? ['1']
+ : ['0']
+ }
+ optionalKey="1"
+ >
+ <Alert
+ style={{
+ marginBottom: '10px',
+ }}
+ description="Please note this message will be public."
+ type="warning"
+ showIcon
+ />
+ <TextArea
+ name="opReturnMsg"
+ placeholder={
+ location &&
+ location.state &&
+ location.state.airdropTokenId
+ ? `(max ${
+ opreturnConfig.cashtabMsgByteLimit -
+ localAirdropTxAddedBytes
+ } bytes)`
+ : `(max ${opreturnConfig.cashtabMsgByteLimit} bytes)`
+ }
+ value={opReturnMsg ? opReturnMsg : ''}
+ handleInput={e => handleMsgChange(e)}
+ onKeyDown={e =>
+ e.keyCode == 13 ? e.preventDefault() : ''
+ }
+ />
+ <MsgBytesizeError>
+ {isMsgError ? isMsgError : ''}
+ </MsgBytesizeError>
+ </CustomCollapseCtn>
+ )}
+ {apiError && <ApiError />}
</SidePaddingCtn>
</>
);
diff --git a/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js b/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js
--- a/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js
+++ b/cashtab/src/components/Send/__tests__/SendByUrlParams.test.js
@@ -115,7 +115,9 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(destinationAddress);
+ await waitFor(() =>
+ expect(addressInputEl).toHaveValue(destinationAddress),
+ );
// The address input is disabled
expect(addressInputEl).toHaveProperty('disabled', true);
@@ -186,7 +188,9 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(destinationAddress);
+ await waitFor(() =>
+ expect(addressInputEl).toHaveValue(destinationAddress),
+ );
// The address input is disabled
expect(addressInputEl).toHaveProperty('disabled', true);
@@ -252,7 +256,9 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(destinationAddress);
+ await waitFor(() =>
+ expect(addressInputEl).toHaveValue(destinationAddress),
+ );
// The address input is disabled
expect(addressInputEl).toHaveProperty('disabled', true);
@@ -318,7 +324,9 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(destinationAddress);
+ await waitFor(() =>
+ expect(addressInputEl).toHaveValue(destinationAddress),
+ );
// The address input is disabled
expect(addressInputEl).toHaveProperty('disabled', true);
@@ -505,7 +513,9 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(destinationAddress);
+ await waitFor(() =>
+ expect(addressInputEl).toHaveValue(destinationAddress),
+ );
// The address input is disabled
expect(addressInputEl).toHaveProperty('disabled', true);
@@ -575,7 +585,7 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(bip21Str);
+ await waitFor(() => expect(addressInputEl).toHaveValue(bip21Str));
// The address input is disabled for app txs with bip21 strings
// Note it is NOT disabled for txs where the user inputs the bip21 string
@@ -669,7 +679,7 @@
).not.toBeInTheDocument();
// The 'Send To' input field has this address as a value
- expect(addressInputEl).toHaveValue(bip21Str);
+ await waitFor(() => expect(addressInputEl).toHaveValue(bip21Str));
// The address input is disabled for app txs with bip21 strings
// Note it is NOT disabled for txs where the user inputs the bip21 string
diff --git a/cashtab/src/components/Send/__tests__/SendToken.test.js b/cashtab/src/components/Send/__tests__/SendToken.test.js
--- a/cashtab/src/components/Send/__tests__/SendToken.test.js
+++ b/cashtab/src/components/Send/__tests__/SendToken.test.js
@@ -133,9 +133,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
const amountInputEl = screen.getByPlaceholderText('Amount');
@@ -166,9 +164,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -197,9 +193,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -229,9 +223,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -289,9 +281,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -314,9 +304,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -341,9 +329,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -388,9 +374,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -428,9 +412,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -466,9 +448,7 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
// The user enters a valid address and send amount
const addressInputEl = screen.getByPlaceholderText('Address');
@@ -510,16 +490,14 @@
);
// Wait for element to get token info and load
- await waitFor(() =>
- expect(screen.getAllByText('BEAR')[0]).toBeInTheDocument(),
- );
-
- // By default, a burn amount of '1' is already entered into the form
+ expect((await screen.findAllByText(/BEAR/))[0]).toBeInTheDocument();
// Click the Burn button
// Note we button title is the token ticker
await user.click(await screen.findByRole('button', { name: /Burn/ }));
+ await user.type(screen.getByPlaceholderText('Burn Amount'), '1');
+
// We see a modal and enter the correct confirmation msg
await user.type(
screen.getByPlaceholderText(`Type "burn BEAR" to confirm`),
diff --git a/cashtab/src/components/Send/__tests__/SendXec.test.js b/cashtab/src/components/Send/__tests__/SendXec.test.js
--- a/cashtab/src/components/Send/__tests__/SendXec.test.js
+++ b/cashtab/src/components/Send/__tests__/SendXec.test.js
@@ -1408,11 +1408,14 @@
await user.type(addressInputEl, addressInput);
// Select USD from currency select
- await user.click(
- screen.getByTestId('currency-select-dropdown').firstElementChild,
+ const currencyDropdownMenu = screen.getByTestId(
+ 'currency-select-dropdown',
);
- await user.click(screen.getAllByTestId('currency-select-option')[1]);
-
+ await user.selectOptions(
+ screen.getByTestId('currency-select-dropdown'),
+ screen.getByTestId('fiat-option'),
+ );
+ await waitFor(() => expect(currencyDropdownMenu).toHaveValue('USD'));
// Send $0.21
// 7000 satoshis at 0.00003 USD / XEC
await user.type(amountInputEl, '0.21');
diff --git a/cashtab/src/components/__tests__/App.test.js b/cashtab/src/components/__tests__/App.test.js
--- a/cashtab/src/components/__tests__/App.test.js
+++ b/cashtab/src/components/__tests__/App.test.js
@@ -408,25 +408,17 @@
).not.toBeInTheDocument(),
);
- await waitFor(async () => {
- // Get the "Reply to" button of Cashtab Msg
- const cashtabMsgReplyBtn = screen.getByTestId('cashtab-msg-reply');
- // Click reply to cashtab msg button
- // ref https://github.com/testing-library/user-event/issues/922
- // ref https://github.com/testing-library/user-event/issues/662
- // issue with using userEvents.click() here likely related to antd
- cashtabMsgReplyBtn.click();
- });
+ await user.click(screen.getByTestId('cashtab-msg-reply'));
// Now we see the Send screen
expect(await screen.findByTestId('send-xec-ctn')).toBeInTheDocument();
// The SendXec send address input is rendered and has expected value
- expect(
- await screen.findByTestId('destination-address-single'),
- ).toHaveValue('ecash:qphlhe78677sz227k83hrh542qeehh8el5lcjwk72y');
+ expect(screen.getByPlaceholderText('Address')).toHaveValue(
+ 'ecash:qphlhe78677sz227k83hrh542qeehh8el5lcjwk72y',
+ );
// The value field is populated with dust
- expect(await screen.findByTestId('send-xec-input')).toHaveValue(5.5);
+ expect(screen.getByPlaceholderText('Amount')).toHaveValue(5.5);
});
it('We do not see the camera auto-open setting in the config screen on a desktop device', async () => {
const mockedChronik = await initializeCashtabStateForTests(
@@ -705,7 +697,7 @@
await user.click(screen.getByText('GRP'));
// Wait for element to get token info and load
- await screen.findByText('Token info for "GRUMPY"');
+ expect((await screen.findAllByText(/GRP/))[0]).toBeInTheDocument();
// We send enough GRP to be under the min
await user.type(
diff --git a/cashtab/src/validation/__tests__/index.test.js b/cashtab/src/validation/__tests__/index.test.js
--- a/cashtab/src/validation/__tests__/index.test.js
+++ b/cashtab/src/validation/__tests__/index.test.js
@@ -466,7 +466,7 @@
expect(isValidOpreturnParam('042e7')).toBe(false);
});
describe('Determining whether Send button should be disabled on SendXec screen', () => {
- const { expectedReturns } = vectors.shouldDisableXecSend;
+ const { expectedReturns } = vectors.shouldSendXecBeDisabled;
// Successfully created targetOutputs
expectedReturns.forEach(expectedReturn => {
diff --git a/cashtab/src/validation/fixtures/vectors.js b/cashtab/src/validation/fixtures/vectors.js
--- a/cashtab/src/validation/fixtures/vectors.js
+++ b/cashtab/src/validation/fixtures/vectors.js
@@ -17,13 +17,13 @@
};
export default {
- shouldDisableXecSend: {
+ shouldSendXecBeDisabled: {
expectedReturns: [
{
description: 'Disabled on startup',
formData: {
address: '',
- value: '',
+ amount: '',
},
balanceSats: 10000,
apiError: false,
@@ -39,7 +39,7 @@
'Disabled if address has been entered but no value',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6',
- value: '',
+ amount: '',
},
balanceSats: 10000,
apiError: false,
@@ -54,7 +54,7 @@
description: 'Enabled for valid address and value',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6',
- value: '50',
+ amount: '50',
},
balanceSats: 10000,
apiError: false,
@@ -69,7 +69,7 @@
description: 'Disabled on zero balance',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6',
- value: '50',
+ amount: '50',
},
balanceSats: 0,
apiError: false,
@@ -84,7 +84,7 @@
description: 'Disabled for invalid address',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg',
- value: '50',
+ amount: '50',
},
balanceSats: 10000,
apiError: false,
@@ -100,7 +100,7 @@
description: 'Disabled for invalid value',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6',
- value: '5',
+ amount: '5',
},
balanceSats: 10000,
apiError: false,
@@ -116,7 +116,7 @@
description: 'Disabled for invalid opreturn msg',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6',
- value: '5',
+ amount: '5',
},
balanceSats: 10000,
apiError: false,
@@ -131,7 +131,7 @@
description: 'Disabled on priceApi error',
formData: {
address: 'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6',
- value: '5',
+ amount: '5',
},
balanceSats: 10000,
apiError: false,
@@ -148,7 +148,7 @@
formData: {
address:
'ecash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6, 22\necash:qp89xgjhcqdnzzemts0aj378nfe2mhu9yvxj9nhgg6, 22',
- value: '',
+ amount: '',
},
balanceSats: 10000,
apiError: false,
diff --git a/cashtab/src/validation/index.js b/cashtab/src/validation/index.js
--- a/cashtab/src/validation/index.js
+++ b/cashtab/src/validation/index.js
@@ -170,9 +170,9 @@
return 'Amount must be greater than 0';
}
if (sendAmountSatoshis < appConfig.dustSats) {
- return `Send amount must be at least ${toXec(
- appConfig.dustSats,
- ).toString()} ${appConfig.ticker}`;
+ return `Send amount must be at least ${toXec(appConfig.dustSats)} ${
+ appConfig.ticker
+ }`;
}
if (sendAmountSatoshis > balanceSats) {
return `Amount ${toXec(sendAmountSatoshis).toLocaleString(userLocale, {
@@ -550,7 +550,7 @@
isOneToManyXECSend,
) => {
return (
- (formData.value === '' && formData.address === '') || // No user inputs
+ (formData.amount === '' && formData.address === '') || // No user inputs
balanceSats === 0 || // user has no funds
apiError || // API error
typeof sendAmountError === 'string' || // validation error for send amount
@@ -558,7 +558,7 @@
typeof isMsgError === 'string' || // validation error in Cashtab Msg
priceApiError || // we don't have a good price AND fiat currency is selected
(!isOneToManyXECSend &&
- (isNaN(formData.value) || formData.value === ''))
+ (isNaN(formData.amount) || formData.amount === ''))
); // Value is blank or NaN and is expected to not be so
};

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 1, 11:26 (5 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5187596
Default Alt Text
D15776.diff (151 KB)

Event Timeline