diff --git a/web/cashtab/extension/public/manifest.json b/web/cashtab/extension/public/manifest.json index 6de20bd5d..3b4238402 100644 --- a/web/cashtab/extension/public/manifest.json +++ b/web/cashtab/extension/public/manifest.json @@ -1,20 +1,31 @@ { "manifest_version": 2, "name": "CashTab", "description": "A browser-integrated BCHA wallet from Bitcoin ABC", "version": "0.0.1", - + "content_scripts": [ + { + "matches": ["file://*/*", "http://*/*", "https://*/*"], + "js": ["contentscript.js"], + "run_at": "document_idle", + "all_frames": true + } + ], + "background": { + "scripts": ["background.js"], + "persistent": false + }, "browser_action": { "default_popup": "index.html", "default_title": "CashTab" }, "icons": { "16": "bch16.png", "48": "bch48.png", "128": "bch128.png", "192": "bch192.png", "512": "bch512.png" }, "content_security_policy": "script-src 'self' https://unpkg.com/minimal-slp-wallet-web-joey 'sha256-03cee7e881f6cdd32ce620d5787d7e6a0eaef8acc55557bded61dd6ad82ff0e6'; object-src 'self'" } diff --git a/web/cashtab/extension/src/background.js b/web/cashtab/extension/src/background.js new file mode 100644 index 000000000..6e79a7ea4 --- /dev/null +++ b/web/cashtab/extension/src/background.js @@ -0,0 +1,113 @@ +const NOTIFICATION_HEIGHT = 600; +const NOTIFICATION_WIDTH = 400; + +let popupIsOpen = false; +let notificationIsOpen = false; +const openMetamaskTabsIDs = {}; +const requestAccountTabIds = {}; + +// This starts listening to the port created with `chrome.runtime.connect` in contentscript.js +chrome.runtime.onConnect.addListener(function (port) { + console.assert(port.name == 'cashtabPort'); + port.onMessage.addListener(function (msg) { + console.log('msg received in background.js'); + console.log(msg.text); + if (msg.text == `CashTab` && msg.txInfo) { + console.log(`Caught, opening popup`); + triggerUi(msg.txInfo); + } + }); +}); + +/** + * Opens the browser popup for user confirmation + */ +/* +Breaking this function down +1) Get all active tabs in browser +2) Determine if the extension UI is currently open +3) If extension is not open AND no other UI triggered popups are open, then open one + +Eventually will need similar model. Note that it actually goes much deeper than this in MetaMask. + +To start, just open a popup +*/ +async function triggerUi(txInfo) { + /* + const tabs = await chrome.getActiveTabs(); + const currentlyActiveCashtabTab = Boolean(tabs.find(tab => openMetamaskTabsIDs[tab.id])); + if (!popupIsOpen && !currentlyActiveCashtabTab) { + await notificationManager.showPopup(); + } + */ + // Open a pop-up + let left = 0; + let top = 0; + try { + const lastFocused = await getLastFocusedWindow(); + // Position window in top right corner of lastFocused window. + top = lastFocused.top; + left = lastFocused.left + (lastFocused.width - NOTIFICATION_WIDTH); + } catch (_) { + // The following properties are more than likely 0, due to being + // opened from the background chrome process for the extension that + // has no physical dimensions + const { screenX, screenY, outerWidth } = window; + top = Math.max(screenY, 0); + left = Math.max(screenX + (outerWidth - NOTIFICATION_WIDTH), 0); + } + + console.log(`txInfo`); + console.log(txInfo); + + const queryString = Object.keys(txInfo) + .map(key => key + '=' + txInfo[key]) + .join('&'); + + // create new notification popup + const popupWindow = await openWindow({ + url: `index.html#/send?${queryString}`, + type: 'popup', + width: NOTIFICATION_WIDTH, + height: NOTIFICATION_HEIGHT, + left, + top, + }); +} + +async function openWindow(options) { + return new Promise((resolve, reject) => { + chrome.windows.create(options, newWindow => { + const error = checkForError(); + if (error) { + return reject(error); + } + return resolve(newWindow); + }); + }); +} + +function checkForError() { + const { lastError } = chrome.runtime; + if (!lastError) { + return undefined; + } + // if it quacks like an Error, its an Error + if (lastError.stack && lastError.message) { + return lastError; + } + // repair incomplete error object (eg chromium v77) + return new Error(lastError.message); +} + +async function getLastFocusedWindow() { + return new Promise((resolve, reject) => { + chrome.windows.getLastFocused(windowObject => { + const error = checkForError(); + if (error) { + return reject(error); + } + return resolve(windowObject); + }); + }); +} diff --git a/web/cashtab/extension/src/contentscript.js b/web/cashtab/extension/src/contentscript.js new file mode 100644 index 000000000..57819f0a5 --- /dev/null +++ b/web/cashtab/extension/src/contentscript.js @@ -0,0 +1,33 @@ +// Insert flag into window object to denote CashTab is available and active as a browser extension +// Could use a div or other approach for now, but emulate MetaMask this way so it is extensible to other items +// Try window object approach +var cashTabInject = document.createElement('script'); +cashTabInject.innerHTML = `window.bitcoinAbc = 'cashtab'`; +document.head.appendChild(cashTabInject); + +// Process page messages +// Chrome extensions communicate with web pages through the DOM +// Page sends a message to itself, chrome extension intercepts it +var port = chrome.runtime.connect({ name: 'cashtabPort' }); +//console.log(`port: ${JSON.stringify(port)}`); +//console.log(port); + +window.addEventListener( + 'message', + function (event) { + if (typeof event.data.text !== 'undefined') { + console.log('Message received:'); + console.log(event.data.text); + } + + // We only accept messages from ourselves + if (event.source != window) return; + + if (event.data.type && event.data.type == 'FROM_PAGE') { + console.log(event); + console.log('Content script received: ' + event.data.text); + port.postMessage(event.data); + } + }, + false, +); diff --git a/web/cashtab/scripts/extension.sh b/web/cashtab/scripts/extension.sh index 4bb6e4205..7e079300e 100755 --- a/web/cashtab/scripts/extension.sh +++ b/web/cashtab/scripts/extension.sh @@ -1,60 +1,64 @@ #!/usr/bin/env bash export LC_ALL=C set -euo pipefail # Build CashTab as a Chrome/Brave browser extension # Create a working directory for stashing non-extension files WORKDIR=$(mktemp -d) echo Using workdir ${WORKDIR} # Delete workdir on script finish function cleanup { echo Deleting workdir ${WORKDIR} rm -rf "${WORKDIR}" } trap cleanup EXIT # Stash web files that require extension changes in workdir mv public/manifest.json ${WORKDIR} mv src/components/App.js ${WORKDIR} mv src/components/App.css ${WORKDIR} # Move extension src files into place for npm build cp extension/src/assets/popout.svg src/assets/ cp extension/public/manifest.json public/ cp extension/src/components/App.js src/components/ cp extension/src/components/App.css src/components/ # Delete the last extension build if [ -d "extension/dist/" ]; then rm -Rf extension/dist/; fi # Build the extension mkdir extension/dist/ echo 'Building Extension...' # Required for extension build rules export INLINE_RUNTIME_CHUNK=false export GENERATE_SOURCEMAP=false npm run build # Copy extension build files to extension/ folder cp -r build/* extension/dist +# Copy other needed extension files +cp extension/src/contentscript.js extension/dist +cp extension/src/background.js extension/dist + # Delete extension build from build/ folder (reserved for web app builds) rm -Rf build # Replace original web files rm src/assets/popout.svg rm public/manifest.json rm src/components/App.js rm src/components/App.css # Note, src/assets/popout.svg does not need to be replaced, not used by web app mv ${WORKDIR}/manifest.json public/ mv ${WORKDIR}/App.js src/components/ mv ${WORKDIR}/App.css src/components/ echo 'Extension built and web files replaced!' \ No newline at end of file diff --git a/web/cashtab/src/components/App.css b/web/cashtab/src/components/App.css index d29c3fbc3..76f037f6e 100644 --- a/web/cashtab/src/components/App.css +++ b/web/cashtab/src/components/App.css @@ -1,468 +1,464 @@ @import '~antd/dist/antd.less'; @import '~@fortawesome/fontawesome-free/css/all.css'; @import url('https://fonts.googleapis.com/css?family=Khula&display=swap&.css'); @font-face { font-family: 'Roboto Mono'; src: local('Roboto Mono'), url(../assets/fonts/RobotoMono-Regular.ttf) format('truetype'); font-weight: normal; } aside::-webkit-scrollbar { width: 0.3em; } aside::-webkit-scrollbar-track { -webkit-box-shadow: inset 0 0 6px #13171f; } aside::-webkit-scrollbar-thumb { background-color: darkgrey; outline: 1px solid slategrey; } /* Hide up and down arros on input type="number" */ /* Chrome, Safari, Edge, Opera */ input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } /* Hide up and down arros on input type="number" */ /* Firefox */ input[type='number'] { -moz-appearance: textfield; } html, body { max-width: 100%; overflow-x: hidden; } .ant-modal-wrap.ant-modal-centered::-webkit-scrollbar { display: none; } .App { text-align: center; font-family: 'Gilroy', sans-serif; background-color: #fbfbfd; } .App-logo { width: 100%; display: block; } .logo img { width: 100%; min-width: 193px; display: block; padding-left: 24px; padding-right: 34px; padding-top: 24px; max-width: 200px; } .ant-list-item-meta .ant-list-item-meta-content { display: flex; } #react-qrcode-logo { border-radius: 8px; } .App-header { background-color: #282c34; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; } .App-link { color: #f59332; } .ant-menu-item-group-title { padding-left: 30px; font-size: 20px !important; font-weight: 500 !important; } .ant-menu-item > span { font-size: 14px !important; font-weight: 500 !important; } .ant-card-actions > li > span:hover, .ant-btn:hover, .ant-btn:focus { color: #f59332; transition: color 0.3s; background-color: white; } .ant-card-actions > li { color: #3e3f42; } .anticon { color: #3e3f42; } .ant-list-item-meta-description, .ant-list-item-meta-title { color: #3e3f42; } .ant-list-item-meta-description > :first-child { right: 20px !important; position: absolute; } .ant-modal-body .ant-list-item-meta { height: 85px; width: 85px; padding-left: 10px; padding-top: 10px; padding-bottom: 20px; overflow: visible !important; } /* .ant-radio-group-solid .ant-radio-button-wrapper { margin-top: 0px; } - .ant-radio-group-solid .ant-radio-button-wrapper-checked { border: none !important; box-shadow: none !important; } */ .identicon { border-radius: 50%; width: 200px; height: 200px; margin-top: -75px; margin-left: -75px; margin-bottom: 20px; box-shadow: 1px 1px 2px 1px #444; } .ant-list-item-meta { width: 40px; height: 40px; } /* .ant-radio-group-solid .ant-radio-button-wrapper-checked { background: #ff8d00 !important; } - .ant-radio-group.ant-radio-group-solid.ant-radio-group-small { font-size: 14px !important; font-weight: 600 !important; vertical-align: middle; border-radius: 100px; overflow: auto; background: rgba(255, 255, 255, 0.5) !important; margin-top: 14px; margin-bottom: 10px; cursor: pointer; } */ .ant-checkbox-inner { border: 1px solid #eaedf3 !important; background: white; } .ant-checkbox-inner::after { border-color: white !important; } .ant-card-bordered { border: 1px solid rgb(234, 237, 243); border-radius: 8px; } .ant-card-actions { border-top: 1px solid rgb(234, 237, 243); border-bottom: 1px solid rgb(234, 237, 243); border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; box-shadow: 0px 5px 8px rgba(0, 0, 0, 0.35); } .ant-input-group-addon { background-color: #f4f4f4 !important; border: 1px solid rgb(234, 237, 243); color: #3e3f42 !important; * { color: #3e3f42 !important; } } .ant-menu-item.ant-menu-item-selected > * { color: #fff !important; } .ant-menu-item.ant-menu-item-selected { border: 0; overflow: hidden; text-align: left; padding-left: 28px; background-color: rgba(255, 255, 255, 0.2) !important; } .ant-btn { border-radius: 8px; background-color: #fff; color: rgb(62, 63, 66); font-weight: bold; } .ant-card-actions > li:not(:last-child) { border-right: 0; } .ant-list-item-meta-avatar > img { margin-left: -12px; transform: translate(0, -6px); } .ant-list-item-meta-avatar > svg { margin-right: -70px; } /* Removing these for ABC SLP warning .ant-alert-warning { background-color: #20242d; border: 1px solid #17171f; border-radius: 0; } - .ant-alert-message { color: #fff; } */ .ant-layout-sider-dark { background: linear-gradient(0deg, #040c3c, #212c6e); } .ant-menu-dark { background: none; } .ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left .anticon.anticon-bars { color: #fff; transform: scale(1.3); } .ant-layout-sider-zero-width-trigger.ant-layout-sider-zero-width-trigger-left { background: #3e3f42; border-radius: 0 8px 8px 0; } .ant-btn-group .ant-btn-primary:first-child:not(:last-child) { border-right-color: transparent !important; } .ant-btn-group .ant-btn-primary:last-child:not(:first-child), .ant-btn-group .ant-btn-primary + .ant-btn-primary { border-left-color: #20242d !important; } .audit { a, a:active { color: #46464a; } a:hover { color: #111117; } } .dividends { a, a:active { color: #111117; } a:hover { color: #46464a; } } .ant-popover-inner-content { color: white; } .ant-modal-body .ant-card { max-width: 100%; } .ant-upload.ant-upload-drag { border: 1px solid #eaedf3; border-radius: 8px; background: #d3d3d3; } .ant-upload-list-item:hover .ant-upload-list-item-info { background-color: #ffffff; } /* .ant-radio-button-wrapper { border: none; } - .ant-radio-button-wrapper-checked { border-radius: none !important; } */ /* .ant-radio-button-wrapper:first-child, .ant-radio-button-wrapper:last-child { border-radius: 0 0 0 0; } */ .ant-radio-group { width: 100%; margin-top: 10px; } .ant-radio-button-wrapper { background: rgba(255, 255, 255, 0.2); width: 104px; border: none; text-align: center; color: #fff; } .ant-radio-button-wrapper:hover { color: #fff; background: rgba(255, 255, 255, 0.3); } .ant-radio-group-small .ant-radio-button-wrapper { height: 35px; line-height: 35px; } .ant-radio-button-wrapper-checked { background: #ff8d00 !important; border: none !important; } .ant-radio-button-wrapper:first-child { border-radius: 100px 0 0 100px; } .ant-radio-button-wrapper:last-child { border-radius: 0 100px 100px 0; } ::selection { background-color: #ff8d00; } @media (max-width: 768px) { .ant-notification { width: 100%; top: 20px !important; max-width: unset; margin-right: 0; } } @media (max-width: 350px) { .ant-select-selection-selected-value { font-size: 10px; } } /*Custom color for CashSpin*/ .ant-spin > span > svg { fill: #ff8d00; } /*Custom Input Fields */ input.ant-input, .ant-select-selection { background-color: #fff !important; box-shadow: none !important; border-radius: 4px; font-weight: bold; color: rgb(62, 63, 66); opacity: 1; height: 50px; } .ant-select-selection:hover { border: 1px solid #eaedf3; } .ant-select-selection-selected-value { color: rgb(62, 63, 66); } .ant-select-dropdown-menu-item { color: #444; background-color: #fff; } .ant-select-dropdown-menu-item-active, .ant-select-dropdown-menu-item:hover { color: #fff; background-color: #ff8d00 !important; } .selectedCurrencyOption:hover { color: #fff !important; background-color: #ff8d00 !important; } .ant-input-affix-wrapper { background-color: #fff; border: 1px solid #eaedf3 !important; } input.ant-input, .ant-select-selection { border: none; } .ant-input::placeholder { text-align: left; } .ant-select-selector { height: 60px !important; border: 1px solid #eaedf3 !important; } /*Revs with updated Antd*/ .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; font-weight: bold; } /* Handle new antd error formatting */ .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; } .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; } .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; } diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js index 28c1c2f2b..f7d3dc107 100644 --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -1,255 +1,252 @@ import React from 'react'; import 'antd/dist/antd.less'; import '../index.css'; import styled from 'styled-components'; import { Tabs } from 'antd'; 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 ABC from '@assets/bitcoinabclogo.png'; import './App.css'; import { WalletContext } from '@utils/context'; import { Route, Redirect, Switch, useLocation, useHistory, } from 'react-router-dom'; import fbt from 'fbt'; const { TabPane } = Tabs; const Footer = styled.div` background-color: #fff; border-radius: 20px; position: fixed; bottom: 0; width: 500px; @media (max-width: 768px) { width: 100%; } border-top: 1px solid #e2e2e2; .ant-tabs-nav-list { } .ant-tabs-top > .ant-tabs-nav::before, .ant-tabs-bottom > .ant-tabs-nav::before, .ant-tabs-top > div > .ant-tabs-nav::before, .ant-tabs-bottom > div > .ant-tabs-nav::before { border-bottom: none; } .ant-tabs-tab { padding: 24px 12px 12px 12px; margin: 0 24px; @media (max-width: 360px) { margin: 0 12px; } span { font-size: 12px; font-weight: bold; } .anticon { display: block; color: rgb(148, 148, 148); font-size: 24px; margin-left: 8px; margin-bottom: 3px; } } .ant-tabs-tab:hover { color: #ff8d00; .anticon { color: #ff8d00; } } .ant-tabs-tab-active > div > span { color: #ff8d00; } .ant-tabs-tab-active.ant-tabs-tab { color: #ff8d00; .anticon { color: #ff8d00; } } .ant-tabs-ink-bar { display: none !important; } .ant-tabs-nav { margin: -3.5px 0 0 0; } `; export const WalletBody = styled.div` display: flex; align-items: center; justify-content: center; width: 100%; min-height: 100vh; background: linear-gradient(270deg, #040c3c, #212c6e); `; export const WalletCtn = styled.div` position: relative; width: 500px; background-color: #fff; 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); @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; } } - @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; } `; export const AbcLogo = styled.img` width: 150px; @media (max-width: 768px) { width: 120px; } `; const App = () => { const ContextValue = React.useContext(WalletContext); const { wallet } = ContextValue; const location = useLocation(); const history = useHistory(); const selectedKey = location && location.pathname ? location.pathname.substr(1) : ''; return (
( )} /> {wallet ? ( ) : null}
); }; export default App; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js index a46b11bbb..706c54ef8 100644 --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,375 +1,451 @@ import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; import { WalletContext } from '@utils/context'; -import { Form, notification, message, Spin } from 'antd'; -import { CashLoader, CashLoadingIcon } from '@components/Common/CustomIcons'; +import { Form, notification, message, Spin, Modal } 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 } from '@components/Common/Ticker.js'; import { Event } from '@utils/GoogleAnalytics'; export const BalanceHeader = styled.div` p { color: #777; width: 100%; font-size: 14px; margin-bottom: 0px; } h3 { color: #444; width: 100%; font-size: 26px; font-weight: bold; margin-bottom: 0px; } `; export const BalanceHeaderFiat = styled.div` color: #444; 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; width: 100%; font-size: 14px; margin-bottom: 20px; `; const ConvertAmount = styled.div` color: #777; 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 [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' + ) { + console.log(`No tx info in URL`); + 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; // Calculate the amount in BCH let bchValue = value; if (selectedCurrency === 'USD') { bchValue = (value / fiatPrice).toFixed(8); } try { const link = await sendBch( BCH, wallet, slpBalancesAndUtxos.nonSlpUtxos, { addresses: [filledAddress || address], values: [bchValue], }, 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 handleChange = e => { const { value, name } = e.target; 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 error = false; let bchValue = value; if (selectedCurrency === 'USD') { bchValue = (value / fiatPrice).toFixed(8); } // Validate value for > 0 if (isNaN(bchValue)) { error = 'Amount must be a number'; } else if (bchValue <= 0) { error = 'Amount must be greater than 0'; } else if (bchValue < 0.00001) { error = `Send amount must be at least 0.00001 ${currency.ticker}`; } else if (bchValue > balances.totalBalance) { error = `Amount cannot exceed your ${currency.ticker} balance`; } else if (!isNaN(bchValue) && bchValue.toString().includes('.')) { if (bchValue.toString().split('.')[1].length > 8) { error = `${currency.ticker} transactions do not support more than 8 decimal places`; } } 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 / 1e8; let value = balances.totalBalance - txFeeBch >= 0 ? (balances.totalBalance - txFeeBch).toFixed(8) : 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 = `${(Number(formData.value) / fiatPrice).toFixed( 8, )} ${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

{balances.totalBalance} {currency.ticker}

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

An error occured on our end. Reconnecting...

)}
); }; export default SendBCH;