diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js index 3ce539366..acac127eb 100644 --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -1,345 +1,381 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import 'antd/dist/antd.less'; -import { Spin } from 'antd'; +import { Modal, Spin } from 'antd'; import { CashLoadingIcon } from '@components/Common/CustomIcons'; import '../index.css'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { theme } from '@assets/styles/theme'; import { FolderOpenFilled, CaretRightOutlined, SettingFilled, AppstoreAddOutlined, } from '@ant-design/icons'; import Wallet from '@components/Wallet/Wallet'; import Tokens from '@components/Tokens/Tokens'; 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_xec.png'; import './App.css'; import { WalletContext } from '@utils/context'; import { isValidStoredWallet } from '@utils/cashMethods'; import WalletLabel from '@components/Common/WalletLabel.js'; import { Route, Redirect, Switch, useLocation, useHistory, } from 'react-router-dom'; // Easter egg imports not used in extension/src/components/App.js import TabCash from '@assets/tabcash.png'; import ABC from '@assets/logo_topright.png'; import { checkForTokenById } from '@utils/tokenMethods.js'; +import { currency } from './Common/Ticker'; 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: ${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: ${props => props.theme.primary}; transition: color 0.3s; 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: ${props => props.theme.contrast} !important; background-color: ${props => props.theme.primary} !important; } #addrSwitch { .ant-switch-checked { background-color: white !important; } } #addrSwitch.ant-switch-checked { background-image: ${props => props.theme.buttons.primary.backgroundImage} !important; } `; const CustomApp = styled.div` text-align: center; font-family: 'Gilroy', sans-serif; background-color: ${props => props.theme.app.background}; `; const Footer = styled.div` z-index: 2; 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 ${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: 475px) { margin: 0 20px; } @media (max-width: 420px) { margin: 0 12px; } @media (max-width: 350px) { margin: 0 8px; } background-color: ${props => props.theme.footer.background}; border: none; font-size: 12px; font-weight: bold; .anticon { display: block; color: ${props => props.theme.footer.navIconInactive}; font-size: 24px; margin-bottom: 6px; } ${({ active, ...props }) => active && ` color: ${props.theme.primary}; .anticon { color: ${props.theme.primary}; } `} `; export const WalletBody = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; min-height: 100vh; background-image: ${props => props.theme.app.sidebars}; background-attachment: fixed; `; export const WalletCtn = styled.div` position: relative; width: 500px; background-color: ${props => props.theme.footerBackground}; min-height: 100vh; padding: 10px 30px 120px 30px; 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 ${props => props.theme.wallet.borders.color}; a { color: ${props => props.theme.wallet.text.secondary}; :hover { color: ${props => props.theme.primary}; } } @media (max-width: 768px) { a { font-size: 12px; } padding: 10px 0 20px; } `; export const CashTabLogo = styled.img` width: 120px; @media (max-width: 768px) { width: 110px; } `; // AbcLogo styled component not included in extension, replaced by open in new tab link export const AbcLogo = styled.img` width: 150px; @media (max-width: 768px) { width: 120px; } `; // Easter egg styled component not used in extension/src/components/App.js export const EasterEgg = styled.img` position: fixed; bottom: -195px; margin: 0; right: 10%; transition-property: bottom; transition-duration: 1.5s; transition-timing-function: ease-out; :hover { bottom: 0; } @media screen and (max-width: 1250px) { display: none; } `; const App = () => { const ContextValue = React.useContext(WalletContext); const { wallet, loading } = ContextValue; const [loadingUtxosAfterSend, setLoadingUtxosAfterSend] = useState(false); // If wallet is unmigrated, do not show page until it has migrated // An invalid wallet will be validated/populated after the next API call, ETA 10s const validWallet = isValidStoredWallet(wallet); const location = useLocation(); const history = useHistory(); const selectedKey = location && location.pathname ? location.pathname.substr(1) : ''; // Easter egg boolean not used in extension/src/components/App.js const hasTab = validWallet ? checkForTokenById( wallet.state.tokens, '50d8292c6255cda7afc6c8566fed3cf42a2794e9619740fe8f4c95431271410e', ) : false; + useEffect(() => { + // If URL is not as specified in currency.appURL in Ticker.js, show a popup + const currentUrl = window.location.hostname; + if (currentUrl !== currency.appUrl) { + console.log( + `Loaded URL ${currentUrl} does not match app URL ${currency.appUrl}!`, + ); + Modal.warning({ + title: 'Cashtab is moving!', + content: ( +
+

+ Cashtab is moving to a new home at{' '} + + Cashtab.com + +

+

+ Please write down your wallet 12-word seed and + import it at the new domain. +

+

+ At the end of the month, cashtabapp.com will + auto-fwd to cashtab.com after one minute. +

+
+ ), + }); + } + }, []); + return ( {/*Begin component not included in extension as desktop only*/} {hasTab && ( )} {/*End component not included in extension as desktop only*/} {/*Begin component not included in extension as replaced by open in tab link*/} {/*Begin component not included in extension as replaced by open in tab link*/} ( )} /> {wallet ? ( ) : null} ); }; export default App; diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js index e06e94612..284372951 100644 --- a/web/cashtab/src/components/Common/Ticker.js +++ b/web/cashtab/src/components/Common/Ticker.js @@ -1,191 +1,192 @@ import mainLogo from '@assets/logo_primary.png'; import tokenLogo from '@assets/logo_secondary.png'; import cashaddr from 'ecashaddrjs'; import BigNumber from 'bignumber.js'; export const currency = { name: 'eCash', ticker: 'XEC', + appUrl: 'cashtabapp.com', 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', tokenLogo: tokenLogo, tokenPrefixes: ['etoken'], tokenIconsUrl: '', //https://tokens.bitcoin.com/32 for BCH SLP txHistoryCount: 5, hydrateUtxoBatchSize: 20, defaultSettings: { fiatCurrency: 'usd' }, settingsValidation: { fiatCurrency: [ 'usd', 'idr', 'krw', 'cny', 'zar', 'vnd', 'cad', 'nok', 'eur', 'gbp', 'jpy', 'try', 'rub', 'inr', 'brl', ], }, fiatCurrencies: { usd: { name: 'US Dollar', symbol: '$', slug: 'usd' }, brl: { name: 'Brazilian Real', symbol: 'R$', slug: 'brl' }, gbp: { name: 'British Pound', symbol: '£', slug: 'gbp' }, cad: { name: 'Canadian Dollar', symbol: '$', slug: 'cad' }, cny: { name: 'Chinese Yuan', symbol: '元', slug: 'cny' }, eur: { name: 'Euro', symbol: '€', slug: 'eur' }, inr: { name: 'Indian Rupee', symbol: '₹', slug: 'inr' }, idr: { name: 'Indonesian Rupiah', symbol: 'Rp', slug: 'idr' }, jpy: { name: 'Japanese Yen', symbol: '¥', slug: 'jpy' }, krw: { name: 'Korean Won', symbol: '₩', slug: 'krw' }, nok: { name: 'Norwegian Krone', symbol: 'kr', slug: 'nok' }, rub: { name: 'Russian Ruble', symbol: 'р.', slug: 'rub' }, 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 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 toLegacy(address) { let testedAddress; let legacyAddress; try { if (isValidCashPrefix(address)) { // Prefix-less addresses may be valid, but the cashaddr.decode function used below // will throw an error without a prefix. Hence, must ensure prefix to use that function. const hasPrefix = address.includes(':'); if (!hasPrefix) { testedAddress = currency.legacyPrefix + ':' + address; } else { testedAddress = address; } // Note: an `ecash:` checksum address with no prefix will not be validated by // parseAddress in Send.js // Only handle the case of prefixless address that is valid `bitcoincash:` address const { type, hash } = cashaddr.decode(testedAddress); legacyAddress = cashaddr.encode(currency.legacyPrefix, type, hash); } else { console.log(`Error: ${address} is not a cash address`); throw new Error( 'Address prefix is not a valid cash address with a prefix from the Ticker.prefixes array', ); } } catch (err) { return err; } return legacyAddress; } export function parseAddress(BCH, addressString, isToken = false) { // Build return obj const addressInfo = { address: '', isValid: false, queryString: null, amount: null, }; // Parse address string for parameters const paramCheck = addressString.split('?'); let cleanAddress = paramCheck[0]; addressInfo.address = cleanAddress; // Validate address let isValidAddress; try { isValidAddress = BCH.Address.isCashAddress(cleanAddress); // Only accept addresses with ecash: prefix const { prefix } = cashaddr.decode(cleanAddress); // If the address does not have a valid prefix or token prefix if ( (!isToken && !currency.prefixes.includes(prefix)) || (isToken && !currency.tokenPrefixes.includes(prefix)) ) { // then it is not a valid destination address for XEC sends isValidAddress = false; } } catch (err) { isValidAddress = false; } addressInfo.isValid = isValidAddress; // 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; }