Page MenuHomePhabricator

D8734.id26515.diff
No OneTemporary

D8734.id26515.diff

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 = 620;
+const NOTIFICATION_WIDTH = 360;
+
+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 (
<>
+ <Modal
+ title="Confirm Send"
+ visible={isModalVisible}
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <p>
+ Are you sure you want to send {formData.value}{' '}
+ {currency.ticker} to {formData.address}?
+ </p>
+ </Modal>
{!balances.totalBalance ? (
<ZeroBalanceHeader>
You currently have 0 {currency.ticker}
@@ -348,9 +412,21 @@
sendBchAmountError ? (
<SecondaryButton>Send</SecondaryButton>
) : (
- <PrimaryButton onClick={() => submit()}>
- Send
- </PrimaryButton>
+ <>
+ {txInfoFromUrl ? (
+ <PrimaryButton
+ onClick={() => showModal()}
+ >
+ Send
+ </PrimaryButton>
+ ) : (
+ <PrimaryButton
+ onClick={() => submit()}
+ >
+ Send
+ </PrimaryButton>
+ )}
+ </>
)}
</div>
{apiError && (

File Metadata

Mime Type
text/plain
Expires
Tue, May 20, 19:41 (8 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5865804
Default Alt Text
D8734.id26515.diff (12 KB)

Event Timeline