aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruno Barbieri <bruno.barbieri@consensys.net>2018-08-11 01:40:06 +0800
committerGitHub <noreply@github.com>2018-08-11 01:40:06 +0800
commite2be22a4b722df608cb764042cc8ade6664414d8 (patch)
tree6a0c48ea06401ea8e06c14630edc7c0157f6e25c
parentbe1d5a7dd959f061b52f475bf8500b943ade786c (diff)
parentd0ccc59e459ecb41742b6e55a1875dfa2a2f9f87 (diff)
downloadtangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.tar
tangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.tar.gz
tangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.tar.bz2
tangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.tar.lz
tangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.tar.xz
tangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.tar.zst
tangerine-wallet-browser-e2be22a4b722df608cb764042cc8ade6664414d8.zip
Merge pull request #4897 from MetaMask/qr-code-scan
QR Code Scanner
-rw-r--r--app/_locales/en/messages.json30
-rw-r--r--app/images/webcam.svg18
-rw-r--r--app/scripts/contentscript.js2
-rw-r--r--app/scripts/lib/enums.js11
-rw-r--r--app/scripts/lib/util.js29
-rw-r--r--app/scripts/platforms/extension.js7
-rw-r--r--old-ui/app/components/transaction-list-item.js2
-rw-r--r--package-lock.json66
-rw-r--r--package.json3
-rw-r--r--ui/app/actions.js39
-rw-r--r--ui/app/components/ens-input.js2
-rw-r--r--ui/app/components/modals/index.scss2
-rw-r--r--ui/app/components/modals/modal.js13
-rw-r--r--ui/app/components/modals/qr-scanner/index.js2
-rw-r--r--ui/app/components/modals/qr-scanner/index.scss83
-rw-r--r--ui/app/components/modals/qr-scanner/qr-scanner.component.js216
-rw-r--r--ui/app/components/modals/qr-scanner/qr-scanner.container.js24
-rw-r--r--ui/app/components/send/send-content/send-content.component.js6
-rw-r--r--ui/app/components/send/send-content/send-to-row/send-to-row.component.js2
-rw-r--r--ui/app/components/send/send.component.js33
-rw-r--r--ui/app/components/send/send.container.js12
-rw-r--r--ui/app/components/send/send.selectors.js5
-rw-r--r--ui/app/components/send/tests/send-container.test.js2
-rw-r--r--ui/app/components/send/to-autocomplete/to-autocomplete.js12
-rw-r--r--ui/app/css/itcss/components/send.scss17
-rw-r--r--ui/app/reducers/app.js10
-rw-r--r--ui/lib/webcam-utils.js36
27 files changed, 662 insertions, 22 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 1b0183c92..62ec4ce37 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -2,6 +2,9 @@
"accept": {
"message": "Accept"
},
+ "accessingYourCamera": {
+ "message": "Accesing your camera..."
+ },
"account": {
"message": "Account"
},
@@ -656,6 +659,12 @@
"notStarted": {
"message": "Not Started"
},
+ "noWebcamFoundTitle": {
+ "message": "Webcam not found"
+ },
+ "noWebcamFound": {
+ "message": "Your computer's webcam was not found. Please try again."
+ },
"oldUI": {
"message": "Old UI"
},
@@ -940,6 +949,12 @@
"info": {
"message": "Info"
},
+ "scanInstructions": {
+ "message": "Place the QR code in front of your camera"
+ },
+ "scanQrCode": {
+ "message": "Scan QR Code"
+ },
"shapeshiftBuy": {
"message": "Buy with Shapeshift"
},
@@ -1059,6 +1074,9 @@
"message": "We had trouble loading your token balances. You can view them ",
"description": "Followed by a link (here) to view token balances"
},
+ "tryAgain": {
+ "message": "Try again"
+ },
"twelveWords": {
"message": "These 12 words are the only way to restore your MetaMask accounts.\nSave them somewhere safe and secret."
},
@@ -1089,6 +1107,15 @@
"unknownNetworkId": {
"message": "Unknown network ID"
},
+ "unknownQrCode": {
+ "message": "Error: We couldn't identify that QR code"
+ },
+ "unknownCameraErrorTitle": {
+ "message": "Ooops! Something went wrong...."
+ },
+ "unknownCameraError": {
+ "message": "There was an error while trying to access you camera. Please try again..."
+ },
"unlock": {
"message": "Unlock"
},
@@ -1135,6 +1162,9 @@
"whatsThis": {
"message": "What's this?"
},
+ "youNeedToAllowCameraAccess": {
+ "message": "You need to allow camera access to use this feature."
+ },
"yourSigRequested": {
"message": "Your signature is being requested"
},
diff --git a/app/images/webcam.svg b/app/images/webcam.svg
new file mode 100644
index 000000000..4b9b58148
--- /dev/null
+++ b/app/images/webcam.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="53px" height="53px" viewBox="0 0 53 53" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
+ <title>webcam</title>
+ <desc>Created with Sketch.</desc>
+ <defs></defs>
+ <g id="QR-Code-Scan" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group-4-Copy" transform="translate(-482.000000, -218.000000)">
+ <g id="webcam" transform="translate(482.000000, 218.000000)">
+ <circle id="Oval" fill="#D5ECFA" cx="26.5" cy="26.5" r="26.5"></circle>
+ <g id="Group" transform="translate(14.000000, 19.000000)" fill="#259DE5">
+ <rect id="Rectangle" x="0" y="0" width="18" height="16"></rect>
+ <polygon id="Triangle" points="19 6.57142857 26 3 26 13 19 9.42857143"></polygon>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index b7496f318..e0a2b0061 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -198,6 +198,6 @@ function blacklistedDomainCheck () {
*/
function redirectToPhishingWarning () {
console.log('MetaMask - routing to Phishing Warning component')
- let extensionURL = extension.runtime.getURL('phishing.html')
+ const extensionURL = extension.runtime.getURL('phishing.html')
window.location.href = extensionURL
}
diff --git a/app/scripts/lib/enums.js b/app/scripts/lib/enums.js
index 0a3afca47..c6d57a1bc 100644
--- a/app/scripts/lib/enums.js
+++ b/app/scripts/lib/enums.js
@@ -2,8 +2,19 @@ const ENVIRONMENT_TYPE_POPUP = 'popup'
const ENVIRONMENT_TYPE_NOTIFICATION = 'notification'
const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen'
+const PLATFORM_BRAVE = 'Brave'
+const PLATFORM_CHROME = 'Chrome'
+const PLATFORM_EDGE = 'Edge'
+const PLATFORM_FIREFOX = 'Firefox'
+const PLATFORM_OPERA = 'Opera'
+
module.exports = {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
+ PLATFORM_BRAVE,
+ PLATFORM_CHROME,
+ PLATFORM_EDGE,
+ PLATFORM_FIREFOX,
+ PLATFORM_OPERA,
}
diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js
index 51e9036cc..d7423f2ad 100644
--- a/app/scripts/lib/util.js
+++ b/app/scripts/lib/util.js
@@ -5,6 +5,11 @@ const {
ENVIRONMENT_TYPE_POPUP,
ENVIRONMENT_TYPE_NOTIFICATION,
ENVIRONMENT_TYPE_FULLSCREEN,
+ PLATFORM_FIREFOX,
+ PLATFORM_OPERA,
+ PLATFORM_CHROME,
+ PLATFORM_EDGE,
+ PLATFORM_BRAVE,
} = require('./enums')
/**
@@ -38,6 +43,29 @@ const getEnvironmentType = (url = window.location.href) => {
}
/**
+ * Returns the platform (browser) where the extension is running.
+ *
+ * @returns {string} the platform ENUM
+ *
+ */
+const getPlatform = _ => {
+ const ua = navigator.userAgent
+ if (ua.search('Firefox') !== -1) {
+ return PLATFORM_FIREFOX
+ } else {
+ if (window && window.chrome && window.chrome.ipcRenderer) {
+ return PLATFORM_BRAVE
+ } else if (ua.search('Edge') !== -1) {
+ return PLATFORM_EDGE
+ } else if (ua.search('OPR') !== -1) {
+ return PLATFORM_OPERA
+ } else {
+ return PLATFORM_CHROME
+ }
+ }
+}
+
+/**
* Checks whether a given balance of ETH, represented as a hex string, is sufficient to pay a value plus a gas fee
*
* @param {object} txParams Contains data about a transaction
@@ -100,6 +128,7 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) {
}
module.exports = {
+ getPlatform,
getStack,
getEnvironmentType,
sufficientBalance,
diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index 0803164e8..71b162dd0 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -24,8 +24,13 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version
}
- openExtensionInBrowser (route = null) {
+ openExtensionInBrowser (route = null, queryString = null) {
let extensionURL = extension.runtime.getURL('home.html')
+
+ if (queryString) {
+ extensionURL += `?${queryString}`
+ }
+
if (route) {
extensionURL += `#${route}`
}
diff --git a/old-ui/app/components/transaction-list-item.js b/old-ui/app/components/transaction-list-item.js
index 015ef6ba6..6ecf7d193 100644
--- a/old-ui/app/components/transaction-list-item.js
+++ b/old-ui/app/components/transaction-list-item.js
@@ -40,7 +40,7 @@ TransactionListItem.prototype.showRetryButton = function () {
const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
- const currentSubmittedTxs = transactions.filter(tx => tx.status === 'submitted')
+ const currentSubmittedTxs = transactions.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[0]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transaction.id
diff --git a/package-lock.json b/package-lock.json
index 99bb67f65..6bee47cd6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1623,6 +1623,15 @@
"@types/react": "*"
}
},
+ "@zxing/library": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.7.0.tgz",
+ "integrity": "sha512-VJ1cJaCWVF8MspnuyaZKGKlrSQLqQ5usgSap8uuCAvWGQ6W6OwN1NeGvnjhT+9hmnwkHK8XjaflvzaDBC7nKnw==",
+ "requires": {
+ "text-encoding": "^0.6.4",
+ "ts-custom-error": "^2.2.1"
+ }
+ },
"JSONStream": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
@@ -4624,9 +4633,9 @@
},
"dependencies": {
"electron-to-chromium": {
- "version": "1.3.52",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz",
- "integrity": "sha1-0tnxJwuko7lnuDHEDvcftNmrXOA="
+ "version": "1.3.55",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.55.tgz",
+ "integrity": "sha1-8VDhCyC3fZ1Br8yjEu/gw7Gn/c4="
}
}
},
@@ -4954,9 +4963,9 @@
"integrity": "sha1-MN/YMAnVcE8C3/s3clBo7RKjZrs="
},
"caniuse-lite": {
- "version": "1.0.30000865",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz",
- "integrity": "sha512-vs79o1mOSKRGv/1pSkp4EXgl4ZviWeYReXw60XfacPU64uQWZwJT6vZNmxRF9O+6zu71sJwMxLK5JXxbzuVrLw=="
+ "version": "1.0.30000874",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000874.tgz",
+ "integrity": "sha512-29nr1EPiHwrJTAHHsEmTt2h+55L8j2GNFdAcYPlRy2NX6iFz7ZZiepVI7kP/QqlnHLq3KvfWpbmGa0d063U09w=="
},
"capture-stack-trace": {
"version": "1.0.0",
@@ -7045,6 +7054,11 @@
}
}
},
+ "detectrtc": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/detectrtc/-/detectrtc-1.3.6.tgz",
+ "integrity": "sha1-2rwDU5gaPadzLelpBxwItt3dW1k="
+ },
"di": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
@@ -8210,7 +8224,7 @@
"dependencies": {
"async-eventemitter": {
"version": "github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c",
- "from": "github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c",
+ "from": "async-eventemitter@github:ahultgren/async-eventemitter#fa06e39e56786ba541c180061dbf2c0a5bbf951c",
"requires": {
"async": "^2.4.0"
}
@@ -8258,7 +8272,7 @@
}
},
"eth-contract-metadata": {
- "version": "github:MetaMask/eth-contract-metadata#f1a59032297fc00b9804de4674667f2955158757",
+ "version": "github:MetaMask/eth-contract-metadata#2da362052a312dc6c72a7eec116abf6284664f50",
"from": "github:MetaMask/eth-contract-metadata#master"
},
"eth-ens-namehash": {
@@ -13227,9 +13241,9 @@
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
},
"node-sass": {
- "version": "4.9.2",
- "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.2.tgz",
- "integrity": "sha512-LdxoJLZutx0aQXHtWIYwJKMj+9pTjneTcLWJgzf2XbGu0q5pRNqW5QvFCEdm3mc5rJOdru/mzln5d0EZLacf6g==",
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.1.tgz",
+ "integrity": "sha512-m6H1I6cHXsHsJ7BIWdnJsz9S9gVMyh+/H2cOTXgl2/2WqyyWlBcl4PHJcqrXo5RZVCfCUFqOtjPN0+0XbVHR5Q==",
"requires": {
"async-foreach": "^0.1.3",
"chalk": "^1.1.1",
@@ -26891,6 +26905,14 @@
"nearley": "^2.7.10"
}
},
+ "rtcpeerconnection-shim": {
+ "version": "1.2.13",
+ "resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.13.tgz",
+ "integrity": "sha512-Xz4zQLZNs9lFBvqbaHGIjLWtyZ1V82ec5r+WNEo7NlIx3zF5M3ytn9mkkfYeZmpE032cNg3Vvf0rP8kNXUNd9w==",
+ "requires": {
+ "sdp": "^2.6.0"
+ }
+ },
"run-async": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
@@ -27209,6 +27231,11 @@
}
}
},
+ "sdp": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-2.8.0.tgz",
+ "integrity": "sha512-wRSES07rAwKWAR7aev9UuClT7kdf9ZTdeUK5gTgHue9vlhs19Fbm3ccNEGJO4y2IitH4/JzS4sdzyPl6H2KQLw=="
+ },
"secp256k1": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.4.0.tgz",
@@ -29897,8 +29924,7 @@
"text-encoding": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz",
- "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
- "dev": true
+ "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk="
},
"text-table": {
"version": "0.2.0",
@@ -30345,6 +30371,11 @@
}
}
},
+ "ts-custom-error": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-2.2.1.tgz",
+ "integrity": "sha512-lHKZtU+PXkVuap6nlFZybIAFLUO8B3jbCs1VynBL8AUSAHfeG6HpztcBTDRp5I+fN5820N9kGg+eTIvr+le2yg=="
+ },
"tslib": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.2.tgz",
@@ -32926,6 +32957,15 @@
}
}
},
+ "webrtc-adapter": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-6.3.2.tgz",
+ "integrity": "sha512-7pFMXpZCka7ScIQyk8Wo+fOr3OlKLtGd6YHqkHVT74zerpY2Siyds8sxsmkE0bNqsi/J1b0vDzN7WpB34dQzAA==",
+ "requires": {
+ "rtcpeerconnection-shim": "^1.2.10",
+ "sdp": "^2.7.0"
+ }
+ },
"websocket": {
"version": "1.0.26",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.26.tgz",
diff --git a/package.json b/package.json
index 16aa143ba..29295a65b 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
},
"dependencies": {
"@material-ui/core": "1.0.0",
+ "@zxing/library": "^0.7.0",
"abi-decoder": "^1.0.9",
"asmcrypto.js": "0.22.0",
"async": "^2.5.0",
@@ -97,6 +98,7 @@
"debounce-stream": "^2.0.0",
"deep-extend": "^0.5.1",
"detect-node": "^2.0.3",
+ "detectrtc": "^1.3.6",
"disc": "^1.3.2",
"dnode": "^1.2.2",
"end-of-stream": "^1.1.0",
@@ -206,6 +208,7 @@
"web3": "^0.20.1",
"web3-provider-engine": "^14.0.5",
"web3-stream-provider": "^3.0.1",
+ "webrtc-adapter": "^6.3.0",
"xtend": "^4.0.1"
},
"devDependencies": {
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 9edb3511a..bd5d25327 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -12,6 +12,7 @@ const { fetchLocale } = require('../i18n-helper')
const log = require('loglevel')
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../app/scripts/lib/enums')
const { hasUnconfirmedTransactions } = require('./helpers/confirm-transaction/util')
+const WebcamUtils = require('../lib/webcam-utils')
var actions = {
_setBackgroundConnection: _setBackgroundConnection,
@@ -33,6 +34,8 @@ var actions = {
ALERT_CLOSE: 'UI_ALERT_CLOSE',
showAlert: showAlert,
hideAlert: hideAlert,
+ QR_CODE_DETECTED: 'UI_QR_CODE_DETECTED',
+ qrCodeDetected,
// network dropdown open
NETWORK_DROPDOWN_OPEN: 'UI_NETWORK_DROPDOWN_OPEN',
NETWORK_DROPDOWN_CLOSE: 'UI_NETWORK_DROPDOWN_CLOSE',
@@ -125,7 +128,8 @@ var actions = {
SHOW_CONF_TX_PAGE: 'SHOW_CONF_TX_PAGE',
SHOW_CONF_MSG_PAGE: 'SHOW_CONF_MSG_PAGE',
SET_CURRENT_FIAT: 'SET_CURRENT_FIAT',
- setCurrentCurrency: setCurrentCurrency,
+ showQrScanner,
+ setCurrentCurrency,
setCurrentAccountTab,
// account detail screen
SHOW_SEND_PAGE: 'SHOW_SEND_PAGE',
@@ -723,6 +727,28 @@ function showInfoPage () {
}
}
+function showQrScanner (ROUTE) {
+ return (dispatch, getState) => {
+ return WebcamUtils.checkStatus()
+ .then(status => {
+ if (!status.environmentReady) {
+ // We need to switch to fullscreen mode to ask for permission
+ global.platform.openExtensionInBrowser(`${ROUTE}`, `scan=true`)
+ } else {
+ dispatch(actions.showModal({
+ name: 'QR_SCANNER',
+ }))
+ }
+ }).catch(e => {
+ dispatch(actions.showModal({
+ name: 'QR_SCANNER',
+ error: true,
+ errorType: e.type,
+ }))
+ })
+ }
+}
+
function setCurrentCurrency (currencyCode) {
return (dispatch) => {
dispatch(actions.showLoadingIndication())
@@ -1809,6 +1835,17 @@ function hideAlert () {
}
}
+/**
+ * This action will receive two types of values via qrCodeData
+ * an object with the following structure {type, values}
+ * or null (used to clear the previous value)
+ */
+function qrCodeDetected (qrCodeData) {
+ return {
+ type: actions.QR_CODE_DETECTED,
+ value: qrCodeData,
+ }
+}
function showLoadingIndication (message) {
return {
diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js
index b9f99b3d1..f538fd555 100644
--- a/ui/app/components/ens-input.js
+++ b/ui/app/components/ens-input.js
@@ -27,6 +27,7 @@ function EnsInput () {
}
EnsInput.prototype.onChange = function (recipient) {
+
const network = this.props.network
const networkHasEnsSupport = getNetworkEnsSupport(network)
@@ -54,6 +55,7 @@ EnsInput.prototype.render = function () {
const opts = extend(props, {
list: 'addresses',
onChange: this.onChange.bind(this),
+ qrScanner: true,
})
return h('div', {
style: { width: '100%', position: 'relative' },
diff --git a/ui/app/components/modals/index.scss b/ui/app/components/modals/index.scss
index e198cca44..0acccf172 100644
--- a/ui/app/components/modals/index.scss
+++ b/ui/app/components/modals/index.scss
@@ -1,5 +1,7 @@
@import './customize-gas/index';
+@import './qr-scanner/index';
+
.modal-container {
width: 100%;
height: 100%;
diff --git a/ui/app/components/modals/modal.js b/ui/app/components/modals/modal.js
index f59825ed1..5dda50e52 100644
--- a/ui/app/components/modals/modal.js
+++ b/ui/app/components/modals/modal.js
@@ -21,6 +21,7 @@ const CustomizeGasModal = require('../customize-gas-modal')
const NotifcationModal = require('./notification-modal')
const ConfirmResetAccount = require('./confirm-reset-account')
const ConfirmRemoveAccount = require('./confirm-remove-account')
+const QRScanner = require('./qr-scanner')
const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification')
@@ -346,6 +347,18 @@ const MODALS = {
borderRadius: '8px',
},
},
+ QR_SCANNER: {
+ contents: h(QRScanner),
+ mobileModalStyle: {
+ ...modalContainerMobileStyle,
+ },
+ laptopModalStyle: {
+ ...modalContainerLaptopStyle,
+ },
+ contentStyle: {
+ borderRadius: '8px',
+ },
+ },
DEFAULT: {
contents: [],
diff --git a/ui/app/components/modals/qr-scanner/index.js b/ui/app/components/modals/qr-scanner/index.js
new file mode 100644
index 000000000..470dee1f4
--- /dev/null
+++ b/ui/app/components/modals/qr-scanner/index.js
@@ -0,0 +1,2 @@
+import QrScanner from './qr-scanner.container'
+module.exports = QrScanner
diff --git a/ui/app/components/modals/qr-scanner/index.scss b/ui/app/components/modals/qr-scanner/index.scss
new file mode 100644
index 000000000..6fa81d51f
--- /dev/null
+++ b/ui/app/components/modals/qr-scanner/index.scss
@@ -0,0 +1,83 @@
+.qr-scanner {
+ width: 100%;
+ height: 100%;
+ background-color: #fff;
+ display: flex;
+ flex-flow: column;
+ border-radius: 8px;
+
+ &__title {
+ font-size: 1.5rem;
+ font-weight: 500;
+ padding: 16px 0;
+ text-align: center;
+ }
+
+ &__content {
+ padding-left: 20px;
+ padding-right: 20px;
+
+ &__video-wrapper {
+ overflow: hidden;
+ width: 100%;
+ height: 275px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ video {
+ transform: scaleX(-1);
+ width: auto;
+ height: 275px;
+ }
+ }
+ }
+
+ &__status {
+ text-align: center;
+ font-size: 14px;
+ padding: 15px;
+ }
+
+ &__image {
+ font-size: 1.5rem;
+ font-weight: 500;
+ padding: 16px 0 0;
+ text-align: center;
+ }
+
+ &__error {
+ text-align: center;
+ font-size: 16px;
+ padding: 15px;
+ }
+
+ &__footer {
+ padding: 20px;
+ flex-direction: row;
+ display: flex;
+
+ button {
+ margin-right: 15px;
+ }
+
+ button:last-of-type {
+ margin-right: 0;
+ background-color: #009eec;
+ border: none;
+ color: #fff;
+ }
+ }
+
+ &__close::after {
+ content: '\00D7';
+ font-size: 35px;
+ color: #9b9b9b;
+ position: absolute;
+ top: 4px;
+ right: 20px;
+ cursor: pointer;
+ font-weight: 300;
+ }
+}
+
diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.component.js b/ui/app/components/modals/qr-scanner/qr-scanner.component.js
new file mode 100644
index 000000000..cb8d07d83
--- /dev/null
+++ b/ui/app/components/modals/qr-scanner/qr-scanner.component.js
@@ -0,0 +1,216 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import { BrowserQRCodeReader } from '@zxing/library'
+import adapter from 'webrtc-adapter' // eslint-disable-line import/no-nodejs-modules, no-unused-vars
+import Spinner from '../../spinner'
+import WebcamUtils from '../../../../lib/webcam-utils'
+import PageContainerFooter from '../../page-container/page-container-footer/page-container-footer.component'
+
+export default class QrScanner extends Component {
+ static propTypes = {
+ hideModal: PropTypes.func.isRequired,
+ qrCodeDetected: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ error: PropTypes.bool,
+ errorType: PropTypes.string,
+ }
+
+ static contextTypes = {
+ t: PropTypes.func,
+ }
+
+ constructor (props, context) {
+ super(props)
+
+ this.state = {
+ ready: false,
+ msg: context.t('accessingYourCamera'),
+ }
+ this.codeReader = null
+ this.permissionChecker = null
+ this.needsToReinit = false
+
+ // Clear pre-existing qr code data before scanning
+ this.props.qrCodeDetected(null)
+ }
+
+ componentDidMount () {
+ this.initCamera()
+ }
+
+ async checkPermisisions () {
+ const { permissions } = await WebcamUtils.checkStatus()
+ if (permissions) {
+ clearTimeout(this.permissionChecker)
+ // Let the video stream load first...
+ setTimeout(_ => {
+ this.setState({
+ ready: true,
+ msg: this.context.t('scanInstructions'),
+ })
+ if (this.needsToReinit) {
+ this.initCamera()
+ this.needsToReinit = false
+ }
+ }, 2000)
+ } else {
+ // Keep checking for permissions
+ this.permissionChecker = setTimeout(_ => {
+ this.checkPermisisions()
+ }, 1000)
+ }
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.permissionChecker)
+ if (this.codeReader) {
+ this.codeReader.reset()
+ }
+ }
+
+ initCamera () {
+ this.codeReader = new BrowserQRCodeReader()
+ this.codeReader.getVideoInputDevices()
+ .then(videoInputDevices => {
+ clearTimeout(this.permissionChecker)
+ this.checkPermisisions()
+ this.codeReader.decodeFromInputVideoDevice(undefined, 'video')
+ .then(content => {
+ const result = this.parseContent(content.text)
+ if (result.type !== 'unknown') {
+ this.props.qrCodeDetected(result)
+ this.stopAndClose()
+ } else {
+ this.setState({msg: this.context.t('unknownQrCode')})
+ }
+ })
+ .catch(err => {
+ if (err && err.name === 'NotAllowedError') {
+ this.setState({msg: this.context.t('youNeedToAllowCameraAccess')})
+ clearTimeout(this.permissionChecker)
+ this.needsToReinit = true
+ this.checkPermisisions()
+ }
+ })
+ }).catch(err => {
+ console.error('[QR-SCANNER]: getVideoInputDevices threw an exception: ', err)
+ })
+ }
+
+ parseContent (content) {
+ let type = 'unknown'
+ let values = {}
+
+ // Here we could add more cases
+ // To parse other type of links
+ // For ex. EIP-681 (https://eips.ethereum.org/EIPS/eip-681)
+
+
+ // Ethereum address links - fox ex. ethereum:0x.....1111
+ if (content.split('ethereum:').length > 1) {
+
+ type = 'address'
+ values = {'address': content.split('ethereum:')[1] }
+
+ // Regular ethereum addresses - fox ex. 0x.....1111
+ } else if (content.substring(0, 2).toLowerCase() === '0x') {
+
+ type = 'address'
+ values = {'address': content }
+
+ }
+ return {type, values}
+ }
+
+
+ stopAndClose = () => {
+ if (this.codeReader) {
+ this.codeReader.reset()
+ }
+ this.setState({ ready: false })
+ this.props.hideModal()
+ }
+
+ tryAgain = () => {
+ // close the modal
+ this.stopAndClose()
+ // wait for the animation and try again
+ setTimeout(_ => {
+ this.props.scanQrCode()
+ }, 1000)
+ }
+
+ renderVideo () {
+ return (
+ <div className={'qr-scanner__content__video-wrapper'}>
+ <video
+ id="video"
+ style={{
+ display: this.state.ready ? 'block' : 'none',
+ }}
+ />
+ { !this.state.ready ? <Spinner color={'#F7C06C'} /> : null}
+ </div>
+ )
+ }
+
+ renderErrorModal () {
+ let title, msg
+
+ if (this.props.error) {
+ if (this.props.errorType === 'NO_WEBCAM_FOUND') {
+ title = this.context.t('noWebcamFoundTitle')
+ msg = this.context.t('noWebcamFound')
+ } else {
+ title = this.context.t('unknownCameraErrorTitle')
+ msg = this.context.t('unknownCameraError')
+ }
+ }
+
+ return (
+ <div className="qr-scanner">
+ <div className="qr-scanner__close" onClick={this.stopAndClose}></div>
+
+ <div className="qr-scanner__image">
+ <img src={'images/webcam.svg'} width={70} height={70} />
+ </div>
+ <div className="qr-scanner__title">
+ { title }
+ </div>
+ <div className={'qr-scanner__error'}>
+ {msg}
+ </div>
+ <PageContainerFooter
+ onCancel={this.stopAndClose}
+ onSubmit={this.tryAgain}
+ cancelText={this.context.t('cancel')}
+ submitText={this.context.t('tryAgain')}
+ submitButtonType="confirm"
+ />
+ </div>
+ )
+ }
+
+ render () {
+ const { t } = this.context
+
+ if (this.props.error) {
+ return this.renderErrorModal()
+ }
+
+ return (
+ <div className="qr-scanner">
+ <div className="qr-scanner__close" onClick={this.stopAndClose}></div>
+ <div className="qr-scanner__title">
+ { `${t('scanQrCode')}` }
+ </div>
+ <div className="qr-scanner__content">
+ { this.renderVideo() }
+ </div>
+ <div className={'qr-scanner__status'}>
+ {this.state.msg}
+ </div>
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/modals/qr-scanner/qr-scanner.container.js b/ui/app/components/modals/qr-scanner/qr-scanner.container.js
new file mode 100644
index 000000000..d0a35e03b
--- /dev/null
+++ b/ui/app/components/modals/qr-scanner/qr-scanner.container.js
@@ -0,0 +1,24 @@
+import { connect } from 'react-redux'
+import QrScanner from './qr-scanner.component'
+
+const { hideModal, qrCodeDetected, showQrScanner } = require('../../../actions')
+import {
+ SEND_ROUTE,
+} from '../../../routes'
+
+const mapStateToProps = state => {
+ return {
+ error: state.appState.modal.modalState.props.error,
+ errorType: state.appState.modal.modalState.props.errorType,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ hideModal: () => dispatch(hideModal()),
+ qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
+ scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(QrScanner)
diff --git a/ui/app/components/send/send-content/send-content.component.js b/ui/app/components/send/send-content/send-content.component.js
index 7a0b1a18e..df7bcb7cc 100644
--- a/ui/app/components/send/send-content/send-content.component.js
+++ b/ui/app/components/send/send-content/send-content.component.js
@@ -11,6 +11,7 @@ export default class SendContent extends Component {
static propTypes = {
updateGas: PropTypes.func,
+ scanQrCode: PropTypes.func,
};
render () {
@@ -18,7 +19,10 @@ export default class SendContent extends Component {
<PageContainerContent>
<div className="send-v2__form">
<SendFromRow />
- <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
+ <SendToRow
+ updateGas={(updateData) => this.props.updateGas(updateData)}
+ scanQrCode={ _ => this.props.scanQrCode()}
+ />
<SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendGasRow />
<SendHexDataRow />
diff --git a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
index 892ad5d67..1163dcffc 100644
--- a/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
+++ b/ui/app/components/send/send-content/send-to-row/send-to-row.component.js
@@ -17,6 +17,7 @@ export default class SendToRow extends Component {
updateGas: PropTypes.func,
updateSendTo: PropTypes.func,
updateSendToError: PropTypes.func,
+ scanQrCode: PropTypes.func,
};
static contextTypes = {
@@ -51,6 +52,7 @@ export default class SendToRow extends Component {
showError={inError}
>
<EnsInput
+ scanQrCode={_ => this.props.scanQrCode()}
accounts={toAccounts}
closeDropdown={() => closeToDropdown()}
dropdownOpen={toDropdownOpen}
diff --git a/ui/app/components/send/send.component.js b/ui/app/components/send/send.component.js
index 6f1b20c55..0d8ffd179 100644
--- a/ui/app/components/send/send.component.js
+++ b/ui/app/components/send/send.component.js
@@ -38,12 +38,30 @@ export default class SendTransactionScreen extends PersistentForm {
updateAndSetGasTotal: PropTypes.func,
updateSendErrors: PropTypes.func,
updateSendTokenBalance: PropTypes.func,
+ scanQrCode: PropTypes.func,
+ qrCodeDetected: PropTypes.func,
+ qrCodeData: PropTypes.object,
};
static contextTypes = {
t: PropTypes.func,
};
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.qrCodeData) {
+ if (nextProps.qrCodeData.type === 'address') {
+ const scannedAddress = nextProps.qrCodeData.values.address.toLowerCase()
+ const currentAddress = this.props.to && this.props.to.toLowerCase()
+ if (currentAddress !== scannedAddress) {
+ this.props.updateSendTo(scannedAddress)
+ this.updateGas({ to: scannedAddress })
+ // Clean up QR code data after handling
+ this.props.qrCodeDetected(null)
+ }
+ }
+ }
+ }
+
updateGas ({ to: updatedToAddress, amount: value } = {}) {
const {
amount,
@@ -158,6 +176,16 @@ export default class SendTransactionScreen extends PersistentForm {
address,
})
this.updateGas()
+
+ // Show QR Scanner modal if ?scan=true
+ if (window.location.search === '?scan=true') {
+ this.props.scanQrCode()
+
+ // Clear the queryString param after showing the modal
+ const cleanUrl = location.href.split('?')[0]
+ history.pushState({}, null, `${cleanUrl}`)
+ window.location.hash = '#send'
+ }
}
componentWillUnmount () {
@@ -170,7 +198,10 @@ export default class SendTransactionScreen extends PersistentForm {
return (
<div className="page-container">
<SendHeader history={history}/>
- <SendContent updateGas={(updateData) => this.updateGas(updateData)}/>
+ <SendContent
+ updateGas={(updateData) => this.updateGas(updateData)}
+ scanQrCode={_ => this.props.scanQrCode()}
+ />
<SendFooter history={history}/>
</div>
)
diff --git a/ui/app/components/send/send.container.js b/ui/app/components/send/send.container.js
index 44ebd2792..41735de64 100644
--- a/ui/app/components/send/send.container.js
+++ b/ui/app/components/send/send.container.js
@@ -21,11 +21,15 @@ import {
getSendFromObject,
getSendTo,
getTokenBalance,
+ getQrCodeData,
} from './send.selectors'
import {
+ updateSendTo,
updateSendTokenBalance,
updateGasData,
setGasTotal,
+ showQrScanner,
+ qrCodeDetected,
} from '../../actions'
import {
resetSendState,
@@ -35,6 +39,10 @@ import {
calcGasTotal,
} from './send.utils.js'
+import {
+ SEND_ROUTE,
+} from '../../routes'
+
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
@@ -60,6 +68,7 @@ function mapStateToProps (state) {
tokenBalance: getTokenBalance(state),
tokenContract: getSelectedTokenContract(state),
tokenToFiatRate: getSelectedTokenToFiatRate(state),
+ qrCodeData: getQrCodeData(state),
}
}
@@ -89,5 +98,8 @@ function mapDispatchToProps (dispatch) {
},
updateSendErrors: newError => dispatch(updateSendErrors(newError)),
resetSendState: () => dispatch(resetSendState()),
+ scanQrCode: () => dispatch(showQrScanner(SEND_ROUTE)),
+ qrCodeDetected: (data) => dispatch(qrCodeDetected(data)),
+ updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
}
}
diff --git a/ui/app/components/send/send.selectors.js b/ui/app/components/send/send.selectors.js
index cf07eafe1..ab3f6d34b 100644
--- a/ui/app/components/send/send.selectors.js
+++ b/ui/app/components/send/send.selectors.js
@@ -46,6 +46,7 @@ const selectors = {
getTokenExchangeRate,
getUnapprovedTxs,
transactionsSelector,
+ getQrCodeData,
}
module.exports = selectors
@@ -282,3 +283,7 @@ function transactionsSelector (state) {
: txsToRender
.sort((a, b) => b.time - a.time)
}
+
+function getQrCodeData (state) {
+ return state.appState.qrCodeData
+}
diff --git a/ui/app/components/send/tests/send-container.test.js b/ui/app/components/send/tests/send-container.test.js
index 7a9120d24..57e332780 100644
--- a/ui/app/components/send/tests/send-container.test.js
+++ b/ui/app/components/send/tests/send-container.test.js
@@ -44,6 +44,7 @@ proxyquire('../send.container.js', {
getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`,
getSendFromObject: (s) => `mockFrom:${s}`,
getTokenBalance: (s) => `mockTokenBalance:${s}`,
+ getQrCodeData: (s) => `mockQrCodeData:${s}`,
},
'../../actions': actionSpies,
'../../ducks/send.duck': duckActionSpies,
@@ -76,6 +77,7 @@ describe('send container', () => {
tokenBalance: 'mockTokenBalance:mockState',
tokenContract: 'mockTokenContract:mockState',
tokenToFiatRate: 'mockTokenToFiatRate:mockState',
+ qrCodeData: 'mockQrCodeData:mockState',
})
})
diff --git a/ui/app/components/send/to-autocomplete/to-autocomplete.js b/ui/app/components/send/to-autocomplete/to-autocomplete.js
index 80cfa7a85..49ebf49d9 100644
--- a/ui/app/components/send/to-autocomplete/to-autocomplete.js
+++ b/ui/app/components/send/to-autocomplete/to-autocomplete.js
@@ -4,6 +4,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountListItem = require('../account-list-item/account-list-item.component').default
const connect = require('react-redux').connect
+const Tooltip = require('../../tooltip')
ToAutoComplete.contextTypes = {
t: PropTypes.func,
@@ -94,11 +95,12 @@ ToAutoComplete.prototype.render = function () {
dropdownOpen,
onChange,
inError,
+ qrScanner,
} = this.props
return h('div.send-v2__to-autocomplete', {}, [
- h('input.send-v2__to-autocomplete__input', {
+ h(`input.send-v2__to-autocomplete__input${qrScanner ? '.with-qr' : ''}`, {
placeholder: this.context.t('recipientAddress'),
className: inError ? `send-v2__error-border` : '',
value: to,
@@ -108,7 +110,13 @@ ToAutoComplete.prototype.render = function () {
borderColor: inError ? 'red' : null,
},
}),
-
+ qrScanner && h(Tooltip, {
+ title: this.context.t('scanQrCode'),
+ position: 'bottom',
+ }, h(`i.fa.fa-qrcode.fa-lg.send-v2__to-autocomplete__qr-code`, {
+ style: { color: '#33333' },
+ onClick: () => this.props.scanQrCode(),
+ })),
!to && h(`i.fa.fa-caret-down.fa-lg.send-v2__to-autocomplete__down-caret`, {
style: { color: '#dedede' },
onClick: () => this.handleInputEvent(),
diff --git a/ui/app/css/itcss/components/send.scss b/ui/app/css/itcss/components/send.scss
index e9c872ea7..806ac8536 100644
--- a/ui/app/css/itcss/components/send.scss
+++ b/ui/app/css/itcss/components/send.scss
@@ -626,6 +626,23 @@
top: 18px;
right: 12px;
}
+
+ &__qr-code {
+ position: absolute;
+ top: 13px;
+ right: 33px;
+ cursor: pointer;
+ padding: 8px 5px 5px;
+ border-radius: 4px;
+ }
+
+ &__qr-code:hover {
+ background: #f1f1f1;
+ }
+
+ &__input.with-qr {
+ padding-right: 65px;
+ }
}
&__to-autocomplete, &__memo-text-area, &__hex-data {
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index e4e4c8581..98d467163 100644
--- a/ui/app/reducers/app.js
+++ b/ui/app/reducers/app.js
@@ -51,6 +51,7 @@ function reduceApp (state, action) {
sidebarOpen: false,
alertOpen: false,
alertMessage: null,
+ qrCodeData: null,
networkDropdownOpen: false,
currentView: seedWords ? seedConfView : defaultView,
accountDetail: {
@@ -91,7 +92,7 @@ function reduceApp (state, action) {
sidebarOpen: false,
})
- // sidebar methods
+ // alert methods
case actions.ALERT_OPEN:
return extend(appState, {
alertOpen: true,
@@ -104,6 +105,13 @@ function reduceApp (state, action) {
alertMessage: null,
})
+ // qr scanner methods
+ case actions.QR_CODE_DETECTED:
+ return extend(appState, {
+ qrCodeData: action.value,
+ })
+
+
// modal methods:
case actions.MODAL_OPEN:
const { name, ...modalProps } = action.payload
diff --git a/ui/lib/webcam-utils.js b/ui/lib/webcam-utils.js
new file mode 100644
index 000000000..eb717b23a
--- /dev/null
+++ b/ui/lib/webcam-utils.js
@@ -0,0 +1,36 @@
+'use strict'
+
+import DetectRTC from 'detectrtc'
+const { ENVIRONMENT_TYPE_POPUP } = require('../../app/scripts/lib/enums')
+const { getEnvironmentType, getPlatform } = require('../../app/scripts/lib/util')
+const { PLATFORM_BRAVE, PLATFORM_FIREFOX } = require('../../app/scripts/lib/enums')
+
+class WebcamUtils {
+
+ static checkStatus () {
+ return new Promise((resolve, reject) => {
+ const isPopup = getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP
+ const isFirefoxOrBrave = getPlatform() === (PLATFORM_FIREFOX || PLATFORM_BRAVE)
+ try {
+ DetectRTC.load(_ => {
+ if (DetectRTC.hasWebcam) {
+ let environmentReady = true
+ if ((isFirefoxOrBrave && isPopup) || (isPopup && !DetectRTC.isWebsiteHasWebcamPermissions)) {
+ environmentReady = false
+ }
+ resolve({
+ permissions: DetectRTC.isWebsiteHasWebcamPermissions,
+ environmentReady,
+ })
+ } else {
+ reject({type: 'NO_WEBCAM_FOUND'})
+ }
+ })
+ } catch (e) {
+ reject({type: 'UNKNOWN_ERROR'})
+ }
+ })
+ }
+}
+
+module.exports = WebcamUtils