mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-26 12:29:06 +01:00
commit
e2be22a4b7
@ -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"
|
||||
},
|
||||
|
18
app/images/webcam.svg
Normal file
18
app/images/webcam.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 1020 B |
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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')
|
||||
|
||||
/**
|
||||
@ -37,6 +42,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
|
||||
*
|
||||
@ -100,6 +128,7 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPlatform,
|
||||
getStack,
|
||||
getEnvironmentType,
|
||||
sufficientBalance,
|
||||
|
@ -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}`
|
||||
}
|
||||
|
66
package-lock.json
generated
66
package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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 {
|
||||
|
@ -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' },
|
||||
|
@ -1,5 +1,7 @@
|
||||
@import './customize-gas/index';
|
||||
|
||||
@import './qr-scanner/index';
|
||||
|
||||
.modal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
@ -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: [],
|
||||
|
2
ui/app/components/modals/qr-scanner/index.js
Normal file
2
ui/app/components/modals/qr-scanner/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import QrScanner from './qr-scanner.container'
|
||||
module.exports = QrScanner
|
83
ui/app/components/modals/qr-scanner/index.scss
Normal file
83
ui/app/components/modals/qr-scanner/index.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
216
ui/app/components/modals/qr-scanner/qr-scanner.component.js
Normal file
216
ui/app/components/modals/qr-scanner/qr-scanner.component.js
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
24
ui/app/components/modals/qr-scanner/qr-scanner.container.js
Normal file
24
ui/app/components/modals/qr-scanner/qr-scanner.container.js
Normal file
@ -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)
|
@ -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 />
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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',
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
36
ui/lib/webcam-utils.js
Normal file
36
ui/lib/webcam-utils.js
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user