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.11.0",
+ "version": "3.12.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.11.2",
+ "version": "2.12.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cashtab",
- "version": "2.11.2",
+ "version": "2.12.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.11.2",
+ "version": "2.12.0",
"private": true,
"scripts": {
"start": "node scripts/start.js",
diff --git a/cashtab/src/components/Airdrop/Airdrop.js b/cashtab/src/components/Airdrop/Airdrop.js
--- a/cashtab/src/components/Airdrop/Airdrop.js
+++ b/cashtab/src/components/Airdrop/Airdrop.js
@@ -16,7 +16,7 @@
isValidXecAirdrop,
isValidAirdropExclusionArray,
} from 'validation';
-import { SidePaddingCtn } from 'components/Common/Atoms';
+import { SidePaddingCtn, SwitchLabel } from 'components/Common/Atoms';
import { getAirdropTx, getEqualAirdropTx } from 'airdrop';
import Communist from 'assets/communist.png';
import { toast } from 'react-toastify';
@@ -47,10 +47,6 @@
text-align: center;
justify-content: center;
`;
-const SwitchLabel = styled.div`
- color: ${props => props.theme.contrast};
- font-size: 18px;
-`;
const Airdrop = ({ passLoadingStatus }) => {
const ContextValue = React.useContext(WalletContext);
diff --git a/cashtab/src/components/Common/Atoms.js b/cashtab/src/components/Common/Atoms.js
--- a/cashtab/src/components/Common/Atoms.js
+++ b/cashtab/src/components/Common/Atoms.js
@@ -79,3 +79,9 @@
border: solid 1px silver;
border-radius: 10px;
`;
+
+export const SwitchLabel = styled.div`
+ text-align: left;
+ color: ${props => props.theme.contrast};
+ font-size: 18px;
+`;
diff --git a/cashtab/src/components/Common/BalanceHeaderToken.js b/cashtab/src/components/Common/BalanceHeaderToken.js
--- a/cashtab/src/components/Common/BalanceHeaderToken.js
+++ b/cashtab/src/components/Common/BalanceHeaderToken.js
@@ -4,7 +4,6 @@
import * as React from 'react';
import PropTypes from 'prop-types';
-import { formatTokenBalance } from 'utils/formatting';
import styled from 'styled-components';
const TokenBalance = styled.div`
@@ -19,19 +18,23 @@
}
`;
-const BalanceHeaderToken = ({ balance, ticker, tokenDecimals }) => {
+const BalanceHeaderToken = ({
+ formattedDecimalizedTokenBalance,
+ name,
+ ticker,
+}) => {
return (
- {formatTokenBalance(balance, tokenDecimals)} {ticker}
+ {formattedDecimalizedTokenBalance} {name} ({ticker})
);
};
// balance may be a string (XEC balance) or a BigNumber object (token balance)
BalanceHeaderToken.propTypes = {
- balance: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
+ formattedDecimalizedTokenBalance: PropTypes.string,
+ name: PropTypes.string,
ticker: PropTypes.string,
- tokenDecimals: PropTypes.number,
};
export default BalanceHeaderToken;
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
@@ -40,7 +40,8 @@
top: 0;
left: 0;
width: 100%;
- height: ${props => props.height - MODAL_HEIGHT_DELTA}px;
+ height: ${props =>
+ props.showButtons ? props.height - MODAL_HEIGHT_DELTA : props.height}px;
overflow: auto;
padding: 6px;
word-wrap: break-word;
@@ -135,7 +136,7 @@
return (
X
-
+
{typeof title !== 'undefined' && (
{title}
)}
diff --git a/cashtab/src/components/Common/Switch.js b/cashtab/src/components/Common/Switch.js
--- a/cashtab/src/components/Common/Switch.js
+++ b/cashtab/src/components/Common/Switch.js
@@ -6,6 +6,9 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';
+const Container = styled.div`
+ width: ${props => props.switchWidth}px;
+`;
const ToggleSwitch = styled.div`
position: relative;
width: ${props => props.switchWidth}px;
@@ -110,28 +113,30 @@
}
return (
-
-
-
-
+
+
-
-
-
+ name={name}
+ id={name}
+ data-testid={name}
+ />
+
+
+
+
+
+
);
};
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,11 +5,10 @@
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';
import { WalletContext } from 'wallet/context';
-import { message, Button } from 'antd';
import PrimaryButton, {
SecondaryButton,
} from 'components/Common/PrimaryButton';
-import { SidePaddingCtn, TxLink } from 'components/Common/Atoms';
+import { SidePaddingCtn, TxLink, SwitchLabel } from 'components/Common/Atoms';
import BalanceHeaderToken from 'components/Common/BalanceHeaderToken';
import { useNavigate } from 'react-router-dom';
import { BN } from 'slp-mdm';
@@ -44,24 +43,77 @@
import CopyToClipboard from 'components/Common/CopyToClipboard';
import { ThemedCopySolid } from 'components/Common/CustomIcons';
import { decimalizedTokenQtyToLocaleFormat } from 'utils/formatting';
+import Switch from 'components/Common/Switch';
-const TokenStatsTable = styled.div`
+const TokenIconExpandButton = styled.button`
+ cursor: pointer;
+ border: none;
+ background-color: transparent;
+`;
+const SendTokenForm = styled.div`
display: flex;
flex-direction: column;
align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ margin-bottom: 12px;
+`;
+const SendTokenFormRow = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ margin: 3px;
+`;
+
+const TokenStatsTable = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
justify-content: center;
width: 100%;
color: ${props => props.theme.contrast};
+ gap: 12px;
margin-bottom: 12px;
`;
const TokenStatsRow = styled.div`
width: 100%;
display: flex;
+ flex-wrap: wrap;
+ align-items: center;
text-align: center;
justify-content: center;
gap: 3px;
`;
-const TokenStatsCol = styled.div``;
+const TokenStatsCol = styled.div`
+ align-items: center;
+ flex-wrap: wrap;
+`;
+const TokenStatsTableRow = styled.div`
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 3px;
+`;
+
+const TokenStatsLabel = styled.div`
+ font-weight: bold;
+ justify-content: flex-end;
+ text-align: right;
+ display: flex;
+ width: 106px;
+`;
+const SwitchHolder = styled.div`
+ width: 100%;
+ display: flex;
+ justify-content: flex-start;
+ gap: 12px;
+ align-content: center;
+ align-items: center;
+ margin: 12px;
+`;
const TokenSentLink = styled.a`
color: ${props => props.theme.walletBackground};
@@ -112,6 +164,10 @@
const [confirmationOfEtokenToBeBurnt, setConfirmationOfEtokenToBeBurnt] =
useState('');
const [aliasInputAddress, setAliasInputAddress] = useState(false);
+ const [showSend, setShowSend] = useState(true);
+ const [showBurn, setShowBurn] = useState(false);
+ const [showAirdrop, setShowAirdrop] = useState(false);
+ const [showLargeIconModal, setShowLargeIconModal] = useState(false);
// Load with QR code open if device is mobile
const openWithScanner =
@@ -339,9 +395,6 @@
} catch (err) {
console.error(`Error in onMax:`);
console.error(err);
- message.error(
- 'Unable to calculate the max value due to network errors',
- );
}
};
@@ -474,6 +527,17 @@
{tokenBalance &&
typeof cashtabCache.tokens.get(tokenId) !== 'undefined' && (
+ {showLargeIconModal && (
+
+ setShowLargeIconModal(false)
+ }
+ >
+
+
+ )}
{isModalVisible && (
)}
-
-
-
-
-
-
-
-
-
-
- Token Id: {tokenId.slice(0, 3)}...
- {tokenId.slice(-3)}
-
-
-
-
-
-
-
-
-
- {decimals} decimal places
-
-
- {url}
-
- Minted{' '}
- {typeof cachedInfo.block !== 'undefined'
- ? formatDate(
- cachedInfo.block.timestamp,
- navigator.language,
- )
- : formatDate(
- cachedInfo.timeFirstSeen,
- navigator.language,
- )}
-
-
- Genesis Supply:{' '}
- {decimalizedTokenQtyToLocaleFormat(
- genesisSupply,
- userLocale,
+
+
+ setShowLargeIconModal(true)}
+ >
+
+
+
+
+
+ Token Id:
+
+
+ {tokenId.slice(0, 3)}...
+ {tokenId.slice(-3)}
+
+
+
+
+
+
+
+
+
+ decimals:
+ {decimals}
+
+ {url && url.startsWith('https://') && (
+
+ url:
+
+
+ {`${url.slice(8, 19)}...`}
+
+
+
)}
-
-
- {genesisMintBatons === 0
- ? 'Fixed Supply'
- : 'Variable Supply'}
-
+
+ created:
+
+ {typeof cachedInfo.block !== 'undefined'
+ ? formatDate(
+ cachedInfo.block.timestamp,
+ navigator.language,
+ )
+ : formatDate(
+ cachedInfo.timeFirstSeen,
+ navigator.language,
+ )}
+
+
+
+
+ Genesis Qty:
+
+
+ {decimalizedTokenQtyToLocaleFormat(
+ genesisSupply,
+ userLocale,
+ )}
+
+
+
+ Supply:
+
+ {genesisMintBatons === 0
+ ? 'Fixed'
+ : 'Variable'}
+
+
+
-
-
-
- {aliasInputAddress &&
- `${aliasInputAddress.slice(
- 0,
- 10,
- )}...${aliasInputAddress.slice(-5)}`}
-
-
-
-
-
-
- checkForConfirmationBeforeSendEtoken()
- }
- >
- Send {tokenName}
-
{apiError && }
-
-
-
+
+ {
+ if (!showSend) {
+ // If showSend is being set to true here, make sure burn and airdrop are false
+ setShowAirdrop(false);
+ setShowBurn(false);
+ }
+ setShowSend(!showSend);
}}
- >
-
+
+ Send {tokenName} ({tokenTicker})
+
+
+ {showSend && (
+ <>
+
+
+
+
+
+
+ {aliasInputAddress &&
+ `${aliasInputAddress.slice(
+ 0,
+ 10,
+ )}...${aliasInputAddress.slice(
+ -5,
+ )}`}
+
+
+
+
+
+
+
+
+ checkForConfirmationBeforeSendEtoken()
+ }
+ >
+ Send {tokenTicker}
+
+
+ >
+ )}
+
+ {
+ if (!showAirdrop) {
+ // If showAirdrop is being set to true here, make sure burn and send are false
+ setShowBurn(false);
+ setShowSend(false);
+ }
+ setShowAirdrop(!showAirdrop);
+ }}
+ />
+
+ Airdrop XEC to {tokenTicker} holders
+
+
+ {showAirdrop && (
+
+
- Airdrop
-
-
-
-
-
-
+ Airdrop Calculator
+
+
+
+ )}
+
+ {
+ if (!showBurn) {
+ // If showBurn is being set to true here, make sure airdrop and send are false
+ setShowAirdrop(false);
+ setShowSend(false);
}
- handleOnMax={onMaxBurn}
- />
+ setShowBurn(!showBurn);
+ }}
+ />
+ Burn {tokenTicker}
+
+ {showBurn && (
+
+
+
-
-
-
-
+
+ Burn {tokenTicker}
+
+
+
+ )}
+
)}
>
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
@@ -493,12 +493,17 @@
// Wait for element to get token info and load
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/ }));
+ // Click the burn switch to show the burn interface
+ await user.click(screen.getByTestId('burn-switch'));
await user.type(screen.getByPlaceholderText('Burn Amount'), '1');
+ // Click the Burn button
+ // Note we button title is the token ticker
+ await user.click(
+ await screen.findByRole('button', { name: /Burn BEAR/ }),
+ );
+
// 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/utils/__tests__/formatting.test.js b/cashtab/src/utils/__tests__/formatting.test.js
--- a/cashtab/src/utils/__tests__/formatting.test.js
+++ b/cashtab/src/utils/__tests__/formatting.test.js
@@ -2,12 +2,10 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-import { BN } from 'slp-mdm';
import {
formatDate,
formatFiatBalance,
formatBalance,
- formatTokenBalance,
decimalizedTokenQtyToLocaleFormat,
} from 'utils/formatting';
import vectors from 'utils/fixtures/vectors';
@@ -109,25 +107,6 @@
it(`test formatFiatBalance with undefined input`, () => {
expect(formatFiatBalance(undefined, 'en-US')).toBe(undefined);
});
- it(`returns undefined formatTokenBalance with undefined inputs`, () => {
- expect(formatTokenBalance(undefined, undefined)).toBe(undefined);
- });
- it(`test formatTokenBalance with valid balance & decimal inputs`, () => {
- const testBalance = new BN(100.00000001);
- expect(formatTokenBalance(testBalance, 8)).toBe('100.00000001');
- });
- it(`returns undefined when passed invalid decimals parameter`, () => {
- const testBalance = new BN(100.00000001);
- expect(formatTokenBalance(testBalance, 'cheese')).toBe(undefined);
- });
- it(`returns undefined when passed invalid balance parameter`, () => {
- const testBalance = '100.000010122';
- expect(formatTokenBalance(testBalance, 9)).toBe(undefined);
- });
- it(`maintains trailing zeros in balance per tokenDecimal parameter`, () => {
- const testBalance = new BN(10000);
- expect(formatTokenBalance(testBalance, 8)).toBe('10,000.00000000');
- });
describe('We can format decimalized token strings for userLocale', () => {
const { expectedReturns } = vectors.decimalizedTokenQtyToLocaleFormat;
expectedReturns.forEach(vector => {
diff --git a/cashtab/src/utils/formatting.js b/cashtab/src/utils/formatting.js
--- a/cashtab/src/utils/formatting.js
+++ b/cashtab/src/utils/formatting.js
@@ -2,7 +2,6 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
-import { BN } from 'slp-mdm';
import appConfig from 'config/app';
export const formatDate = (dateString, userLocale = 'en') => {
const options = { month: 'short', day: 'numeric', year: 'numeric' };
@@ -55,47 +54,6 @@
}
};
-// unformattedBalance will always be a BigNumber, tokenDecimal will always be a number
-export const formatTokenBalance = (
- unformattedBalance,
- tokenDecimal,
- defaultLocale = 'en',
-) => {
- let formattedTokenBalance;
- let convertedTokenBalance;
- let locale = defaultLocale;
- try {
- if (
- tokenDecimal === undefined ||
- unformattedBalance === undefined ||
- typeof tokenDecimal !== 'number' ||
- !BN.isBigNumber(unformattedBalance)
- ) {
- return undefined;
- }
- if (navigator && navigator.language) {
- locale = navigator.language;
- }
-
- // Use toFixed to get a string with the correct decimal places
- formattedTokenBalance = new BN(unformattedBalance).toFixed(
- tokenDecimal,
- );
- // formattedTokenBalance is converted into a number as toLocaleString does not work with a string
- convertedTokenBalance = parseFloat(
- formattedTokenBalance,
- ).toLocaleString(locale, {
- minimumFractionDigits: tokenDecimal,
- });
-
- return convertedTokenBalance;
- } catch (err) {
- console.error(`Error in formatTokenBalance for ${unformattedBalance}`);
- console.error(err);
- return unformattedBalance;
- }
-};
-
/**
* Add locale number formatting to a decimalized token quantity
* @param {string} decimalizedTokenQty e.g. 100.123