diff --git a/web/cashtab/extension/public/manifest.json b/web/cashtab/extension/public/manifest.json --- a/web/cashtab/extension/public/manifest.json +++ b/web/cashtab/extension/public/manifest.json @@ -4,7 +4,18 @@ "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" diff --git a/web/cashtab/extension/src/background.js b/web/cashtab/extension/src/background.js new file mode 100644 --- /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 --- /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 --- a/web/cashtab/scripts/extension.sh +++ b/web/cashtab/scripts/extension.sh @@ -43,6 +43,10 @@ # 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 diff --git a/web/cashtab/src/components/App.css b/web/cashtab/src/components/App.css --- a/web/cashtab/src/components/App.css +++ b/web/cashtab/src/components/App.css @@ -131,7 +131,6 @@ /* .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; @@ -153,7 +152,6 @@ /* .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; @@ -235,7 +233,6 @@ border: 1px solid #17171f; border-radius: 0; } - .ant-alert-message { color: #fff; } @@ -312,7 +309,6 @@ /* .ant-radio-button-wrapper { border: none; } - .ant-radio-button-wrapper-checked { border-radius: none !important; } */ diff --git a/web/cashtab/src/components/App.js b/web/cashtab/src/components/App.js --- a/web/cashtab/src/components/App.js +++ b/web/cashtab/src/components/App.js @@ -124,15 +124,12 @@ 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; diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js --- a/web/cashtab/src/components/Send/Send.js +++ b/web/cashtab/src/components/Send/Send.js @@ -1,8 +1,8 @@ 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, { @@ -87,6 +87,25 @@ 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(); @@ -96,6 +115,40 @@ 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, @@ -264,6 +317,17 @@ return ( <> + +

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

+
{!balances.totalBalance ? ( You currently have 0 {currency.ticker} @@ -348,9 +412,21 @@ sendBchAmountError ? ( Send ) : ( - submit()}> - Send - + <> + {txInfoFromUrl ? ( + showModal()} + > + Send + + ) : ( + submit()} + > + Send + + )} + )} {apiError && (