diff --git a/web/cashtab/src/assets/styles/theme.js b/web/cashtab/src/assets/styles/theme.js index e705ffc57..5b744ab9f 100644 --- a/web/cashtab/src/assets/styles/theme.js +++ b/web/cashtab/src/assets/styles/theme.js @@ -1,6 +1,79 @@ export const theme = { - iconOutlined: '#3e3f42', - formBorders: '#e7edf3', - formAddonBackground: '#f4f4f4', - formAddonForeground: '#3e3f42', + primary: '#ff8d00', + contrast: '#fff', + app: { + sidebars: 'linear-gradient(270deg, #040c3c, #212c6e)', + background: '#fbfbfd', + }, + wallet: { + background: '#fff', + text: { + primary: '#000', + secondary: '#3e3f42', + }, + switch: { + activeCash: { + shadow: + 'inset 8px 8px 16px #d67600, inset -8px -8px 16px #ffa400', + }, + activeToken: { + background: '#5ebd6d', + shadow: + 'inset 5px 5px 11px #4e9d5a, inset -5px -5px 11px #6edd80', + }, + inactive: { + background: 'linear-gradient(145deg, #eeeeee, #c8c8c8)', + }, + }, + borders: { color: '#e2e2e2' }, + shadow: 'rgba(0, 0, 0, 1)', + }, + tokenListItem: { + background: '#ffffff', + color: '', + boxShadow: + 'rgba(0, 0, 0, 0.01) 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 4px 8px,rgba(0, 0, 0, 0.04) 0px 16px 24px, rgba(0, 0, 0, 0.01) 0px 24px 32px', + border: '#e9eaed', + hoverBorder: '#5ebd6d', + }, + footer: { + background: '#fff', + navIconInactive: '#949494', + }, + forms: { + error: '#f04134', + border: '#e7edf3', + addonBackground: '#f4f4f4', + addonForeground: '#3e3f42', + selectionBackground: '#fff', + }, + icons: { outlined: '#3e3f42' }, + modals: { + buttons: { background: '#fff' }, + }, + settings: { delete: 'red' }, + qr: { + background: '#fff', + token: '#5ebd6d', + shadow: + 'rgba(0, 0, 0, 0.01) 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 4px 8px, rgba(0, 0, 0, 0.04) 0px 16px 24px, rgba(0, 0, 0, 0.01) 0px 24px 32px', + }, + buttons: { + primary: { + backgroundImage: + 'linear-gradient(270deg, #ff8d00 0%, #bb5a00 100%)', + color: '#fff', + hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)', + }, + secondary: { + background: '#e9eaed', + color: '#444', + hoverShadow: '0px 3px 10px -5px rgba(0, 0, 0, 0.75)', + }, + }, + collapses: { + background: '#fbfcfd', + border: '#eaedf3', + color: '#3e3f42', + }, }; diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js index 0713c66e3..211cb621d 100644 --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -1,269 +1,272 @@ import React from 'react'; import 'antd/dist/antd.less'; import '../index.css'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { theme } from '@assets/styles/theme'; import { FolderOpenFilled, CaretRightOutlined, SettingFilled, } from '@ant-design/icons'; import Wallet from '@components/Wallet/Wallet'; import Send from '@components/Send/Send'; import SendToken from '@components/Send/SendToken'; import Configure from '@components/Configure/Configure'; import NotFound from '@components/NotFound'; import CashTab from '@assets/cashtab.png'; import TabCash from '@assets/tabcash.png'; import ABC from '@assets/bitcoinabclogo.png'; import './App.css'; import { WalletContext } from '@utils/context'; import { checkForTokenById } from '@utils/tokenMethods.js'; import WalletLabel from '@components/Common/WalletLabel.js'; import { Route, Redirect, Switch, useLocation, useHistory, } from 'react-router-dom'; import fbt from 'fbt'; const GlobalStyle = createGlobalStyle` .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button, .ant-modal > button, .ant-modal-confirm-btns > button, .ant-modal-footer > button { border-radius: 8px; - background-color: #fff; - color: rgb(62, 63, 66); + background-color: ${props => props.theme.modals.buttons.background}; + color: ${props => props.theme.wallet.text.secondary}; font-weight: bold; } .ant-modal-wrap > div > div.ant-modal-content > div > div > div.ant-modal-confirm-btns > button:hover,.ant-modal-confirm-btns > button:hover, .ant-modal-footer > button:hover { - color: #f59332; + color: ${props => props.theme.primary}; transition: color 0.3s; - background-color: white; + background-color: ${props => props.theme.modals.buttons.background}; } - + .selectedCurrencyOption { + text-align: left; + color: ${props => props.theme.wallet.text.secondary} !important; + background-color: ${props => props.theme.contrast} !important; + } + .cashLoadingIcon { + color: ${props => props.theme.primary} !important + font-size: 48px !important; + } .selectedCurrencyOption:hover { - color: #fff !important; - background-color: #ff8d00 !important; + color: ${props => props.theme.contrast} !important; + background-color: ${props => props.theme.primary} !important; } `; const CustomApp = styled.div` text-align: center; font-family: 'Gilroy', sans-serif; - background-color: #fbfbfd; + background-color: ${props => props.theme.app.background}; `; const Footer = styled.div` - background-color: #fff; + background-color: ${props => props.theme.footer.background}; border-radius: 20px; position: fixed; bottom: 0; width: 500px; @media (max-width: 768px) { width: 100%; } - border-top: 1px solid #e2e2e2; + border-top: 1px solid ${props => props.theme.wallet.borders.color}; `; export const NavButton = styled.button` :focus, :active { outline: none; } cursor: pointer; padding: 24px 12px 12px 12px; margin: 0 28px; @media (max-width: 360px) { margin: 0 12px; } - background-color: #fff; + background-color: ${props => props.theme.footer.background}; border: none; font-size: 12px; font-weight: bold; .anticon { display: block; - color: rgb(148, 148, 148); + color: ${props => props.theme.footer.navIconInactive}; font-size: 24px; margin-bottom: 6px; } - ${({ active }) => + ${({ active, ...props }) => active && ` - color: #ff8d00; + color: ${props.theme.primary}; .anticon { - color: #ff8d00; + color: ${props.theme.primary}; } `} `; export const WalletBody = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; min-height: 100vh; - background: linear-gradient(270deg, #040c3c, #212c6e); + background: ${props => props.theme.app.sidebars}; `; export const WalletCtn = styled.div` position: relative; width: 500px; - background-color: #fff; + background-color: ${props => props.theme.footerBackground}; min-height: 100vh; padding: 10px 30px 120px 30px; - background: #fff; - -webkit-box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1); - -moz-box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1); - box-shadow: 0px 0px 24px 1px rgba(0, 0, 0, 1); + background: ${props => props.theme.wallet.background}; + -webkit-box-shadow: 0px 0px 24px 1px ${props => props.theme.wallet.shadow}; + -moz-box-shadow: 0px 0px 24px 1px ${props => props.theme.wallet.shadow}; + box-shadow: 0px 0px 24px 1px ${props => props.theme.wallet.shadow}; @media (max-width: 768px) { width: 100%; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } `; export const HeaderCtn = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; padding: 20px 0 30px; margin-bottom: 20px; justify-content: space-between; - border-bottom: 1px solid #e2e2e2; - a { - color: #848484; - :hover { - color: #ff8d00; - } - } + border-bottom: 1px solid ${props => props.theme.wallet.borders.color}; + @media (max-width: 768px) { a { font-size: 12px; } padding: 10px 0 20px; } `; export const EasterEgg = styled.img` position: fixed; bottom: -195px; margin: 0; right: 10%; transition-property: bottom; transition-duration: 1.5s; transition-timing-function: ease-out; :hover { bottom: 0; } @media screen and (max-width: 1250px) { display: none; } `; export const CashTabLogo = styled.img` width: 120px; @media (max-width: 768px) { width: 110px; } `; export const AbcLogo = styled.img` width: 150px; @media (max-width: 768px) { width: 120px; } `; const App = () => { const ContextValue = React.useContext(WalletContext); const { wallet, tokens } = ContextValue; const hasTab = checkForTokenById( tokens, '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', ); const location = useLocation(); const history = useHistory(); const selectedKey = location && location.pathname ? location.pathname.substr(1) : ''; return ( {hasTab && ( )} ( )} /> {wallet ? ( ) : null} ); }; export default App; diff --git a/web/cashtab/src/components/Common/CustomIcons.js b/web/cashtab/src/components/Common/CustomIcons.js index eb5caf9c4..075e903b9 100644 --- a/web/cashtab/src/components/Common/CustomIcons.js +++ b/web/cashtab/src/components/Common/CustomIcons.js @@ -1,51 +1,44 @@ import * as React from 'react'; import styled from 'styled-components'; import { CopyOutlined, DollarOutlined, LoadingOutlined, WalletOutlined, QrcodeOutlined, } from '@ant-design/icons'; -export const CashLoadingIcon = ( - -); +export const CashLoadingIcon = ; export const ThemedCopyOutlined = styled(CopyOutlined)` - color: ${props => props.theme.iconOutlined} !important; + color: ${props => props.theme.icons.outlined} !important; `; export const ThemedDollarOutlined = styled(DollarOutlined)` - color: ${props => props.theme.iconOutlined} !important; + color: ${props => props.theme.icons.outlined} !important; `; export const ThemedWalletOutlined = styled(WalletOutlined)` - color: ${props => props.theme.iconOutlined} !important; + color: ${props => props.theme.icons.outlined} !important; `; export const ThemedQrcodeOutlined = styled(QrcodeOutlined)` - color: ${props => props.theme.iconOutlined} !important; + color: ${props => props.theme.icons.outlined} !important; `; export const LoadingBlock = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; flex-direction: column; svg { width: 50px; height: 50px; - fill: #ff8d00; + fill: ${props => props.theme.primary}; } `; export const CashLoader = () => ( ); diff --git a/web/cashtab/src/components/Common/EnhancedInputs.js b/web/cashtab/src/components/Common/EnhancedInputs.js index e8803fbc4..00074cb32 100644 --- a/web/cashtab/src/components/Common/EnhancedInputs.js +++ b/web/cashtab/src/components/Common/EnhancedInputs.js @@ -1,265 +1,260 @@ import * as React from 'react'; import { Form, Input, Select } from 'antd'; import { ThemedDollarOutlined, ThemedWalletOutlined, } from '@components/Common/CustomIcons'; import styled, { css } from 'styled-components'; import { ScanQRCode } from './ScanQRCode'; import useBCH from '@hooks/useBCH'; import { currency } from '@components/Common/Ticker.js'; export const AntdFormCss = css` .ant-input-group-addon { background-color: ${props => - props.theme.formAddonBackground} !important; - border: 1px solid ${props => props.theme.formBorders}; - color: ${props => props.theme.formAddonForeground} !important; + props.theme.forms.addonBackground} !important; + border: 1px solid ${props => props.theme.forms.border}; + color: ${props => props.theme.forms.addonForeground} !important; } input.ant-input, .ant-select-selection { - background-color: #fff !important; + background-color: ${props => + props.theme.forms.selectionBackground} !important; box-shadow: none !important; border-radius: 4px; font-weight: bold; - color: rgb(62, 63, 66); + color: ${props => props.theme.wallet.text.secondary}; opacity: 1; height: 50px; } .ant-input-affix-wrapper { - background-color: #fff; - border: 1px solid #eaedf3 !important; + background-color: ${props => props.theme.forms.selectionBackground}; + border: 1px solid ${props => props.theme.wallet.borders.color} !important; } .ant-select-selector { height: 60px !important; - border: 1px solid #eaedf3 !important; + border: 1px solid ${props => props.theme.wallet.borders.color} !important; } .ant-form-item-has-error > div > div.ant-form-item-control-input > div > span > span > span.ant-input-affix-wrapper { - background-color: #fff; - border-color: #f04134 !important; + background-color: ${props => props.theme.forms.selectionBackground}; + border-color: ${props => props.theme.forms.error} !important; } .ant-form-item-has-error .ant-input, .ant-form-item-has-error .ant-input-affix-wrapper, .ant-form-item-has-error .ant-input:hover, .ant-form-item-has-error .ant-input-affix-wrapper:hover { - background-color: #fff; - border-color: #f04134 !important; + background-color: ${props => props.theme.forms.selectionBackground}; + border-color: ${props => props.theme.forms.error} !important; } .ant-form-item-has-error .ant-select:not(.ant-select-disabled):not(.ant-select-customize-input) .ant-select-selector { - background-color: #fff; - border-color: #f04134 !important; + background-color: ${props => props.theme.forms.selectionBackground}; + border-color: ${props => props.theme.forms.error} !important; } .ant-select-single .ant-select-selector .ant-select-selection-item, .ant-select-single .ant-select-selector .ant-select-selection-placeholder { line-height: 60px; text-align: left; - color: #3e3f42; + color: ${props => props.theme.wallet.text.secondary}; font-weight: bold; } `; export const AntdFormWrapper = styled.div` ${AntdFormCss} `; export const InputAddonText = styled.span` width: 100%; height: 100%; display: block; ${props => props.disabled ? ` cursor: not-allowed; ` : `cursor: pointer;`} `; export const InputNumberAddonText = styled.span` - background-color: ${props => - props.theme.formAddonBackground} !important; - border: border: 1px solid ${props => props.theme.formBorders}; - color: ${props => props.theme.formAddonForeground} !important; - height: 50px; - line-height: 47px; + background-color: ${props => props.theme.forms.addonBackground} !important; + border: 1px solid ${props => props.theme.forms.border}; + color: ${props => props.theme.forms.addonForeground} !important; + height: 50px; + line-height: 47px; - * { - color: ${props => props.theme.formAddonForeground} !important; - } - ${props => - props.disabled - ? ` + * { + color: ${props => props.theme.forms.addonForeground} !important; + } + ${props => + props.disabled + ? ` cursor: not-allowed; ` - : `cursor: pointer;`} - `; + : `cursor: pointer;`} +`; export const SendBchInput = ({ onMax, inputProps, selectProps, ...otherProps }) => { const { Option } = Select; const currencies = [ { value: currency.ticker, label: currency.ticker, }, { value: 'USD', label: 'USD' }, ]; const currencyOptions = currencies.map(currency => { return ( ); }); const CurrencySelect = ( ); return ( ) : ( ) } {...inputProps} /> {CurrencySelect} max ); }; export const FormItemWithMaxAddon = ({ onMax, inputProps, ...otherProps }) => { return ( } addonAfter={ max } {...inputProps} /> ); }; // loadWithCameraOpen prop: if true, load page with camera scanning open export const FormItemWithQRCodeAddon = ({ onScan, loadWithCameraOpen, inputProps, ...otherProps }) => { return ( } autoComplete="off" addonAfter={ } {...inputProps} /> ); }; export const AddressValidators = () => { const { BCH } = useBCH(); return { safelyDetectAddressFormat: value => { try { return BCH.Address.detectAddressFormat(value); } catch (error) { return null; } }, isSLPAddress: value => AddressValidators.safelyDetectAddressFormat(value) === 'slpaddr', isBCHAddress: value => AddressValidators.safelyDetectAddressFormat(value) === 'cashaddr', isLegacyAddress: value => AddressValidators.safelyDetectAddressFormat(value) === 'legacy', }(); }; diff --git a/web/cashtab/src/components/Common/PrimaryButton.js b/web/cashtab/src/components/Common/PrimaryButton.js index 9819e0882..2ffb0c690 100644 --- a/web/cashtab/src/components/Common/PrimaryButton.js +++ b/web/cashtab/src/components/Common/PrimaryButton.js @@ -1,88 +1,103 @@ import styled from 'styled-components'; const PrimaryButton = styled.button` border: none; - color: #fff; - background-image: linear-gradient(270deg, #ff8d00 0%, #bb5a00 100%); + color: ${props => props.theme.buttons.primary.color}; + background-image: ${props => props.theme.buttons.primary.backgroundImage}; transition: all 0.5s ease; background-size: 200% auto; font-size: 18px; width: 100%; padding: 20px 0; border-radius: 4px; margin-bottom: 20px; cursor: pointer; :hover { background-position: right center; - -webkit-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - -moz-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); + -webkit-box-shadow: ${props => props.theme.buttons.primary.hoverShadow}; + -moz-box-shadow: ${props => props.theme.buttons.primary.hoverShadow}; + box-shadow: ${props => props.theme.buttons.primary.hoverShadow}; } svg { - fill: #fff; + fill: ${props => props.theme.buttons.primary.color}; } @media (max-width: 768px) { font-size: 16px; padding: 15px 0; } `; const SecondaryButton = styled.button` border: none; - color: #444; - background: #e9eaed; + color: ${props => props.theme.buttons.secondary.color}; + background: ${props => props.theme.buttons.secondary.background}; transition: all 0.5s ease; font-size: 18px; width: 100%; padding: 15px 0; border-radius: 4px; cursor: pointer; outline: none; margin-bottom: 20px; :hover { - -webkit-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - -moz-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); + -webkit-box-shadow: ${props => + props.theme.buttons.secondary.hoverShadow}; + -moz-box-shadow: ${props => props.theme.buttons.secondary.hoverShadow}; + box-shadow: ${props => props.theme.buttons.secondary.hoverShadow}; } svg { - fill: #444; + fill: ${props => props.theme.buttons.secondary.color}; } @media (max-width: 768px) { font-size: 16px; padding: 12px 0; } `; const SmartButton = styled.button` - background-image: ${({ disabled = false }) => + ${({ disabled = false, ...props }) => disabled === true - ? 'none' - : 'linear-gradient(270deg, #ff8d00 0%, #bb5a00 100%);'}; - color: ${({ disabled = false }) => (disabled === true ? '#444;' : '#fff;')}; - background: ${({ disabled = false }) => - disabled === true ? '#e9eaed;' : ''}; + ? ` + background-image: 'none'; + color: ${props.theme.buttons.secondary.color}; + background: ${props.theme.buttons.secondary.background}; + :hover { + -webkit-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); + -moz-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); + box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); + } + svg { + fill: ${props.theme.buttons.secondary.color}; + } + ` + : ` + background-image: ${props.theme.buttons.primary.backgroundImage}; + color: ${props.theme.buttons.primary.color}; + :hover { + background-position: right center; + -webkit-box-shadow: ${props.theme.buttons.primary.hoverShadow}; + -moz-box-shadow: ${props.theme.buttons.primary.hoverShadow}; + box-shadow: ${props.theme.buttons.primary.hoverShadow}; + svg { + fill: ${props.theme.buttons.primary.color}; + } + }`} + border: none; transition: all 0.5s ease; font-size: 18px; width: 100%; padding: 15px 0; border-radius: 4px; cursor: pointer; outline: none; margin-bottom: 20px; - :hover { - -webkit-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - -moz-box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - box-shadow: 0px 3px 10px -5px rgba(0, 0, 0, 0.75); - } - svg { - fill: #444; - } + @media (max-width: 768px) { font-size: 16px; padding: 12px 0; } `; export default PrimaryButton; export { SecondaryButton, SmartButton }; diff --git a/web/cashtab/src/components/Common/QRCode.js b/web/cashtab/src/components/Common/QRCode.js index 248562d54..e6d773e41 100644 --- a/web/cashtab/src/components/Common/QRCode.js +++ b/web/cashtab/src/components/Common/QRCode.js @@ -1,214 +1,215 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import RawQRCode from 'qrcode.react'; import { currency } from '@components/Common/Ticker.js'; import { CopyToClipboard } from 'react-copy-to-clipboard'; import { Event } from '@utils/GoogleAnalytics'; export const StyledRawQRCode = styled(RawQRCode)` cursor: pointer; border-radius: 23px; - background: #ffffff; - box-shadow: rgba(0, 0, 0, 0.01) 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 4px 8px, - rgba(0, 0, 0, 0.04) 0px 16px 24px, rgba(0, 0, 0, 0.01) 0px 24px 32px; + background: ${props => props.theme.qr.background}; + box-shadow: ${props => props.theme.qr.shadow}; margin-bottom: 10px; - border: 1px solid #e9eaed; + border: 1px solid ${props => props.theme.wallet.borders.color}; path:first-child { - fill: #fff; + fill: ${props => props.theme.qr.background}; } :hover { - border-color: ${({ bch = 0 }) => (bch === 1 ? '#ff8d00;' : '#5ebd6d')}; + border-color: ${({ bch = 0, ...props }) => + bch === 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: ${({ bch = 0 }) => (bch === 1 ? '#f59332;' : '#5ebd6d')}; - color: #fff; + background-color: ${({ bch = 0, ...props }) => + bch === 1 ? props.theme.primary : props.theme.qr.token}; + color: ${props => props.theme.contrast}; position: absolute; top: 65px; padding: 30px 0; @media (max-width: 768px) { top: 52px; padding: 20px 0; } `; const CustomInput = styled.div` font-size: 15px; - color: #8e8e8e; + color: ${props => props.theme.wallet.text.secondary}; text-align: center; cursor: pointer; margin-bottom: 15px; padding: 10px 0; font-family: 'Roboto Mono', monospace; border-radius: 5px; span { font-weight: bold; - color: #444; + color: ${props => props.theme.wallet.text.primary}; font-size: 16px; } 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: #444; + 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: #444; + color: ${props => props.theme.wallet.text.primary}; } @media (max-width: 768px) { font-size: 11px; span { font-size: 12px; } input { font-size: 11px; margin-bottom: 10px; } } @media (max-width: 340px) { font-size: 10px; span { font-size: 11px; } input { font-size: 11px; margin-bottom: 10px; } } `; export const QRCode = ({ address, size = 210, onClick = () => null, ...otherProps }) => { const [visible, setVisible] = useState(false); const trimAmount = 6; const address_trim = address ? address.length - trimAmount : ''; const txtRef = React.useRef(null); const handleOnClick = evt => { setVisible(true); setTimeout(() => { setVisible(false); }, 1500); onClick(evt); }; const handleOnCopy = () => { // Event.("Category", "Action", "Label") // BCH or slp? let eventLabel = currency.ticker; if (address) { const isToken = address.includes(currency.tokenPrefix); if (isToken) { eventLabel = currency.tokenTicker; } // Event('Category', 'Action', 'Label') Event('Wallet', 'Copy Address', eventLabel); } setVisible(true); setTimeout(() => { txtRef.current.select(); }, 100); }; return (
Copied {address && ( {address.slice( address.includes('bitcoin') ? 12 : 13, address.includes('bitcoin') ? 12 + trimAmount : 13 + trimAmount, )} {address.slice( address.includes('bitcoin') ? 12 + trimAmount : 13 + trimAmount, address.includes('bitcoin') ? address_trim : address_trim, )} {address.slice(-trimAmount)} )}
); }; diff --git a/web/cashtab/src/components/Common/StyledCollapse.js b/web/cashtab/src/components/Common/StyledCollapse.js index 079fb4e30..38f0c248b 100644 --- a/web/cashtab/src/components/Common/StyledCollapse.js +++ b/web/cashtab/src/components/Common/StyledCollapse.js @@ -1,20 +1,20 @@ import styled from 'styled-components'; import { Collapse } from 'antd'; export const StyledCollapse = styled(Collapse)` - background: #fbfcfd !important; - border: 1px solid #eaedf3 !important; + background: ${props => props.theme.collapses.background} !important; + border: 1px solid ${props => props.theme.collapses.border} !important; .ant-collapse-content { - border: 1px solid #eaedf3; + border: 1px solid ${props => props.theme.collapses.border}; border-top: none; } .ant-collapse-item { border-bottom: none !important; } * { - color: rgb(62, 63, 66) !important; + color: ${props => props.theme.collapses.color} !important; } `; diff --git a/web/cashtab/src/components/Common/StyledOnBoarding.js b/web/cashtab/src/components/Common/StyledOnBoarding.js deleted file mode 100644 index 0ccb2cd39..000000000 --- a/web/cashtab/src/components/Common/StyledOnBoarding.js +++ /dev/null @@ -1,57 +0,0 @@ -import styled from 'styled-components'; - -const StyledOnBoarding = styled.div` - .ant-card { - background: #ffffff; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.04); - overflow: hidden; - - &:hover { - box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); - } - - * { - color: rgb(62, 63, 66); - } - - .ant-card-head { - color: #6e6e6e !important; - background: #fbfcfd; - border-bottom: 1px solid #eaedf3; - } - - .ant-alert { - background: #fbfcfd; - border: 1px solid #eaedf3; - } - } - .ant-card-body { - border: none; - } - .ant-collapse { - background: #fbfcfd; - border: 1px solid #eaedf3; - - .ant-collapse-content { - border: 1px solid #eaedf3; - border-top: none; - - .ant-collapse-content-box { - padding: 6px; - .ant-row.ant-form-item { - margin-bottom: 0px; - } - } - } - - .ant-collapse-item { - border-bottom: 1px solid #eaedf3; - } - - * { - color: rgb(62, 63, 66) !important; - } - } -`; - -export default StyledOnBoarding; diff --git a/web/cashtab/src/components/Common/StyledPage.js b/web/cashtab/src/components/Common/StyledPage.js deleted file mode 100644 index a3de31c48..000000000 --- a/web/cashtab/src/components/Common/StyledPage.js +++ /dev/null @@ -1,50 +0,0 @@ -import styled from 'styled-components'; - -const StyledPage = styled.div` - .ant-card { - background: #ffffff; - box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.04); - overflow: hidden; - - &:hover { - box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); - } - - * { - color: rgb(62, 63, 66); - } - - .ant-card-head { - color: #6e6e6e !important; - background: #fbfcfd; - border-bottom: 1px solid #eaedf3; - } - - .ant-alert { - background: #fbfcfd; - border: 1px solid #eaedf3; - } - } - .ant-card-body { - border: none; - } - .ant-collapse { - background: #fbfcfd; - border: 1px solid #eaedf3; - - .ant-collapse-content { - border: 1px solid #eaedf3; - border-top: none; - } - - .ant-collapse-item { - border-bottom: 1px solid #eaedf3; - } - - * { - color: rgb(62, 63, 66) !important; - } - } -`; - -export default StyledPage; diff --git a/web/cashtab/src/components/Common/WalletLabel.js b/web/cashtab/src/components/Common/WalletLabel.js index 1fba05adb..475e8e480 100644 --- a/web/cashtab/src/components/Common/WalletLabel.js +++ b/web/cashtab/src/components/Common/WalletLabel.js @@ -1,25 +1,25 @@ import * as React from 'react'; import styled from 'styled-components'; const WalletName = styled.h4` font-size: 20px; font-weight: bold; display: inline-block; - color: #ff8d00; + color: ${props => props.theme.primary}; margin-bottom: 0px; @media (max-width: 400px) { font-size: 16px; } `; const WalletLabel = ({ name }) => { return ( <> {name && typeof name === 'string' && ( {name} )} ); }; export default WalletLabel; diff --git a/web/cashtab/src/components/Common/__tests__/QRCode.test.js b/web/cashtab/src/components/Common/__tests__/QRCode.test.js index 715f535c1..4b5f62514 100644 --- a/web/cashtab/src/components/Common/__tests__/QRCode.test.js +++ b/web/cashtab/src/components/Common/__tests__/QRCode.test.js @@ -1,50 +1,60 @@ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; import { QRCode } from '../QRCode'; +import { ThemeProvider } from 'styled-components'; +import { theme } from '@assets/styles/theme'; describe('', () => { jest.useFakeTimers(); it('QRCode copying cash address', async () => { const OnClick = jest.fn(); const { container } = render( - , + + + , ); const qrCodeElement = container.querySelector('#borderedQRCode'); fireEvent.click(qrCodeElement); act(() => { jest.runAllTimers(); }); expect(OnClick).toHaveBeenCalled(); expect(setTimeout).toHaveBeenCalled(); }); it('QRCode copying SLP address', () => { const OnClick = jest.fn(); const { container } = render( - , + + + , ); const qrCodeElement = container.querySelector('#borderedQRCode'); fireEvent.click(qrCodeElement); expect(OnClick).toHaveBeenCalled(); }); it('QRCode without address', () => { - const { container } = render(); + const { container } = render( + + + , + ); const qrCodeElement = container.querySelector('#borderedQRCode'); fireEvent.click(qrCodeElement); expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1500); expect(setTimeout).toHaveBeenCalled(); }); }); diff --git a/web/cashtab/src/components/Common/__tests__/StyledCollapse.test.js b/web/cashtab/src/components/Common/__tests__/StyledCollapse.test.js index dfa77aa85..3d1f37b50 100644 --- a/web/cashtab/src/components/Common/__tests__/StyledCollapse.test.js +++ b/web/cashtab/src/components/Common/__tests__/StyledCollapse.test.js @@ -1,9 +1,15 @@ import React from 'react'; import renderer from 'react-test-renderer'; import { StyledCollapse } from '../StyledCollapse'; +import { ThemeProvider } from 'styled-components'; +import { theme } from '@assets/styles/theme'; test('Render StyledCollapse component', () => { - const component = renderer.create(); + const component = renderer.create( + + + , + ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/web/cashtab/src/components/Common/__tests__/StyledOnBoarding.test.js b/web/cashtab/src/components/Common/__tests__/StyledOnBoarding.test.js deleted file mode 100644 index 34c3fa1e9..000000000 --- a/web/cashtab/src/components/Common/__tests__/StyledOnBoarding.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import StyledOnBoarding from '../StyledOnBoarding'; - -test('Render StyledOnBoarding component', () => { - const component = renderer.create(); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); diff --git a/web/cashtab/src/components/Common/__tests__/StyledPage.test.js b/web/cashtab/src/components/Common/__tests__/StyledPage.test.js deleted file mode 100644 index f9047f1d5..000000000 --- a/web/cashtab/src/components/Common/__tests__/StyledPage.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import StyledPage from '../StyledPage'; - -test('Render StyledPage component', () => { - const component = renderer.create(); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); diff --git a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap b/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap index 67f50c564..dae14d353 100644 --- a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap +++ b/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledCollapse.test.js.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render StyledCollapse component 1`] = `
`; diff --git a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledOnBoarding.test.js.snap b/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledOnBoarding.test.js.snap deleted file mode 100644 index 71ec9dc52..000000000 --- a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledOnBoarding.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render StyledOnBoarding component 1`] = ` -
-`; diff --git a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledPage.test.js.snap b/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledPage.test.js.snap deleted file mode 100644 index bf718e704..000000000 --- a/web/cashtab/src/components/Common/__tests__/__snapshots__/StyledPage.test.js.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render StyledPage component 1`] = ` -
-`; diff --git a/web/cashtab/src/components/Configure/Configure.js b/web/cashtab/src/components/Configure/Configure.js index 0c9e876c4..830b19907 100644 --- a/web/cashtab/src/components/Configure/Configure.js +++ b/web/cashtab/src/components/Configure/Configure.js @@ -1,598 +1,598 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { Collapse, Form, Input, Modal, Spin, Alert } from 'antd'; import { PlusSquareOutlined, WalletFilled, ImportOutlined, LockOutlined, } from '@ant-design/icons'; import { WalletContext } from '@utils/context'; import { StyledCollapse } from '@components/Common/StyledCollapse'; import { AntdFormWrapper } from '@components/Common/EnhancedInputs'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import { CashLoader, CashLoadingIcon, ThemedCopyOutlined, ThemedWalletOutlined, } from '@components/Common/CustomIcons'; import { ReactComponent as Trashcan } from '@assets/trashcan.svg'; import { ReactComponent as Edit } from '@assets/edit.svg'; import { Event } from '@utils/GoogleAnalytics'; const { Panel } = Collapse; const SettingsLink = styled.a` text-decoration: underline; - color: #ff8d00; + color: ${props => props.theme.primary}; :visited { text-decoration: underline; - color: #ff8d00; + color: ${props => props.theme.primary}; } `; const SWRow = styled.div` border-radius: 3px; padding: 10px 0; display: flex; align-items: center; justify-content: center; margin-bottom: 6px; @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const SWName = styled.div` width: 50%; display: flex; align-items: center; justify-content: space-between; word-wrap: break-word; hyphens: auto; @media (max-width: 500px) { width: 100%; justify-content: center; margin-bottom: 15px; } h3 { font-size: 16px; - color: #444; + color: ${props => props.theme.wallet.text.secondary}; margin: 0; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } `; const SWButtonCtn = styled.div` width: 50%; display: flex; align-items: center; justify-content: flex-end; @media (max-width: 500px) { width: 100%; justify-content: center; } button { cursor: pointer; @media (max-width: 768px) { font-size: 14px; } } svg { - stroke: #444; - fill: #444; + stroke: ${props => props.theme.wallet.text.secondary}; + fill: ${props => props.theme.wallet.text.secondary}; width: 25px; height: 25px; margin-right: 20px; cursor: pointer; :first-child:hover { - stroke: #ff8d00; - fill: #ff8d00; + stroke: ${props => props.theme.primary}; + fill: ${props => props.theme.primary}; } :hover { - stroke: red; - fill: red; + stroke: ${props => props.theme.settings.delete}; + fill: ${props => props.theme.settings.delete}; } } `; const AWRow = styled.div` padding: 10px 0; display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; h3 { font-size: 16px; display: inline-block; - color: #444; + color: ${props => props.theme.wallet.text.secondary}; margin: 0; text-align: left; font-weight: bold; @media (max-width: 500px) { font-size: 14px; } } h4 { font-size: 16px; display: inline-block; - color: #ff8d00 !important; + color: ${props => props.theme.primary} !important; margin: 0; text-align: right; } @media (max-width: 500px) { flex-direction: column; margin-bottom: 12px; } `; const StyledConfigure = styled.div` h2 { - color: #444; + color: ${props => props.theme.wallet.text.secondary}; font-size: 25px; } p { - color: #444; + color: ${props => props.theme.wallet.text.secondary}; } `; const StyledSpacer = styled.div` height: 1px; width: 100%; - background-color: #e2e2e2; + background-color: ${props => props.theme.wallet.borders.color}; margin: 60px 0 50px; `; const Configure = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading, apiError } = ContextValue; const { addNewSavedWallet, activateWallet, renameWallet, deleteWallet, validateMnemonic, getSavedWallets, } = ContextValue; const [savedWallets, setSavedWallets] = useState([]); const [formData, setFormData] = useState({ dirty: true, mnemonic: '', }); const [showRenameWalletModal, setShowRenameWalletModal] = useState(false); const [showDeleteWalletModal, setShowDeleteWalletModal] = useState(false); const [walletToBeRenamed, setWalletToBeRenamed] = useState(null); const [walletToBeDeleted, setWalletToBeDeleted] = useState(null); const [newWalletName, setNewWalletName] = useState(''); const [ confirmationOfWalletToBeDeleted, setConfirmationOfWalletToBeDeleted, ] = useState(''); const [newWalletNameIsValid, setNewWalletNameIsValid] = useState(null); const [walletDeleteValid, setWalletDeleteValid] = useState(null); const [seedInput, openSeedInput] = useState(false); const showPopulatedDeleteWalletModal = walletInfo => { setWalletToBeDeleted(walletInfo); setShowDeleteWalletModal(true); }; const showPopulatedRenameWalletModal = walletInfo => { setWalletToBeRenamed(walletInfo); setShowRenameWalletModal(true); }; const cancelRenameWallet = () => { // Delete form value setNewWalletName(''); setShowRenameWalletModal(false); }; const cancelDeleteWallet = () => { setWalletToBeDeleted(null); setConfirmationOfWalletToBeDeleted(''); setShowDeleteWalletModal(false); }; const updateSavedWallets = async activeWallet => { if (activeWallet) { let savedWallets; try { savedWallets = await getSavedWallets(activeWallet); setSavedWallets(savedWallets); } catch (err) { console.log(`Error in getSavedWallets()`); console.log(err); } } }; const [isValidMnemonic, setIsValidMnemonic] = useState(false); useEffect(() => { // Update savedWallets every time the active wallet changes updateSavedWallets(wallet); }, [wallet]); // Need this function to ensure that savedWallets are updated on new wallet creation const updateSavedWalletsOnCreate = async importMnemonic => { // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Create Wallet', 'New'); const walletAdded = await addNewSavedWallet(importMnemonic); if (!walletAdded) { Modal.error({ title: 'This wallet already exists!', content: 'Wallet not added', }); } else { Modal.success({ content: 'Wallet added to your saved wallets', }); } await updateSavedWallets(wallet); }; // Same here // TODO you need to lock UI here until this is complete // Otherwise user may try to load an already-loading wallet, wreak havoc with indexedDB const updateSavedWalletsOnLoad = async walletToActivate => { // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Activate', ''); await activateWallet(walletToActivate); await updateSavedWallets(wallet); }; async function submit() { setFormData({ ...formData, dirty: false, }); // Exit if no user input if (!formData.mnemonic) { return; } // Exit if mnemonic is invalid if (!isValidMnemonic) { return; } // Event("Category", "Action", "Label") // Track number of times a different wallet is activated Event('Configure.js', 'Create Wallet', 'Imported'); updateSavedWalletsOnCreate(formData.mnemonic); } const handleChange = e => { const { value, name } = e.target; // Validate mnemonic on change // Import button should be disabled unless mnemonic is valid setIsValidMnemonic(validateMnemonic(value)); setFormData(p => ({ ...p, [name]: value })); }; const changeWalletName = async () => { if (newWalletName === '' || newWalletName.length > 24) { setNewWalletNameIsValid(false); return; } // Hide modal setShowRenameWalletModal(false); // Change wallet name console.log( `Changing wallet ${walletToBeRenamed.name} name to ${newWalletName}`, ); const renameSuccess = await renameWallet( walletToBeRenamed.name, newWalletName, ); if (renameSuccess) { Modal.success({ content: `Wallet "${walletToBeRenamed.name}" renamed to "${newWalletName}"`, }); } else { Modal.error({ content: `Rename failed. All wallets must have a unique name.`, }); } await updateSavedWallets(wallet); // Clear wallet name for form setNewWalletName(''); }; const deleteSelectedWallet = async () => { if (!walletDeleteValid) { return; } if ( confirmationOfWalletToBeDeleted !== `delete ${walletToBeDeleted.name}` ) { setWalletDeleteValid(false); return; } // Hide modal setShowDeleteWalletModal(false); // Change wallet name console.log(`Deleting wallet "${walletToBeDeleted.name}"`); const walletDeletedSuccess = await deleteWallet(walletToBeDeleted); if (walletDeletedSuccess) { Modal.success({ content: `Wallet "${walletToBeDeleted.name}" successfully deleted`, }); } else { Modal.error({ content: `Error deleting ${walletToBeDeleted.name}.`, }); } await updateSavedWallets(wallet); // Clear wallet delete confirmation from form setConfirmationOfWalletToBeDeleted(''); }; const handleWalletNameInput = e => { const { value } = e.target; // validation if (value && value.length && value.length < 24) { setNewWalletNameIsValid(true); } else { setNewWalletNameIsValid(false); } setNewWalletName(value); }; const handleWalletToDeleteInput = e => { const { value } = e.target; if (value && value === `delete ${walletToBeDeleted.name}`) { setWalletDeleteValid(true); } else { setWalletDeleteValid(false); } setConfirmationOfWalletToBeDeleted(value); }; return ( {walletToBeRenamed !== null && ( cancelRenameWallet()} >
} placeholder="Enter new wallet name" name="newName" value={newWalletName} onChange={e => handleWalletNameInput(e)} />
)} {walletToBeDeleted !== null && ( cancelDeleteWallet()} >
} placeholder={`Type "delete ${walletToBeDeleted.name}" to confirm`} name="walletToBeDeletedInput" value={confirmationOfWalletToBeDeleted} onChange={e => handleWalletToDeleteInput(e) } />
)}

Backup your wallet

{wallet && wallet.mnemonic && (

{wallet && wallet.mnemonic ? wallet.mnemonic : ''}

)}

Manage Wallets

{apiError ? ( <>

An error occured on our end. Reconnecting...

) : ( <> updateSavedWalletsOnCreate()} > New Wallet openSeedInput(!seedInput)} > Import Wallet {seedInput && ( <>

Copy and paste your mnemonic seed phrase below to import an existing wallet

} placeholder="mnemonic (seed phrase)" name="mnemonic" autoComplete="off" onChange={e => handleChange(e)} required /> submit()} > Import
)} )} {savedWallets && savedWallets.length > 0 && ( <>

{wallet.name}

Currently active

{savedWallets.map(sw => (

{sw.name}

showPopulatedRenameWalletModal( sw, ) } /> showPopulatedDeleteWalletModal( sw, ) } />
))}
)} [ Documentation ]
); }; export default Configure; diff --git a/web/cashtab/src/components/Configure/__tests__/Configure.test.js b/web/cashtab/src/components/Configure/__tests__/Configure.test.js index 54f7047de..92bf37144 100644 --- a/web/cashtab/src/components/Configure/__tests__/Configure.test.js +++ b/web/cashtab/src/components/Configure/__tests__/Configure.test.js @@ -1,26 +1,36 @@ import React from 'react'; import renderer from 'react-test-renderer'; import Configure from '../Configure'; +import { ThemeProvider } from 'styled-components'; +import { theme } from '@assets/styles/theme'; let realUseContext; let useContextMock; beforeEach(() => { realUseContext = React.useContext; useContextMock = React.useContext = jest.fn(); }); afterEach(() => { React.useContext = realUseContext; }); test('Configure without a wallet', () => { useContextMock.mockReturnValue({ wallet: undefined }); - const component = renderer.create(); + const component = renderer.create( + + + , + ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); test('Configure with a wallet', () => { useContextMock.mockReturnValue({ wallet: { mnemonic: 'test mnemonic' } }); - const component = renderer.create(); + const component = renderer.create( + + + , + ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap index 3a8a0547f..5acbcb771 100644 --- a/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap +++ b/web/cashtab/src/components/Configure/__tests__/__snapshots__/Configure.test.js.snap @@ -1,449 +1,437 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Configure with a wallet 1`] = `

Backup your wallet

Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.
Click to reveal seed phrase

Manage Wallets

`; exports[`Configure without a wallet 1`] = `

Backup your wallet

Your seed phrase is the only way to restore your wallet. Write it down. Keep it safe.

Manage Wallets

`; diff --git a/web/cashtab/src/components/OnBoarding/OnBoarding.js b/web/cashtab/src/components/OnBoarding/OnBoarding.js index 67a56c406..6ea0e6152 100644 --- a/web/cashtab/src/components/OnBoarding/OnBoarding.js +++ b/web/cashtab/src/components/OnBoarding/OnBoarding.js @@ -1,155 +1,152 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { WalletContext } from '@utils/context'; import { Input, Form, Modal } from 'antd'; import { AntdFormWrapper } from '@components/Common/EnhancedInputs'; import { ExclamationCircleOutlined, PlusSquareOutlined, ImportOutlined, LockOutlined, } from '@ant-design/icons'; -import StyledOnboarding from '@components/Common/StyledOnBoarding'; import PrimaryButton, { SecondaryButton, SmartButton, } from '@components/Common/PrimaryButton'; import { currency } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; export const WelcomeText = styled.p` - color: #444; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin-bottom: 60px; text-align: left; `; export const WelcomeLink = styled.a` text-decoration: underline; - color: #ff8d00; + color: ${props => props.theme.primary}; `; export const OnBoarding = ({ history }) => { const ContextValue = React.useContext(WalletContext); const { createWallet, validateMnemonic } = ContextValue; const [formData, setFormData] = useState({ dirty: true, mnemonic: '', }); const [seedInput, openSeedInput] = useState(false); const [isValidMnemonic, setIsValidMnemonic] = useState(false); const { confirm } = Modal; async function submit() { setFormData({ ...formData, dirty: false, }); if (!formData.mnemonic) { return; } // Event("Category", "Action", "Label") // Track number of created wallets from onboarding Event('Onboarding.js', 'Create Wallet', 'Imported'); createWallet(formData.mnemonic); } const handleChange = e => { const { value, name } = e.target; // Validate mnemonic on change // Import button should be disabled unless mnemonic is valid setIsValidMnemonic(validateMnemonic(value)); setFormData(p => ({ ...p, [name]: value })); }; function showBackupConfirmModal() { confirm({ title: "Don't forget to back up your wallet", icon: , content: `Once your wallet is created you can back it up by writing down your 12-word seed. You can find your seed on the Settings page. If you are browsing in Incognito mode or if you clear your browser history, you will lose any funds that are not backed up!`, okText: 'Okay, make me a wallet!', onOk() { // Event("Category", "Action", "Label") // Track number of created wallets from onboarding Event('Onboarding.js', 'Create Wallet', 'New'); createWallet(); }, }); } return ( <>

Welcome to Cashtab!

Cashtab is an{' '} open source, {' '} non-custodial web wallet for {currency.name}.

Want to learn more?{' '} Check out the Cashtab documentation.
showBackupConfirmModal()}> New Wallet openSeedInput(!seedInput)}> Import Wallet {seedInput && ( - - -
- - } - placeholder="mnemonic (seed phrase)" - name="mnemonic" - autoComplete="off" - onChange={e => handleChange(e)} - required - /> - + + + + } + placeholder="mnemonic (seed phrase)" + name="mnemonic" + autoComplete="off" + onChange={e => handleChange(e)} + required + /> + - submit()} - > - Import - - -
-
+ submit()} + > + Import + + + )} ); }; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index dedef5f78..0e9821d36 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,533 +1,533 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { WalletContext } from '@utils/context'; import { Form, notification, message, Spin, Modal, Alert } from 'antd'; import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; import { Row, Col } from 'antd'; import Paragraph from 'antd/lib/typography/Paragraph'; import PrimaryButton, { SecondaryButton, } from '@components/Common/PrimaryButton'; import { SendBchInput, FormItemWithQRCodeAddon, } from '@components/Common/EnhancedInputs'; import useBCH from '@hooks/useBCH'; import useWindowDimensions from '@hooks/useWindowDimensions'; import { isMobile, isIOS, isSafari } from 'react-device-detect'; import { currency, isValidTokenPrefix, parseAddress, toLegacy, } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; import { fiatToCrypto, shouldRejectAmountInput } from '@utils/validation'; import { formatBalance } from '@utils/cashMethods'; export const BalanceHeader = styled.div` p { - color: #777; + color: ${props => props.theme.wallet.text.secondary} width: 100%; font-size: 14px; margin-bottom: 0px; } h3 { - color: #444; + color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 26px; font-weight: bold; margin-bottom: 0px; } `; export const BalanceHeaderFiat = styled.div` - color: #444; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 18px; margin-bottom: 20px; font-weight: bold; @media (max-width: 768px) { font-size: 16px; } `; export const ZeroBalanceHeader = styled.div` - color: #444; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 14px; margin-bottom: 20px; `; const ConvertAmount = styled.div` - color: #777; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 14px; margin-bottom: 10px; font-weight: bold; @media (max-width: 768px) { font-size: 12px; } `; const SendBCH = ({ filledAddress, callbackTxId }) => { const { wallet, fiatPrice, balances, slpBalancesAndUtxos, apiError, } = React.useContext(WalletContext); // Get device window width // If this is less than 769, the page will open with QR scanner open const { width } = useWindowDimensions(); // Load with QR code open if device is mobile and NOT iOS + anything but safari const scannerSupported = width < 769 && isMobile && !(isIOS && !isSafari); const [formData, setFormData] = useState({ dirty: true, value: '', address: filledAddress || '', }); const [loading, setLoading] = useState(false); const [queryStringText, setQueryStringText] = useState(null); const [sendBchAddressError, setSendBchAddressError] = useState(false); const [sendBchAmountError, setSendBchAmountError] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(currency.ticker); // Support cashtab button from web pages const [txInfoFromUrl, setTxInfoFromUrl] = useState(false); // Show a confirmation modal on transactions created by populating form from web page button const [isModalVisible, setIsModalVisible] = useState(false); const showModal = () => { setIsModalVisible(true); }; const handleOk = () => { setIsModalVisible(false); submit(); }; const handleCancel = () => { setIsModalVisible(false); }; const { getBCH, getRestUrl, sendBch, calcFee } = useBCH(); const BCH = getBCH(); // If the balance has changed, unlock the UI // This is redundant, if backend has refreshed in 1.75s timeout below, UI will already be unlocked useEffect(() => { setLoading(false); }, [balances.totalBalance]); useEffect(() => { // Manually parse for txInfo object on page load when Send.js is loaded with a query string // Do not set txInfo in state if query strings are not present if ( !window.location || !window.location.hash || window.location.hash === '#/send' ) { return; } const txInfoArr = window.location.hash.split('?')[1].split('&'); // Iterate over this to create object const txInfo = {}; for (let i = 0; i < txInfoArr.length; i += 1) { let txInfoKeyValue = txInfoArr[i].split('='); let key = txInfoKeyValue[0]; let value = txInfoKeyValue[1]; txInfo[key] = value; } console.log(`txInfo from page params`, txInfo); setTxInfoFromUrl(txInfo); populateFormsFromUrl(txInfo); }, []); function populateFormsFromUrl(txInfo) { if (txInfo && txInfo.address && txInfo.value) { setFormData({ address: txInfo.address, value: txInfo.value }); } } async function submit() { setFormData({ ...formData, dirty: false, }); if ( !formData.address || !formData.value || Number(formData.value) <= 0 ) { return; } // Event("Category", "Action", "Label") // Track number of BCHA send transactions and whether users // are sending BCHA or USD Event('Send.js', 'Send', selectedCurrency); setLoading(true); const { address, value } = formData; // Get the param-free address let cleanAddress = address.split('?')[0]; // Ensure address has bitcoincash: prefix and checksum cleanAddress = toLegacy(cleanAddress); let hasValidCashPrefix; try { hasValidCashPrefix = cleanAddress.startsWith( currency.legacyPrefix + ':', ); } catch (err) { hasValidCashPrefix = false; console.log(`toLegacy() returned an error:`, cleanAddress); } if (!hasValidCashPrefix) { // set loading to false and set address validation to false // Now that the no-prefix case is handled, this happens when user tries to send // BCHA to an SLPA address setLoading(false); setSendBchAddressError( `Destination is not a valid ${currency.ticker} address`, ); return; } // Calculate the amount in BCH let bchValue = value; if (selectedCurrency === 'USD') { bchValue = fiatToCrypto(value, fiatPrice); } try { const link = await sendBch( BCH, wallet, slpBalancesAndUtxos.nonSlpUtxos, filledAddress || cleanAddress, bchValue, currency.defaultFee, callbackTxId, ); notification.success({ message: 'Success', description: ( Transaction successful. Click or tap here for more details ), duration: 5, }); } catch (e) { // Set loading to false here as well, as balance may not change depending on where error occured in try loop setLoading(false); let message; if (!e.error && !e.message) { message = `Transaction failed: no response from ${getRestUrl()}.`; } else if ( /Could not communicate with full node or other external service/.test( e.error, ) ) { message = 'Could not communicate with API. Please try again.'; } else if ( e.error && e.error.includes( 'too-long-mempool-chain, too many unconfirmed ancestors [limit: 50] (code 64)', ) ) { message = `The ${currency.ticker} you are trying to send has too many unconfirmed ancestors to send (limit 50). Sending will be possible after a block confirmation. Try again in about 10 minutes.`; } else { message = e.message || e.error || JSON.stringify(e); } notification.error({ message: 'Error', description: message, duration: 5, }); console.error(e); } } const handleAddressChange = e => { const { value, name } = e.target; let error = false; let addressString = value; // parse address const addressInfo = parseAddress(BCH, addressString); /* Model addressInfo = { address: '', isValid: false, queryString: '', amount: null, }; */ const { address, isValid, queryString, amount } = addressInfo; // If query string, // Show an alert that only amount and currency.ticker are supported setQueryStringText(queryString); // Is this valid address? if (!isValid) { error = 'Address is not a valid cash address'; // If valid address but token format if (isValidTokenPrefix(address)) { error = `Token addresses are not supported for ${currency.ticker} sends`; } } setSendBchAddressError(error); // Set amount if it's in the query string if (amount !== null) { // Set currency to BCHA setSelectedCurrency(currency.ticker); // Use this object to mimic user input and get validation for the value let amountObj = { target: { name: 'value', value: amount } }; handleBchAmountChange(amountObj); setFormData({ ...formData, value: amount, }); } // Set address field to user input setFormData(p => ({ ...p, [name]: value, })); }; const handleSelectedCurrencyChange = e => { setSelectedCurrency(e); // Clear input field to prevent accidentally sending 1 BCH instead of 1 USD setFormData(p => ({ ...p, value: '' })); }; const handleBchAmountChange = e => { const { value, name } = e.target; let bchValue = value; const error = shouldRejectAmountInput( bchValue, selectedCurrency, fiatPrice, balances.totalBalance, ); setSendBchAmountError(error); setFormData(p => ({ ...p, [name]: value })); }; const onMax = async () => { // Clear amt error setSendBchAmountError(false); // Set currency to BCH setSelectedCurrency(currency.ticker); try { const txFeeSats = calcFee(BCH, slpBalancesAndUtxos.nonSlpUtxos); const txFeeBch = txFeeSats / 10 ** currency.cashDecimals; let value = balances.totalBalance - txFeeBch >= 0 ? (balances.totalBalance - txFeeBch).toFixed( currency.cashDecimals, ) : 0; setFormData({ ...formData, value, }); } catch (err) { console.log(`Error in onMax:`); console.log(err); message.error( 'Unable to calculate the max value due to network errors', ); } }; // Display price in USD below input field for send amount, if it can be calculated let fiatPriceString = ''; if (fiatPrice !== null && !isNaN(formData.value)) { if (selectedCurrency === currency.ticker) { fiatPriceString = `$ ${(fiatPrice * Number(formData.value)).toFixed( 2, )} USD`; } else { fiatPriceString = `${ formData.value ? fiatToCrypto(formData.value, fiatPrice) : '0' } ${currency.ticker}`; } } return ( <>

Are you sure you want to send {formData.value}{' '} {currency.ticker} to {formData.address}?

{!balances.totalBalance ? ( You currently have 0 {currency.ticker}
Deposit some funds to use this feature
) : ( <>

Available balance

{formatBalance(balances.totalBalance)}{' '} {currency.ticker}

{fiatPrice !== null && ( ${(balances.totalBalance * fiatPrice).toFixed(2)}{' '} USD )} )}
handleAddressChange({ target: { name: 'address', value: result, }, }) } inputProps={{ disabled: Boolean(filledAddress), placeholder: `${currency.ticker} Address`, name: 'address', onChange: e => handleAddressChange(e), required: true, value: filledAddress || formData.address, }} > handleBchAmountChange(e), required: true, value: formData.value, }} selectProps={{ value: selectedCurrency, disabled: queryStringText !== null, onChange: e => handleSelectedCurrencyChange(e), }} > = {fiatPriceString}
{!balances.totalBalance || apiError || sendBchAmountError || sendBchAddressError ? ( Send ) : ( <> {txInfoFromUrl ? ( showModal()} > Send ) : ( submit()} > Send )} )}
{queryStringText && ( )} {apiError && ( <>

An error occured on our end. Reconnecting...

)}
); }; export default SendBCH; diff --git a/web/cashtab/src/components/Wallet/TokenList.js b/web/cashtab/src/components/Wallet/TokenList.js index 23d635547..bb0c7e2d2 100644 --- a/web/cashtab/src/components/Wallet/TokenList.js +++ b/web/cashtab/src/components/Wallet/TokenList.js @@ -1,24 +1,29 @@ import React from 'react'; +import styled from 'styled-components'; import TokenListItem from './TokenListItem'; import { Link } from 'react-router-dom'; import { currency } from '@components/Common/Ticker.js'; import { formatBalance } from '@utils/cashMethods'; +export const TokenTitle = styled.h2` + color: ${props => props.theme.wallet.text.secondary}; +`; + const TokenList = ({ tokens }) => { return (
-

{currency.tokenTicker} Tokens

+ {currency.tokenTicker} Tokens {tokens.map(token => ( ))}
); }; export default TokenList; diff --git a/web/cashtab/src/components/Wallet/TokenListItem.js b/web/cashtab/src/components/Wallet/TokenListItem.js index 6605f0dd7..427224e0a 100644 --- a/web/cashtab/src/components/Wallet/TokenListItem.js +++ b/web/cashtab/src/components/Wallet/TokenListItem.js @@ -1,75 +1,74 @@ import React from 'react'; import styled from 'styled-components'; import makeBlockie from 'ethereum-blockies-base64'; import { Img } from 'react-image'; import { currency } from '@components/Common/Ticker'; const TokenIcon = styled.div` height: 32px; width: 32px; `; const BalanceAndTicker = styled.div` font-size: 1rem; `; const Wrapper = styled.div` display: flex; justify-content: space-between; align-items: center; padding: 15px 25px; border-radius: 3px; - background: #ffffff; + background: ${props => props.theme.tokenListItem.background}; margin-bottom: 3px; - box-shadow: rgba(0, 0, 0, 0.01) 0px 0px 1px, rgba(0, 0, 0, 0.04) 0px 4px 8px, - rgba(0, 0, 0, 0.04) 0px 16px 24px, rgba(0, 0, 0, 0.01) 0px 24px 32px; - border: 1px solid #e9eaed; + box-shadow: ${props => props.theme.tokenListItem.boxShadow}; + border: 1px solid ${props => props.theme.tokenListItem.border}; :hover { - border-color: #5ebd6d; + border-color: ${props => props.theme.tokenListItem.hoverBorder}; } `; const TokenListItem = ({ ticker, balance, tokenId }) => { return ( {currency.tokenIconsUrl !== '' ? ( {`identicon } /> ) : ( {`identicon )} {balance} {ticker} ); }; export default TokenListItem; diff --git a/web/cashtab/src/components/Wallet/Wallet.js b/web/cashtab/src/components/Wallet/Wallet.js index 264d018dd..5b219f083 100644 --- a/web/cashtab/src/components/Wallet/Wallet.js +++ b/web/cashtab/src/components/Wallet/Wallet.js @@ -1,285 +1,288 @@ import React from 'react'; import styled from 'styled-components'; import { LinkOutlined, LoadingOutlined } from '@ant-design/icons'; 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 { CashLoader } from '@components/Common/CustomIcons'; import { formatBalance } from '@utils/cashMethods'; export const LoadingCtn = styled.div` width: 100%; display: flex; align-items: center; justify-content: center; height: 400px; flex-direction: column; svg { width: 50px; height: 50px; - fill: #ff8d00; + fill: ${props => props.theme.primary}; } `; export const BalanceHeader = styled.div` - color: #444; + color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 30px; font-weight: bold; @media (max-width: 768px) { font-size: 23px; } `; export const BalanceHeaderFiat = styled.div` - color: #444; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 18px; margin-bottom: 20px; font-weight: bold; @media (max-width: 768px) { font-size: 16px; } `; export const ZeroBalanceHeader = styled.div` - color: #444; + color: ${props => props.theme.wallet.text.primary}; width: 100%; font-size: 14px; margin-bottom: 5px; `; export const SwitchBtnCtn = styled.div` display: flex; align-items: center; justify-content: center; align-content: space-between; margin-bottom: 15px; .nonactiveBtn { - color: #444; - background: linear-gradient(145deg, #eeeeee, #c8c8c8) !important; + color: ${props => props.theme.wallet.text.secondary}; + background: ${props => + props.theme.wallet.switch.inactive.background} !important; box-shadow: none !important; } .slpActive { - background: #5ebd6d !important; - box-shadow: inset 5px 5px 11px #4e9d5a, inset -5px -5px 11px #6edd80 !important; + 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: #ffffff; + color: ${props => props.theme.contrast}; font-size: 14px; padding: 6px 0; width: 100px; margin: 0 1px; text-decoration: none; - background: #ff8d00; - box-shadow: inset 8px 8px 16px #d67600, inset -8px -8px 16px #ffa400; + 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: #444; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin: 10px 0 20px 0; - border: 1px solid #444; + 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: #444; + fill: ${props => props.theme.wallet.text.secondary}; } :hover { - color: #ff8d00; - border-color: #ff8d00; + color: ${props => props.theme.primary}; + border-color: ${props => props.theme.primary}; svg { - fill: #444; + fill: ${props => props.theme.primary}; } } @media (max-width: 768px) { padding: 10px 0; font-size: 14px; } `; export const ExternalLink = styled.a` - color: #444; + color: ${props => props.theme.wallet.text.secondary}; width: 100%; font-size: 16px; margin: 0 0 20px 0; - border: 1px solid #444; + 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: #444; + fill: ${props => props.theme.wallet.text.secondary}; transition: all 200ms ease-in-out; } :hover { - color: #ff8d00; - border-color: #ff8d00; + color: ${props => props.theme.primary}; + border-color: ${props => props.theme.primary}; svg { - fill: #ff8d00; + fill: ${props => props.theme.primary}; } } @media (max-width: 768px) { padding: 10px 0; font-size: 14px; } `; const WalletInfo = () => { const ContextValue = React.useContext(WalletContext); const { wallet, fiatPrice, balances, txHistory, apiError } = ContextValue; const [address, setAddress] = React.useState('cashAddress'); const hasHistory = (txHistory && txHistory[0] && txHistory[0].transactions && txHistory[0].transactions.length > 0) || (txHistory && txHistory[1] && txHistory[1].transactions && txHistory[1].transactions.length > 0); const handleChangeAddress = () => { setAddress(address === 'cashAddress' ? 'slpAddress' : 'cashAddress'); }; 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
0 {currency.ticker} ) : ( <> {formatBalance(balances.totalBalance)} {currency.ticker} {fiatPrice !== null && !isNaN(balances.totalBalance) && ( ${(balances.totalBalance * fiatPrice).toFixed(2)}{' '} USD )} )} {apiError && ( <>

An error occured on our end.

Re-establishing connection...

)} {wallet && ((wallet.Path245 && wallet.Path145) || wallet.Path1899) && ( <> {wallet.Path1899 ? ( ) : ( )} )} handleChangeAddress()} className={ address !== 'cashAddress' ? 'nonactiveBtn' : null } > {currency.ticker} handleChangeAddress()} className={ address === 'cashAddress' ? 'nonactiveBtn' : 'slpActive' } > {currency.tokenTicker} {balances.totalBalance ? ( <> View Transactions ) : null} ); }; const Wallet = () => { const ContextValue = React.useContext(WalletContext); const { wallet, tokens, loading } = ContextValue; return ( <> {loading && ( )} {!loading && wallet.Path245 && } {!loading && wallet.Path245 && tokens && tokens.length > 0 && ( )} {!loading && !wallet.Path245 ? : null} ); }; export default Wallet; diff --git a/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js b/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js index b203dd4fe..bf02c4430 100644 --- a/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js +++ b/web/cashtab/src/components/Wallet/__tests__/Wallet.test.js @@ -1,68 +1,78 @@ import React from 'react'; import renderer from 'react-test-renderer'; +import { ThemeProvider } from 'styled-components'; +import { theme } from '@assets/styles/theme'; import Wallet from '../Wallet'; import { walletWithBalancesAndTokens, walletWithBalancesMock, walletWithoutBalancesMock, } from '../__mocks__/walletAndBalancesMock'; import { BrowserRouter as Router } from 'react-router-dom'; let realUseContext; let useContextMock; beforeEach(() => { realUseContext = React.useContext; useContextMock = React.useContext = jest.fn(); }); afterEach(() => { React.useContext = realUseContext; }); test('Wallet without BCH balance', () => { useContextMock.mockReturnValue(walletWithoutBalancesMock); const component = renderer.create( - - - , + + + + + , ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); test('Wallet with BCH balances', () => { useContextMock.mockReturnValue(walletWithBalancesMock); const component = renderer.create( - - - , + + + + + , ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); test('Wallet with BCH balances and tokens', () => { useContextMock.mockReturnValue(walletWithBalancesAndTokens); const component = renderer.create( - - - , + + + + + , ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); test('Without wallet defined', () => { useContextMock.mockReturnValue({ wallet: {}, balances: { totalBalance: 0 }, }); const component = renderer.create( - - - , + + + + + , ); let tree = component.toJSON(); expect(tree).toMatchSnapshot(); }); diff --git a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap index 6a1023d0e..e1c47e66f 100644 --- a/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap +++ b/web/cashtab/src/components/Wallet/__tests__/__snapshots__/Wallet.test.js.snap @@ -1,508 +1,504 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Wallet with BCH balances 1`] = ` Array [
0.06047469 BCHA
,
$ NaN USD
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, View Transactions , ] `; exports[`Wallet with BCH balances and tokens 1`] = ` Array [
0.06047469 BCHA
,
$ NaN USD
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, View Transactions ,

SLPA Tokens

identicon of tokenId bd1acc4c986de57af8d6d2a64aecad8c30ee80f37ae9d066d758923732ddc9ba
6.001 TBS
, ] `; exports[`Wallet without BCH balance 1`] = ` Array [
🎉 Congratulations on your new wallet! 🎉
Start using the wallet immediately to receive BCHA payments, or load it up with BCHA to send to others
,
0 BCHA
,
Copied
qzagy4 7mvh6qxkvcn3acjnz73rkhkc6y7cpt zgcqy6
,
BCHA
SLPA
, ] `; exports[`Without wallet defined 1`] = ` Array [

Welcome to Cashtab!

,

Cashtab is an open source, non-custodial web wallet for Bitcoin ABC .

Want to learn more? Check out the Cashtab documentation.

, , , ] `;