diff --git a/web/cashtab/src/components/Common/QRCode.js b/web/cashtab/src/components/Common/QRCode.js index 72574bb0e..faffe9cd3 100644 --- a/web/cashtab/src/components/Common/QRCode.js +++ b/web/cashtab/src/components/Common/QRCode.js @@ -1,260 +1,252 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import RawQRCode from 'qrcode.react'; -import { - currency, - isValidCashPrefix, - isValidTokenPrefix, -} from '@components/Common/Ticker.js'; +import { currency } from '@components/Common/Ticker.js'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { Event } from '@utils/GoogleAnalytics'; import { convertToEcashPrefix } from '@utils/cashMethods'; export const StyledRawQRCode = styled(RawQRCode)` cursor: pointer; border-radius: 26px; background: ${props => props.theme.qr.background}; box-shadow: ${props => props.theme.qr.shadow}; margin-bottom: 10px; path:first-child { fill: ${props => props.theme.qr.background}; } :hover { border-color: ${({ xec = 0, ...props }) => xec === 1 ? props.theme.primary : props.theme.qr.token}; } @media (max-width: 768px) { border-radius: 18px; width: 170px; height: 170px; } `; const Copied = styled.div` font-size: 18px; font-weight: bold; width: 100%; text-align: center; background-color: ${({ xec = 0, ...props }) => xec === 1 ? props.theme.primary : props.theme.qr.token}; border: 1px solid; border-color: ${({ xec = 0, ...props }) => xec === 1 ? props.theme.qr.copyBorderCash : props.theme.qr.copyBorderToken}; color: ${props => props.theme.contrast}; position: absolute; top: 65px; padding: 30px 0; @media (max-width: 768px) { top: 52px; padding: 20px 0; } `; const PrefixLabel = styled.span` text-align: right; font-size: 14px; font-weight: bold; @media (max-width: 768px) { font-size: 12px; } @media (max-width: 400px) { font-size: 10px; } -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; `; const AddressHighlightTrim = styled.span` font-weight: bold; font-size: 14px; @media (max-width: 768px) { font-size: 12px; } @media (max-width: 400px) { font-size: 10px; } -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; `; const CustomInput = styled.div` font-size: 12px; color: ${({ xec = 0, ...props }) => xec === 1 ? props.theme.wallet.text.secondary : props.theme.brandSecondary}; text-align: center; cursor: pointer; margin-bottom: 0px; padding: 6px 0; font-family: 'Roboto Mono', monospace; border-radius: 5px; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; input { border: none; width: 100%; text-align: center; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; cursor: pointer; color: ${props => props.theme.wallet.text.primary}; padding: 10px 0; background: transparent; margin-bottom: 15px; display: none; } input:focus { outline: none; } input::selection { background: transparent; color: ${props => props.theme.wallet.text.primary}; } @media (max-width: 768px) { font-size: 10px; input { font-size: 10px; margin-bottom: 10px; } } @media (max-width: 400px) { font-size: 7px; input { font-size: 10px; margin-bottom: 10px; } } `; export const QRCode = ({ address, + isCashAddress, size = 210, onClick = () => null, ...otherProps }) => { address = address ? convertToEcashPrefix(address) : ''; const [visible, setVisible] = useState(false); const trimAmount = 8; const address_trim = address ? address.length - trimAmount : ''; const addressSplit = address ? address.split(':') : ['']; const addressPrefix = addressSplit[0]; const prefixLength = addressPrefix.length + 1; - const isCash = isValidCashPrefix(address); - const txtRef = React.useRef(null); const handleOnClick = evt => { setVisible(true); setTimeout(() => { setVisible(false); }, 1500); onClick(evt); }; const handleOnCopy = () => { // Event.("Category", "Action", "Label") // xec or etoken? let eventLabel = currency.ticker; - if (address) { - const isToken = isValidTokenPrefix(address); - if (isToken) { - eventLabel = currency.tokenTicker; - } + if (address && !isCashAddress) { + eventLabel = currency.tokenTicker; // Event('Category', 'Action', 'Label') Event('Wallet', 'Copy Address', eventLabel); } setVisible(true); setTimeout(() => { txtRef.current.select(); }, 100); }; return (
Copied
{address}
{address && ( {address.slice(0, prefixLength)} {address.slice( prefixLength, prefixLength + trimAmount, )} {address.slice(prefixLength + trimAmount, address_trim)} {address.slice(-trimAmount)} )}
); }; diff --git a/web/cashtab/src/components/Common/ScanQRCode.js b/web/cashtab/src/components/Common/ScanQRCode.js index a0d19196a..c8d2c83bc 100644 --- a/web/cashtab/src/components/Common/ScanQRCode.js +++ b/web/cashtab/src/components/Common/ScanQRCode.js @@ -1,177 +1,181 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Alert, Modal } from 'antd'; import { ThemedQrcodeOutlined } from '@components/Common/CustomIcons'; +import { errorNotification } from './Notifications'; import styled from 'styled-components'; import { BrowserQRCodeReader } from '@zxing/library'; -import { - currency, - isValidCashPrefix, - isValidTokenPrefix, -} from '@components/Common/Ticker.js'; +import { currency } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; +import { isValidXecAddress, isValidEtokenAddress } from '@utils/validation'; const StyledScanQRCode = styled.span` display: block; `; const StyledModal = styled(Modal)` width: 400px !important; height: 400px !important; .ant-modal-close { top: 0 !important; right: 0 !important; } `; const QRPreview = styled.video` width: 100%; `; const ScanQRCode = ({ loadWithCameraOpen, onScan = () => null, ...otherProps }) => { const [visible, setVisible] = useState(loadWithCameraOpen); const [error, setError] = useState(false); // Use these states to debug video errors on mobile // Note: iOS chrome/brave/firefox does not support accessing camera, will throw error // iOS users can use safari // todo only show scanner with safari //const [mobileError, setMobileError] = useState(false); //const [mobileErrorMsg, setMobileErrorMsg] = useState(false); const [activeCodeReader, setActiveCodeReader] = useState(null); const teardownCodeReader = codeReader => { if (codeReader !== null) { codeReader.reset(); codeReader.stop(); codeReader = null; setActiveCodeReader(codeReader); } }; const parseContent = content => { let type = 'unknown'; let values = {}; - // If what scanner reads from QR code begins with 'bitcoincash:' or 'simpleledger:' or their successor prefixes - if (isValidCashPrefix(content) || isValidTokenPrefix(content)) { + // If what scanner reads from QR code is a valid eCash or eToken address + if (isValidXecAddress(content) || isValidEtokenAddress(content)) { type = 'address'; values = { address: content }; // Event("Category", "Action", "Label") // Track number of successful QR code scans // BCH or slp? let eventLabel = currency.ticker; const isToken = content.split(currency.tokenPrefix).length > 1; if (isToken) { eventLabel = currency.tokenTicker; } Event('ScanQRCode.js', 'Address Scanned', eventLabel); } return { type, values }; }; const scanForQrCode = async () => { const codeReader = new BrowserQRCodeReader(); setActiveCodeReader(codeReader); try { // Need to execute this before you can decode input // eslint-disable-next-line no-unused-vars const videoInputDevices = await codeReader.getVideoInputDevices(); //console.log(`videoInputDevices`, videoInputDevices); //setMobileError(JSON.stringify(videoInputDevices)); // choose your media device (webcam, frontal camera, back camera, etc.) // TODO implement if necessary //const selectedDeviceId = videoInputDevices[0].deviceId; //const previewElem = document.querySelector("#test-area-qr-code-webcam"); - const content = await codeReader.decodeFromInputVideoDevice( - undefined, - 'test-area-qr-code-webcam', - ); - const result = parseContent(content.text); - - // stop scanning and fill form if it's an address - if (result.type === 'address') { - // Hide the scanner - setVisible(false); - onScan(result.values.address); - return teardownCodeReader(codeReader); + let result = { type: 'unknown', values: {} }; + + while (result.type !== 'address') { + const content = await codeReader.decodeFromInputVideoDevice( + undefined, + 'test-area-qr-code-webcam', + ); + result = parseContent(content.text); + if (result.type !== 'address') { + errorNotification( + content.text, + `${content.text} is not a valid eCash address`, + `${content.text} is not a valid eCash address`, + ); + } } + // When you scan a valid address, stop scanning and fill form + // Hide the scanner + setVisible(false); + onScan(result.values.address); + return teardownCodeReader(codeReader); } catch (err) { console.log(`Error in QR scanner:`); console.log(err); console.log(JSON.stringify(err.message)); //setMobileErrorMsg(JSON.stringify(err.message)); setError(err); - teardownCodeReader(codeReader); + return teardownCodeReader(codeReader); } - - // stop scanning after 20s no matter what }; React.useEffect(() => { if (!visible) { setError(false); // Stop the camera if user closes modal if (activeCodeReader !== null) { teardownCodeReader(activeCodeReader); } } else { scanForQrCode(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible]); return ( <> setVisible(!visible)} > setVisible(false)} footer={null} > {visible ? (
{error ? ( <> {/*

{mobileError}

{mobileErrorMsg}

*/} ) : ( )}
) : null}
); }; ScanQRCode.propTypes = { loadWithCameraOpen: PropTypes.bool, onScan: PropTypes.func, }; export default ScanQRCode; diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js index 55dd28f15..6de78aaa7 100644 --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -1,235 +1,202 @@ import mainLogo from '@assets/logo_primary.png'; import tokenLogo from '@assets/logo_secondary.png'; import BigNumber from 'bignumber.js'; export const currency = { name: 'eCash', ticker: 'XEC', logo: mainLogo, legacyPrefix: 'bitcoincash', prefixes: ['ecash'], coingeckoId: 'ecash', defaultFee: 2.01, dustSats: 550, etokenSats: 546, cashDecimals: 2, blockExplorerUrl: 'https://explorer.bitcoinabc.org', tokenExplorerUrl: 'https://explorer.be.cash', blockExplorerUrlTestnet: 'https://texplorer.bitcoinabc.org', tokenName: 'eToken', tokenTicker: 'eToken', tokenIconSubmitApi: 'https://icons.etokens.cash/new', tokenLogo: tokenLogo, tokenPrefixes: ['etoken'], tokenIconsUrl: 'https://etoken-icons.s3.us-west-2.amazonaws.com', txHistoryCount: 10, xecApiBatchSize: 20, defaultSettings: { fiatCurrency: 'usd' }, notificationDurationShort: 3, notificationDurationLong: 5, newTokenDefaultUrl: 'https://cashtab.com/', opReturn: { opReturnPrefixHex: '6a', opReturnAppPrefixLengthHex: '04', opPushDataOne: '4c', appPrefixesHex: { eToken: '534c5000', cashtab: '00746162', cashtabEncrypted: '65746162', }, encryptedMsgCharLimit: 94, unencryptedMsgCharLimit: 160, }, settingsValidation: { fiatCurrency: [ 'usd', 'idr', 'krw', 'cny', 'zar', 'vnd', 'cad', 'nok', 'eur', 'gbp', 'jpy', 'try', 'rub', 'inr', 'brl', 'php', 'ils', 'clp', 'twd', 'hkd', 'bhd', 'sar', 'aud', 'nzd', ], }, fiatCurrencies: { usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, aud: { name: 'Australian Dollar', symbol: '$', slug: 'aud' }, bhd: { name: 'Bahraini Dinar', symbol: 'BD', slug: 'bhd' }, brl: { name: 'Brazilian Real', symbol: 'R$', slug: 'brl' }, gbp: { name: 'British Pound', symbol: '£', slug: 'gbp' }, cad: { name: 'Canadian Dollar', symbol: '$', slug: 'cad' }, clp: { name: 'Chilean Peso', symbol: '$', slug: 'clp' }, cny: { name: 'Chinese Yuan', symbol: '元', slug: 'cny' }, eur: { name: 'Euro', symbol: '€', slug: 'eur' }, hkd: { name: 'Hong Kong Dollar', symbol: 'HK$', slug: 'hkd' }, inr: { name: 'Indian Rupee', symbol: '₹', slug: 'inr' }, idr: { name: 'Indonesian Rupiah', symbol: 'Rp', slug: 'idr' }, ils: { name: 'Israeli Shekel', symbol: '₪', slug: 'ils' }, jpy: { name: 'Japanese Yen', symbol: '¥', slug: 'jpy' }, krw: { name: 'Korean Won', symbol: '₩', slug: 'krw' }, nzd: { name: 'New Zealand Dollar', symbol: '$', slug: 'nzd' }, nok: { name: 'Norwegian Krone', symbol: 'kr', slug: 'nok' }, php: { name: 'Philippine Peso', symbol: '₱', slug: 'php' }, rub: { name: 'Russian Ruble', symbol: 'р.', slug: 'rub' }, twd: { name: 'New Taiwan Dollar', symbol: 'NT$', slug: 'twd' }, sar: { name: 'Saudi Riyal', symbol: 'SAR', slug: 'sar' }, zar: { name: 'South African Rand', symbol: 'R', slug: 'zar' }, try: { name: 'Turkish Lira', symbol: '₺', slug: 'try' }, vnd: { name: 'Vietnamese đồng', symbol: 'đ', slug: 'vnd' }, }, }; export function parseOpReturn(hexStr) { if ( !hexStr || typeof hexStr !== 'string' || hexStr.substring(0, 2) !== currency.opReturn.opReturnPrefixHex ) { return false; } hexStr = hexStr.slice(2); // remove the first byte i.e. 6a /* * @Return: resultArray is structured as follows: * resultArray[0] is the transaction type i.e. eToken prefix, cashtab prefix, external message itself if unrecognized prefix * resultArray[1] is the actual cashtab message or the 2nd part of an external message * resultArray[2 - n] are the additional messages for future protcols */ let resultArray = []; let message = ''; let hexStrLength = hexStr.length; for (let i = 0; hexStrLength !== 0; i++) { // part 1: check the preceding byte value for the subsequent message let byteValue = hexStr.substring(0, 2); let msgByteSize = 0; if (byteValue === currency.opReturn.opPushDataOne) { // if this byte is 4c then the next byte is the message byte size - retrieve the message byte size only msgByteSize = parseInt(hexStr.substring(2, 4), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(4); // strip the 4c + message byte size info } else { // take the byte as the message byte size msgByteSize = parseInt(hexStr.substring(0, 2), 16); // hex base 16 to decimal base 10 hexStr = hexStr.slice(2); // strip the message byte size info } // part 2: parse the subsequent message based on bytesize const msgCharLength = 2 * msgByteSize; message = hexStr.substring(0, msgCharLength); if (i === 0 && message === currency.opReturn.appPrefixesHex.eToken) { // add the extracted eToken prefix to array then exit loop resultArray[i] = currency.opReturn.appPrefixesHex.eToken; break; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtab ) { // add the extracted Cashtab prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtab; } else if ( i === 0 && message === currency.opReturn.appPrefixesHex.cashtabEncrypted ) { // add the Cashtab encryption prefix to array resultArray[i] = currency.opReturn.appPrefixesHex.cashtabEncrypted; } else { // this is either an external message or a subsequent cashtab message loop to extract the message resultArray[i] = message; } // strip out the parsed message hexStr = hexStr.slice(msgCharLength); hexStrLength = hexStr.length; } return resultArray; } -export function isValidCashPrefix(addressString) { - // Note that this function validates prefix only - // Check for prefix included in currency.prefixes array - // For now, validation is handled by converting to bitcoincash: prefix and checksum - // and relying on legacy validation methods of bitcoincash: prefix addresses - - // Also accept an address with no prefix, as some exchanges provide these - for (let i = 0; i < currency.prefixes.length; i += 1) { - // If the addressString being tested starts with an accepted prefix or no prefix at all - if ( - addressString.startsWith(currency.prefixes[i] + ':') || - !addressString.includes(':') - ) { - return true; - } - } - return false; -} - -export function isValidTokenPrefix(addressString) { - // Check for prefix included in currency.tokenPrefixes array - // For now, validation is handled by converting to simpleledger: prefix and checksum - // and relying on legacy validation methods of simpleledger: prefix addresses - - // For token addresses, do not accept an address with no prefix - for (let i = 0; i < currency.tokenPrefixes.length; i += 1) { - if (addressString.startsWith(currency.tokenPrefixes[i] + ':')) { - return true; - } - } - return false; -} - export function parseAddressForParams(addressString) { // Build return obj const addressInfo = { address: '', queryString: null, amount: null, }; // Parse address string for parameters const paramCheck = addressString.split('?'); let cleanAddress = paramCheck[0]; addressInfo.address = cleanAddress; // Check for parameters // only the amount param is currently supported let queryString = null; let amount = null; if (paramCheck.length > 1) { queryString = paramCheck[1]; addressInfo.queryString = queryString; const addrParams = new URLSearchParams(queryString); if (addrParams.has('amount')) { // Amount in satoshis try { amount = new BigNumber(parseInt(addrParams.get('amount'))) .div(10 ** currency.cashDecimals) .toString(); } catch (err) { amount = null; } } } addressInfo.amount = amount; return addressInfo; } diff --git a/web/cashtab/src/components/Common/__tests__/Ticker.test.js b/web/cashtab/src/components/Common/__tests__/Ticker.test.js index 5039198e2..50493d58e 100644 --- a/web/cashtab/src/components/Common/__tests__/Ticker.test.js +++ b/web/cashtab/src/components/Common/__tests__/Ticker.test.js @@ -1,134 +1,60 @@ -import { - isValidCashPrefix, - isValidTokenPrefix, - parseOpReturn, -} from '../Ticker'; +import { parseOpReturn } from '../Ticker'; import { shortCashtabMessageInputHex, longCashtabMessageInputHex, shortExternalMessageInputHex, longExternalMessageInputHex, shortSegmentedExternalMessageInputHex, longSegmentedExternalMessageInputHex, mixedSegmentedExternalMessageInputHex, mockParsedShortCashtabMessageArray, mockParsedLongCashtabMessageArray, mockParsedShortExternalMessageArray, mockParsedLongExternalMessageArray, mockParsedShortSegmentedExternalMessageArray, mockParsedLongSegmentedExternalMessageArray, mockParsedMixedSegmentedExternalMessageArray, eTokenInputHex, mockParsedETokenOutputArray, } from '../__mocks__/mockOpReturnParsedArray'; -test('Rejects cash address with bitcoincash: prefix', async () => { - const result = isValidCashPrefix( - 'bitcoincash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gjykk3wa0', - ); - expect(result).toStrictEqual(false); -}); - -test('Correctly validates cash address with bitcoincash: checksum but no prefix', async () => { - const result = isValidCashPrefix( - 'qphpmfj0qn7znklqhrfn5dq7qh36l3vxavu346vqcl', - ); - expect(result).toStrictEqual(true); -}); - -test('Correctly validates cash address with ecash: checksum but no prefix', async () => { - const result = isValidCashPrefix( - 'qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', - ); - expect(result).toStrictEqual(true); -}); - -test('Correctly validates cash address with ecash: prefix', async () => { - const result = isValidCashPrefix( - 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', - ); - expect(result).toStrictEqual(true); -}); - -test('Rejects token address with simpleledger: prefix', async () => { - const result = isValidTokenPrefix( - 'simpleledger:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', - ); - expect(result).toStrictEqual(false); -}); - -test('Does not accept a valid token address without a prefix', async () => { - const result = isValidTokenPrefix( - 'qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', - ); - expect(result).toStrictEqual(false); -}); - -test('Correctly validates token address with etoken: prefix (prefix only, not checksum)', async () => { - const result = isValidTokenPrefix( - 'etoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', - ); - expect(result).toStrictEqual(true); -}); - -test('Recognizes unaccepted token prefix (prefix only, not checksum)', async () => { - const result = isValidTokenPrefix( - 'wtftoken:qpmytrdsakt0axrrlswvaj069nat3p9s7c8w5tu8gm', - ); - expect(result).toStrictEqual(false); -}); - -test('Knows that acceptable cash prefixes are not tokens', async () => { - const result = isValidTokenPrefix( - 'ecash:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', - ); - expect(result).toStrictEqual(false); -}); - -test('Address with unlisted prefix is invalid', async () => { - const result = isValidCashPrefix( - 'ecashdoge:qqd3qn4zazjhygk5a2vzw2gvqgqwempr4gtfza25mc', - ); - expect(result).toStrictEqual(false); -}); - test('parseOpReturn() successfully parses a short cashtab message', async () => { const result = parseOpReturn(shortCashtabMessageInputHex); expect(result).toStrictEqual(mockParsedShortCashtabMessageArray); }); test('parseOpReturn() successfully parses a long cashtab message where an additional PUSHDATA1 is present', async () => { const result = parseOpReturn(longCashtabMessageInputHex); expect(result).toStrictEqual(mockParsedLongCashtabMessageArray); }); test('parseOpReturn() successfully parses a short external message', async () => { const result = parseOpReturn(shortExternalMessageInputHex); expect(result).toStrictEqual(mockParsedShortExternalMessageArray); }); test('parseOpReturn() successfully parses a long external message where an additional PUSHDATA1 is present', async () => { const result = parseOpReturn(longExternalMessageInputHex); expect(result).toStrictEqual(mockParsedLongExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate short parts', async () => { const result = parseOpReturn(shortSegmentedExternalMessageInputHex); expect(result).toStrictEqual(mockParsedShortSegmentedExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate long parts', async () => { const result = parseOpReturn(longSegmentedExternalMessageInputHex); expect(result).toStrictEqual(mockParsedLongSegmentedExternalMessageArray); }); test('parseOpReturn() successfully parses an external message that is segmented into separate long and short parts', async () => { const result = parseOpReturn(mixedSegmentedExternalMessageInputHex); expect(result).toStrictEqual(mockParsedMixedSegmentedExternalMessageArray); }); test('parseOpReturn() successfully parses an eToken output', async () => { const result = parseOpReturn(eTokenInputHex); expect(result).toStrictEqual(mockParsedETokenOutputArray); }); diff --git a/web/cashtab/src/components/Wallet/Wallet.js b/web/cashtab/src/components/Wallet/Wallet.js index aa88f78b0..0daa0b22b 100644 --- a/web/cashtab/src/components/Wallet/Wallet.js +++ b/web/cashtab/src/components/Wallet/Wallet.js @@ -1,335 +1,333 @@ import React from 'react'; import styled from 'styled-components'; import { WalletContext } from '@utils/context'; import OnBoarding from '@components/OnBoarding/OnBoarding'; import { QRCode } from '@components/Common/QRCode'; import { currency } from '@components/Common/Ticker.js'; import { Link } from 'react-router-dom'; import TokenList from './TokenList'; import TxHistory from './TxHistory'; import ApiError from '@components/Common/ApiError'; import BalanceHeader from '@components/Common/BalanceHeader'; import BalanceHeaderFiat from '@components/Common/BalanceHeaderFiat'; import { LoadingCtn, ZeroBalanceHeader } from '@components/Common/Atoms'; import { getWalletState } from '@utils/cashMethods'; export const Tabs = styled.div` margin: auto; margin-bottom: 12px; display: inline-block; text-align: center; `; export const TabLabel = styled.button` :focus, :active { outline: none; } border: none; background: none; font-size: 20px; cursor: pointer; @media (max-width: 400px) { font-size: 16px; } ${({ active, ...props }) => active && ` color: ${props.theme.primary}; `} `; export const TabLine = styled.div` margin: auto; transition: margin-left 0.5s ease-in-out, width 0.5s 0.1s; height: 4px; border-radius: 5px; background-color: ${props => props.theme.primary}; pointer-events: none; margin-left: 69.5%; width: 29%; ${({ left, ...props }) => left && ` margin-left: 3% width: 63%; `} `; export const TabPane = styled.div` ${({ active }) => !active && ` display: none; `} `; export const SwitchBtnCtn = styled.div` display: flex; align-items: center; justify-content: center; align-content: space-between; margin-bottom: 15px; .nonactiveBtn { color: ${props => props.theme.wallet.text.secondary}; background: ${props => props.theme.wallet.switch.inactive.background} !important; box-shadow: none !important; } .slpActive { background: ${props => props.theme.wallet.switch.activeToken.background} !important; box-shadow: ${props => props.theme.wallet.switch.activeToken.shadow} !important; } `; export const SwitchBtn = styled.div` font-weight: bold; display: inline-block; cursor: pointer; color: ${props => props.theme.contrast}; font-size: 14px; padding: 6px 0; width: 100px; margin: 0 1px; text-decoration: none; background: ${props => props.theme.primary}; box-shadow: ${props => props.theme.wallet.switch.activeCash.shadow}; user-select: none; :first-child { border-radius: 100px 0 0 100px; } :nth-child(2) { border-radius: 0 100px 100px 0; } `; export const Links = styled(Link)` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin: 10px 0 20px 0; border: 1px solid ${props => props.theme.wallet.text.secondary}; padding: 14px 0; display: inline-block; border-radius: 3px; transition: all 200ms ease-in-out; svg { fill: ${props => props.theme.wallet.text.secondary}; } :hover { color: ${props => props.theme.primary}; border-color: ${props => props.theme.primary}; svg { fill: ${props => props.theme.primary}; } } @media (max-width: 768px) { padding: 10px 0; font-size: 14px; } `; export const ExternalLink = styled.a` color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin: 0 0 20px 0; border: 1px solid ${props => props.theme.wallet.text.secondary}; padding: 14px 0; display: inline-block; border-radius: 3px; transition: all 200ms ease-in-out; svg { fill: ${props => props.theme.wallet.text.secondary}; transition: all 200ms ease-in-out; } :hover { color: ${props => props.theme.primary}; border-color: ${props => props.theme.primary}; svg { fill: ${props => props.theme.primary}; } } @media (max-width: 768px) { padding: 10px 0; font-size: 14px; } `; export const AddrSwitchContainer = styled.div` text-align: center; padding: 6px 0 12px 0; `; const WalletInfo = () => { const ContextValue = React.useContext(WalletContext); const { wallet, fiatPrice, apiError, cashtabSettings } = ContextValue; const walletState = getWalletState(wallet); const { balances, parsedTxHistory, tokens } = walletState; - const [address, setAddress] = React.useState('cashAddress'); + const [isCashAddress, setIsCashAddress] = React.useState(true); const [activeTab, setActiveTab] = React.useState('txHistory'); const hasHistory = parsedTxHistory && parsedTxHistory.length > 0; const handleChangeAddress = () => { - setAddress(address === 'cashAddress' ? 'slpAddress' : 'cashAddress'); + setIsCashAddress(!isCashAddress); }; return ( <> {!balances.totalBalance && !apiError && !hasHistory ? ( <> 🎉 Congratulations on your new wallet!{' '} 🎉
Start using the wallet immediately to receive{' '} {currency.ticker} payments, or load it up with{' '} {currency.ticker} to send to others
) : ( <> {fiatPrice !== null && !isNaN(balances.totalBalance) && ( )} )} {apiError && } {wallet && ((wallet.Path245 && wallet.Path145) || wallet.Path1899) && ( <> {wallet.Path1899 ? ( <> ) : ( <> )} )} handleChangeAddress()} - className={ - address !== 'cashAddress' ? 'nonactiveBtn' : null - } + className={isCashAddress ? null : 'nonactiveBtn'} > {currency.ticker} handleChangeAddress()} - className={ - address === 'cashAddress' ? 'nonactiveBtn' : 'slpActive' - } + className={isCashAddress ? 'nonactiveBtn' : 'slpActive'} > {currency.tokenTicker} {hasHistory && parsedTxHistory && ( <> setActiveTab('txHistory')} > Transaction History setActiveTab('tokens')} > eTokens {tokens && tokens.length > 0 ? ( ) : (

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

)}
)} ); }; const Wallet = () => { const ContextValue = React.useContext(WalletContext); const { wallet, previousWallet, loading } = ContextValue; return ( <> {loading ? ( ) : ( <> {(wallet && wallet.Path1899) || (previousWallet && previousWallet.path1899) ? ( ) : ( )} )} ); }; export default Wallet;