aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app
diff options
context:
space:
mode:
authorEsteban MIno <efmino@uc.cl>2018-08-21 06:35:38 +0800
committerEsteban MIno <efmino@uc.cl>2018-08-21 06:35:38 +0800
commit81cd29df43feb2469b78c6240d12ffcb9a68678e (patch)
tree15b14d9d3a29e5c21776ce5f9a3b35c7934abb81 /ui/app
parentdbab9a007fc9663427cebdbe1d41c51df67fd1fe (diff)
parent887cad973f25f43d2d4502ff31657f156a44b188 (diff)
downloadtangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.tar
tangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.tar.gz
tangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.tar.bz2
tangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.tar.lz
tangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.tar.xz
tangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.tar.zst
tangerine-wallet-browser-81cd29df43feb2469b78c6240d12ffcb9a68678e.zip
Merge branch 'develop' into WatchTokenFeature
Diffstat (limited to 'ui/app')
-rw-r--r--ui/app/actions.js88
-rw-r--r--ui/app/app.js3
-rw-r--r--ui/app/components/account-menu/index.js1
-rw-r--r--ui/app/components/app-header/app-header.component.js23
-rw-r--r--ui/app/components/app-header/index.js3
-rw-r--r--ui/app/components/app-header/index.scss (renamed from ui/app/css/itcss/components/header.scss)51
-rw-r--r--ui/app/components/ens-input.js2
-rw-r--r--ui/app/components/index.scss2
-rw-r--r--ui/app/components/modals/account-details-modal.js16
-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/pages/confirm-transaction-base/confirm-transaction-base.component.js2
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/account-list.js73
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/connect-screen.js87
-rw-r--r--ui/app/components/pages/create-account/connect-hardware/index.js91
-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/components/tx-list-item.js8
-rw-r--r--ui/app/components/tx-list.js18
-rw-r--r--ui/app/components/wallet-view.js16
-rw-r--r--ui/app/constants/error-keys.js2
-rw-r--r--ui/app/conversion-util.js4
-rw-r--r--ui/app/css/itcss/components/index.scss2
-rw-r--r--ui/app/css/itcss/components/new-account.scss105
-rw-r--r--ui/app/css/itcss/components/send.scss17
-rw-r--r--ui/app/first-time/init-menu.js2
-rw-r--r--ui/app/helpers/confirm-transaction/util.js2
-rw-r--r--ui/app/reducers/app.js29
-rw-r--r--ui/app/selectors/confirm-transaction.js10
-rw-r--r--ui/app/token-util.js2
-rw-r--r--ui/app/util.js4
40 files changed, 920 insertions, 155 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 0760377c2..b5f97d374 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',
@@ -88,7 +91,7 @@ var actions = {
connectHardware,
checkHardwareStatus,
forgetDevice,
- unlockTrezorAccount,
+ unlockHardwareWalletAccount,
NEW_ACCOUNT_SCREEN: 'NEW_ACCOUNT_SCREEN',
navigateToNewAccountScreen,
resetAccount,
@@ -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',
@@ -143,6 +147,8 @@ var actions = {
exportAccountComplete,
SET_ACCOUNT_LABEL: 'SET_ACCOUNT_LABEL',
setAccountLabel,
+ updateNetworkNonce,
+ SET_NETWORK_NONCE: 'SET_NETWORK_NONCE',
// tx conf screen
COMPLETED_TX: 'COMPLETED_TX',
TRANSACTION_ERROR: 'TRANSACTION_ERROR',
@@ -232,6 +238,8 @@ var actions = {
UPDATE_TOKENS: 'UPDATE_TOKENS',
setRpcTarget: setRpcTarget,
setProviderType: setProviderType,
+ SET_HARDWARE_WALLET_DEFAULT_HD_PATH: 'SET_HARDWARE_WALLET_DEFAULT_HD_PATH',
+ setHardwareWalletDefaultHdPath,
updateProviderType,
// loading overlay
SHOW_LOADING: 'SHOW_LOADING_INDICATION',
@@ -636,12 +644,12 @@ function addNewAccount () {
}
}
-function checkHardwareStatus (deviceName) {
- log.debug(`background.checkHardwareStatus`, deviceName)
+function checkHardwareStatus (deviceName, hdPath) {
+ log.debug(`background.checkHardwareStatus`, deviceName, hdPath)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.checkHardwareStatus(deviceName, (err, unlocked) => {
+ background.checkHardwareStatus(deviceName, hdPath, (err, unlocked) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
@@ -678,12 +686,12 @@ function forgetDevice (deviceName) {
}
}
-function connectHardware (deviceName, page) {
- log.debug(`background.connectHardware`, deviceName, page)
+function connectHardware (deviceName, page, hdPath) {
+ log.debug(`background.connectHardware`, deviceName, page, hdPath)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.connectHardware(deviceName, page, (err, accounts) => {
+ background.connectHardware(deviceName, page, hdPath, (err, accounts) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
@@ -699,12 +707,12 @@ function connectHardware (deviceName, page) {
}
}
-function unlockTrezorAccount (index) {
- log.debug(`background.unlockTrezorAccount`, index)
+function unlockHardwareWalletAccount (index, deviceName, hdPath) {
+ log.debug(`background.unlockHardwareWalletAccount`, index, deviceName, hdPath)
return (dispatch, getState) => {
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => {
- background.unlockTrezorAccount(index, (err, accounts) => {
+ background.unlockHardwareWalletAccount(index, deviceName, hdPath, (err, accounts) => {
if (err) {
log.error(err)
dispatch(actions.displayWarning(err.message))
@@ -724,6 +732,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())
@@ -1844,6 +1874,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 {
@@ -1852,6 +1893,13 @@ function showLoadingIndication (message) {
}
}
+function setHardwareWalletDefaultHdPath ({ device, path }) {
+ return {
+ type: actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH,
+ value: {device, path},
+ }
+}
+
function hideLoadingIndication () {
return {
type: actions.HIDE_LOADING,
@@ -2153,6 +2201,24 @@ function updateFeatureFlags (updatedFeatureFlags) {
}
}
+function setNetworkNonce (networkNonce) {
+ return {
+ type: actions.SET_NETWORK_NONCE,
+ value: networkNonce,
+ }
+}
+
+function updateNetworkNonce (address) {
+ return (dispatch) => {
+ return new Promise((resolve, reject) => {
+ global.ethQuery.getTransactionCount(address, (err, data) => {
+ dispatch(setNetworkNonce(data))
+ resolve(data)
+ })
+ })
+ }
+}
+
function setMouseUserState (isMouseUser) {
return {
type: actions.SET_MOUSE_USER_STATE,
diff --git a/ui/app/app.js b/ui/app/app.js
index 83c063c3f..cdda44d40 100644
--- a/ui/app/app.js
+++ b/ui/app/app.js
@@ -40,8 +40,7 @@ const Modal = require('./components/modals/index').Modal
// Global Alert
const Alert = require('./components/alert')
-const AppHeader = require('./components/app-header')
-
+import AppHeader from './components/app-header'
import UnlockPage from './components/pages/unlock-page'
// Routes
diff --git a/ui/app/components/account-menu/index.js b/ui/app/components/account-menu/index.js
index 9c063d31e..bcada41e3 100644
--- a/ui/app/components/account-menu/index.js
+++ b/ui/app/components/account-menu/index.js
@@ -229,6 +229,7 @@ AccountMenu.prototype.renderKeyringType = function (keyring) {
let label
switch (type) {
case 'Trezor Hardware':
+ case 'Ledger Hardware':
label = this.context.t('hardware')
break
case 'Simple Key Pair':
diff --git a/ui/app/components/app-header/app-header.component.js b/ui/app/components/app-header/app-header.component.js
index 07ca6cf84..b8b002dcc 100644
--- a/ui/app/components/app-header/app-header.component.js
+++ b/ui/app/components/app-header/app-header.component.js
@@ -1,4 +1,4 @@
-import React, { Component } from 'react'
+import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { matchPath } from 'react-router-dom'
@@ -11,7 +11,7 @@ const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('
const Identicon = require('../identicon')
const NetworkIndicator = require('../network')
-class AppHeader extends Component {
+export default class AppHeader extends PureComponent {
static propTypes = {
history: PropTypes.object,
location: PropTypes.object,
@@ -107,20 +107,19 @@ class AppHeader extends Component {
onClick={() => history.push(DEFAULT_ROUTE)}
>
<img
- className="app-header__metafox"
- src="/images/metamask-fox.svg"
+ className="app-header__metafox-logo app-header__metafox-logo--horizontal"
+ src="/images/logo/metamask-logo-horizontal-beta.svg"
+ height={30}
+ />
+ <img
+ className="app-header__metafox-logo app-header__metafox-logo--icon"
+ src="/images/logo/metamask-fox.svg"
height={42}
width={42}
/>
- <div className="flex-row">
- <h1>{ this.context.t('appName') }</h1>
- <div className="app-header__beta-label">
- { this.context.t('beta') }
- </div>
- </div>
</div>
<div className="app-header__account-menu-container">
- <div className="network-component-wrapper">
+ <div className="app-header__network-component-wrapper">
<NetworkIndicator
network={network}
provider={provider}
@@ -135,5 +134,3 @@ class AppHeader extends Component {
)
}
}
-
-export default AppHeader
diff --git a/ui/app/components/app-header/index.js b/ui/app/components/app-header/index.js
index daa31f621..6de2f9c78 100644
--- a/ui/app/components/app-header/index.js
+++ b/ui/app/components/app-header/index.js
@@ -1,2 +1 @@
-import AppHeader from './app-header.container'
-module.exports = AppHeader
+export { default } from './app-header.container'
diff --git a/ui/app/css/itcss/components/header.scss b/ui/app/components/app-header/index.scss
index 3ccfd5c15..325844af5 100644
--- a/ui/app/css/itcss/components/header.scss
+++ b/ui/app/components/app-header/index.scss
@@ -30,21 +30,19 @@
}
}
- &__metafox {
+ &__metafox-logo {
cursor: pointer;
- }
- &__beta-label {
- font-family: Roboto;
- text-transform: uppercase;
- font-weight: 500;
- font-size: .8rem;
- color: $buttercup;
- margin-left: 5px;
- line-height: initial;
+ &--icon {
+ @media screen and (min-width: $break-large) {
+ display: none;
+ }
+ }
- @media screen and (max-width: 575px) {
- display: none;
+ &--horizontal {
+ @media screen and (max-width: $break-small) {
+ display: none;
+ }
}
}
@@ -83,31 +81,10 @@
flex-flow: row nowrap;
align-items: center;
}
-}
-
-.app-header h1 {
- font-family: Roboto;
- text-transform: uppercase;
- font-weight: 400;
- font-size: 1.1rem;
- position: relative;
- padding-left: 15px;
- color: #5b5d67;
- @media screen and (max-width: 575px) {
- display: none;
+ &__network-component-wrapper {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
}
}
-
-h2.page-subtitle {
- text-transform: uppercase;
- color: #aeaeae;
- font-size: 1em;
- margin: 12px;
-}
-
-.network-component-wrapper {
- display: flex;
- flex-direction: row;
- align-items: center;
-}
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/index.scss b/ui/app/components/index.scss
index b3e14ce23..35d38e2a3 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -19,3 +19,5 @@
@import './sender-to-recipient/index';
@import './tabs/index';
+
+@import './app-header/index';
diff --git a/ui/app/components/modals/account-details-modal.js b/ui/app/components/modals/account-details-modal.js
index 5607cf051..cc90cf578 100644
--- a/ui/app/components/modals/account-details-modal.js
+++ b/ui/app/components/modals/account-details-modal.js
@@ -14,6 +14,7 @@ function mapStateToProps (state) {
return {
network: state.metamask.network,
selectedIdentity: getSelectedIdentity(state),
+ keyrings: state.metamask.keyrings,
}
}
@@ -50,9 +51,20 @@ AccountDetailsModal.prototype.render = function () {
network,
showExportPrivateKeyModal,
setAccountLabel,
+ keyrings,
} = this.props
const { name, address } = selectedIdentity
+ const keyring = keyrings.find((kr) => {
+ return kr.accounts.includes(address)
+ })
+
+ let exportPrivateKeyFeatureEnabled = true
+ // This feature is disabled for hardware wallets
+ if (keyring.type.search('Hardware') !== -1) {
+ exportPrivateKeyFeatureEnabled = false
+ }
+
return h(AccountModalContainer, {}, [
h(EditableLabel, {
className: 'account-modal__name',
@@ -73,9 +85,9 @@ AccountDetailsModal.prototype.render = function () {
}, this.context.t('etherscanView')),
// Holding on redesign for Export Private Key functionality
- h('button.btn-primary.account-modal__button', {
+ exportPrivateKeyFeatureEnabled ? h('button.btn-primary.account-modal__button', {
onClick: () => showExportPrivateKeyModal(),
- }, this.context.t('exportPrivateKey')),
+ }, this.context.t('exportPrivateKey')) : null,
])
}
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/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
index b170880b4..961aa304e 100644
--- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
+++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -124,7 +124,7 @@ export default class ConfirmTransactionBase extends Component {
if (simulationFails) {
return {
- valid: false,
+ valid: true,
errorKey: TRANSACTION_ERROR_KEY,
}
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/account-list.js b/ui/app/components/pages/create-account/connect-hardware/account-list.js
index c722d1f55..488a189ea 100644
--- a/ui/app/components/pages/create-account/connect-hardware/account-list.js
+++ b/ui/app/components/pages/create-account/connect-hardware/account-list.js
@@ -2,16 +2,75 @@ const { Component } = require('react')
const PropTypes = require('prop-types')
const h = require('react-hyperscript')
const genAccountLink = require('../../../../../lib/account-link.js')
+const Select = require('react-select').default
class AccountList extends Component {
constructor (props, context) {
super(props)
}
+ getHdPaths () {
+ return [
+ {
+ label: `Ledger Live`,
+ value: `m/44'/60'/0'/0/0`,
+ },
+ {
+ label: `Legacy (MEW / MyCrypto)`,
+ value: `m/44'/60'/0'`,
+ },
+ ]
+ }
+
+ goToNextPage = () => {
+ // If we have < 5 accounts, it's restricted by BIP-44
+ if (this.props.accounts.length === 5) {
+ this.props.getPage(this.props.device, 1, this.props.selectedPath)
+ } else {
+ this.props.onAccountRestriction()
+ }
+ }
+
+ goToPreviousPage = () => {
+ this.props.getPage(this.props.device, -1, this.props.selectedPath)
+ }
+
+ renderHdPathSelector () {
+ const { onPathChange, selectedPath } = this.props
+
+ const options = this.getHdPaths()
+ return h('div', [
+ h('h3.hw-connect__hdPath__title', {}, this.context.t('selectHdPath')),
+ h('p.hw-connect__msg', {}, this.context.t('selectPathHelp')),
+ h('div.hw-connect__hdPath', [
+ h(Select, {
+ className: 'hw-connect__hdPath__select',
+ name: 'hd-path-select',
+ clearable: false,
+ value: selectedPath,
+ options,
+ onChange: (opt) => {
+ onPathChange(opt.value)
+ },
+ }),
+ ]),
+ ])
+ }
+
+ capitalizeDevice (device) {
+ return device.slice(0, 1).toUpperCase() + device.slice(1)
+ }
+
renderHeader () {
+ const { device } = this.props
return (
h('div.hw-connect', [
- h('h3.hw-connect__title', {}, this.context.t('selectAnAccount')),
+
+ h('h3.hw-connect__unlock-title', {}, `${this.context.t('unlock')} ${this.capitalizeDevice(device)}`),
+
+ device.toLowerCase() === 'ledger' ? this.renderHdPathSelector() : null,
+
+ h('h3.hw-connect__hdPath__title', {}, this.context.t('selectAnAccount')),
h('p.hw-connect__msg', {}, this.context.t('selectAnAccountHelp')),
])
)
@@ -61,7 +120,7 @@ class AccountList extends Component {
h(
'button.hw-list-pagination__button',
{
- onClick: () => this.props.getPage(-1),
+ onClick: this.goToPreviousPage,
},
`< ${this.context.t('prev')}`
),
@@ -69,7 +128,7 @@ class AccountList extends Component {
h(
'button.hw-list-pagination__button',
{
- onClick: () => this.props.getPage(1),
+ onClick: this.goToNextPage,
},
`${this.context.t('next')} >`
),
@@ -95,7 +154,7 @@ class AccountList extends Component {
h(
`button.btn-primary.btn--large.new-account-connect-form__button.unlock ${disabled ? '.btn-primary--disabled' : ''}`,
{
- onClick: this.props.onUnlockAccount.bind(this),
+ onClick: this.props.onUnlockAccount.bind(this, this.props.device),
...buttonProps,
},
[this.context.t('unlock')]
@@ -106,7 +165,7 @@ class AccountList extends Component {
renderForgetDevice () {
return h('div.hw-forget-device-container', {}, [
h('a', {
- onClick: this.props.onForgetDevice.bind(this),
+ onClick: this.props.onForgetDevice.bind(this, this.props.device),
}, this.context.t('forgetDevice')),
])
}
@@ -125,6 +184,9 @@ class AccountList extends Component {
AccountList.propTypes = {
+ onPathChange: PropTypes.func.isRequired,
+ selectedPath: PropTypes.string.isRequired,
+ device: PropTypes.string.isRequired,
accounts: PropTypes.array.isRequired,
onAccountChange: PropTypes.func.isRequired,
onForgetDevice: PropTypes.func.isRequired,
@@ -134,6 +196,7 @@ AccountList.propTypes = {
history: PropTypes.object,
onUnlockAccount: PropTypes.func,
onCancel: PropTypes.func,
+ onAccountRestriction: PropTypes.func,
}
AccountList.contextTypes = {
diff --git a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
index cb2b86595..b3dfa4ee2 100644
--- a/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
+++ b/ui/app/components/pages/create-account/connect-hardware/connect-screen.js
@@ -5,6 +5,52 @@ const h = require('react-hyperscript')
class ConnectScreen extends Component {
constructor (props, context) {
super(props)
+ this.state = {
+ selectedDevice: null,
+ }
+ }
+
+ connect = () => {
+ if (this.state.selectedDevice) {
+ this.props.connectToHardwareWallet(this.state.selectedDevice)
+ }
+ return null
+ }
+
+ renderConnectToTrezorButton () {
+ return h(
+ `button.hw-connect__btn${this.state.selectedDevice === 'trezor' ? '.selected' : ''}`,
+ { onClick: _ => this.setState({selectedDevice: 'trezor'}) },
+ h('img.hw-connect__btn__img', {
+ src: 'images/trezor-logo.svg',
+ })
+ )
+ }
+
+ renderConnectToLedgerButton () {
+ return h(
+ `button.hw-connect__btn${this.state.selectedDevice === 'ledger' ? '.selected' : ''}`,
+ { onClick: _ => this.setState({selectedDevice: 'ledger'}) },
+ h('img.hw-connect__btn__img', {
+ src: 'images/ledger-logo.svg',
+ })
+ )
+ }
+
+ renderButtons () {
+ return (
+ h('div', {}, [
+ h('div.hw-connect__btn-wrapper', {}, [
+ this.renderConnectToLedgerButton(),
+ this.renderConnectToTrezorButton(),
+ ]),
+ h(
+ `button.hw-connect__connect-btn${!this.state.selectedDevice ? '.disabled' : ''}`,
+ { onClick: this.connect },
+ this.context.t('connect')
+ ),
+ ])
+ )
}
renderUnsupportedBrowser () {
@@ -12,7 +58,7 @@ class ConnectScreen extends Component {
h('div.new-account-connect-form.unsupported-browser', {}, [
h('div.hw-connect', [
h('h3.hw-connect__title', {}, this.context.t('browserNotSupported')),
- h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForTrezor')),
+ h('p.hw-connect__msg', {}, this.context.t('chromeRequiredForHardwareWallets')),
]),
h(
'button.btn-primary.btn--large',
@@ -30,29 +76,31 @@ class ConnectScreen extends Component {
renderHeader () {
return (
h('div.hw-connect__header', {}, [
- h('h3.hw-connect__header__title', {}, this.context.t(`hardwareSupport`)),
- h('p.hw-connect__header__msg', {}, this.context.t(`hardwareSupportMsg`)),
+ h('h3.hw-connect__header__title', {}, this.context.t(`hardwareWallets`)),
+ h('p.hw-connect__header__msg', {}, this.context.t(`hardwareWalletsMsg`)),
])
)
}
+ getAffiliateLinks () {
+ const links = {
+ trezor: `<a class='hw-connect__get-hw__link' href='https://shop.trezor.io/?a=metamask' target='_blank'>Trezor</a>`,
+ ledger: `<a class='hw-connect__get-hw__link' href='https://www.ledger.com/products/ledger-nano-s?r=17c4991a03fa&tracker=MY_TRACKER' target='_blank'>Ledger</a>`,
+ }
+
+ const text = this.context.t('orderOneHere')
+ const response = text.replace('Trezor', links.trezor).replace('Ledger', links.ledger)
+
+ return h('div.hw-connect__get-hw__msg', { dangerouslySetInnerHTML: {__html: response }})
+ }
+
renderTrezorAffiliateLink () {
- return h('div.hw-connect__get-trezor', {}, [
- h('p.hw-connect__get-trezor__msg', {}, this.context.t(`dontHaveATrezorWallet`)),
- h('a.hw-connect__get-trezor__link', {
- href: 'https://shop.trezor.io/?a=metamask',
- target: '_blank',
- }, this.context.t('orderOneHere')),
+ return h('div.hw-connect__get-hw', {}, [
+ h('p.hw-connect__get-hw__msg', {}, this.context.t(`dontHaveAHardwareWallet`)),
+ this.getAffiliateLinks(),
])
}
- renderConnectToTrezorButton () {
- return h(
- 'button.btn-primary.btn--large',
- { onClick: this.props.connectToTrezor.bind(this) },
- this.props.btnText
- )
- }
scrollToTutorial = (e) => {
if (this.referenceNode) this.referenceNode.scrollIntoView({behavior: 'smooth'})
@@ -102,7 +150,7 @@ class ConnectScreen extends Component {
return (
h('div.hw-connect__footer', {}, [
h('h3.hw-connect__footer__title', {}, this.context.t(`readyToConnect`)),
- this.renderConnectToTrezorButton(),
+ this.renderButtons(),
h('p.hw-connect__footer__msg', {}, [
this.context.t(`havingTroubleConnecting`),
h('a.hw-connect__footer__link', {
@@ -118,8 +166,8 @@ class ConnectScreen extends Component {
return (
h('div.new-account-connect-form', {}, [
this.renderHeader(),
+ this.renderButtons(),
this.renderTrezorAffiliateLink(),
- this.renderConnectToTrezorButton(),
this.renderLearnMore(),
this.renderTutorialSteps(),
this.renderFooter(),
@@ -136,8 +184,7 @@ class ConnectScreen extends Component {
}
ConnectScreen.propTypes = {
- connectToTrezor: PropTypes.func.isRequired,
- btnText: PropTypes.string.isRequired,
+ connectToHardwareWallet: PropTypes.func.isRequired,
browserSupported: PropTypes.bool.isRequired,
}
diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js
index 3f66e7098..547df5223 100644
--- a/ui/app/components/pages/create-account/connect-hardware/index.js
+++ b/ui/app/components/pages/create-account/connect-hardware/index.js
@@ -7,17 +7,19 @@ const ConnectScreen = require('./connect-screen')
const AccountList = require('./account-list')
const { DEFAULT_ROUTE } = require('../../../../routes')
const { formatBalance } = require('../../../../util')
+const { getPlatform } = require('../../../../../../app/scripts/lib/util')
+const { PLATFORM_FIREFOX } = require('../../../../../../app/scripts/lib/enums')
class ConnectHardwareForm extends Component {
constructor (props, context) {
super(props)
this.state = {
error: null,
- btnText: context.t('connectToTrezor'),
selectedAccount: null,
accounts: [],
browserSupported: true,
unlocked: false,
+ device: null,
}
}
@@ -38,25 +40,44 @@ class ConnectHardwareForm extends Component {
}
async checkIfUnlocked () {
- const unlocked = await this.props.checkHardwareStatus('trezor')
- if (unlocked) {
- this.setState({unlocked: true})
- this.getPage(0)
- }
+ ['trezor', 'ledger'].forEach(async device => {
+ const unlocked = await this.props.checkHardwareStatus(device, this.props.defaultHdPaths[device])
+ if (unlocked) {
+ this.setState({unlocked: true})
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+ })
}
- connectToTrezor = () => {
+ connectToHardwareWallet = (device) => {
+ // None of the hardware wallets are supported
+ // At least for now
+ if (getPlatform() === PLATFORM_FIREFOX) {
+ this.setState({ browserSupported: false, error: null})
+ return null
+ }
+
if (this.state.accounts.length) {
return null
}
- this.setState({ btnText: this.context.t('connecting')})
- this.getPage(0)
+
+ // Default values
+ this.getPage(device, 0, this.props.defaultHdPaths[device])
+ }
+
+ onPathChange = (path) => {
+ this.props.setHardwareWalletDefaultHdPath({device: this.state.device, path})
+ this.getPage(this.state.device, 0, path)
}
onAccountChange = (account) => {
this.setState({selectedAccount: account.toString(), error: null})
}
+ onAccountRestriction = () => {
+ this.setState({error: this.context.t('ledgerAccountRestriction') })
+ }
+
showTemporaryAlert () {
this.props.showAlert(this.context.t('hardwareWalletConnected'))
// Autohide the alert after 5 seconds
@@ -65,9 +86,9 @@ class ConnectHardwareForm extends Component {
}, 5000)
}
- getPage = (page) => {
+ getPage = (device, page, hdPath) => {
this.props
- .connectHardware('trezor', page)
+ .connectHardware(device, page, hdPath)
.then(accounts => {
if (accounts.length) {
@@ -77,7 +98,7 @@ class ConnectHardwareForm extends Component {
this.showTemporaryAlert()
}
- const newState = { unlocked: true }
+ const newState = { unlocked: true, device, error: null }
// Default to the first account
if (this.state.selectedAccount === null) {
accounts.forEach((a, i) => {
@@ -104,18 +125,18 @@ class ConnectHardwareForm extends Component {
})
.catch(e => {
if (e === 'Window blocked') {
- this.setState({ browserSupported: false })
+ this.setState({ browserSupported: false, error: null})
+ } else if (e !== 'Window closed') {
+ this.setState({ error: e.toString() })
}
- this.setState({ btnText: this.context.t('connectToTrezor') })
})
}
- onForgetDevice = () => {
- this.props.forgetDevice('trezor')
+ onForgetDevice = (device) => {
+ this.props.forgetDevice(device)
.then(_ => {
this.setState({
error: null,
- btnText: this.context.t('connectToTrezor'),
selectedAccount: null,
accounts: [],
unlocked: false,
@@ -125,13 +146,13 @@ class ConnectHardwareForm extends Component {
})
}
- onUnlockAccount = () => {
+ onUnlockAccount = (device) => {
if (this.state.selectedAccount === null) {
this.setState({ error: this.context.t('accountSelectionRequired') })
}
- this.props.unlockTrezorAccount(this.state.selectedAccount)
+ this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device)
.then(_ => {
this.props.history.push(DEFAULT_ROUTE)
}).catch(e => {
@@ -145,20 +166,22 @@ class ConnectHardwareForm extends Component {
renderError () {
return this.state.error
- ? h('span.error', { style: { marginBottom: 40 } }, this.state.error)
+ ? h('span.error', { style: { margin: '20px 20px 10px', display: 'block', textAlign: 'center' } }, this.state.error)
: null
}
renderContent () {
if (!this.state.accounts.length) {
return h(ConnectScreen, {
- connectToTrezor: this.connectToTrezor,
- btnText: this.state.btnText,
+ connectToHardwareWallet: this.connectToHardwareWallet,
browserSupported: this.state.browserSupported,
})
}
return h(AccountList, {
+ onPathChange: this.onPathChange,
+ selectedPath: this.props.defaultHdPaths[this.state.device],
+ device: this.state.device,
accounts: this.state.accounts,
selectedAccount: this.state.selectedAccount,
onAccountChange: this.onAccountChange,
@@ -168,6 +191,7 @@ class ConnectHardwareForm extends Component {
onUnlockAccount: this.onUnlockAccount,
onForgetDevice: this.onForgetDevice,
onCancel: this.onCancel,
+ onAccountRestriction: this.onAccountRestriction,
})
}
@@ -188,13 +212,15 @@ ConnectHardwareForm.propTypes = {
forgetDevice: PropTypes.func,
showAlert: PropTypes.func,
hideAlert: PropTypes.func,
- unlockTrezorAccount: PropTypes.func,
+ unlockHardwareWalletAccount: PropTypes.func,
+ setHardwareWalletDefaultHdPath: PropTypes.func,
numberOfExistingAccounts: PropTypes.number,
history: PropTypes.object,
t: PropTypes.func,
network: PropTypes.string,
accounts: PropTypes.object,
address: PropTypes.string,
+ defaultHdPaths: PropTypes.object,
}
const mapStateToProps = state => {
@@ -202,28 +228,35 @@ const mapStateToProps = state => {
metamask: { network, selectedAddress, identities = {}, accounts = [] },
} = state
const numberOfExistingAccounts = Object.keys(identities).length
+ const {
+ appState: { defaultHdPaths },
+ } = state
return {
network,
accounts,
address: selectedAddress,
numberOfExistingAccounts,
+ defaultHdPaths,
}
}
const mapDispatchToProps = dispatch => {
return {
- connectHardware: (deviceName, page) => {
- return dispatch(actions.connectHardware(deviceName, page))
+ setHardwareWalletDefaultHdPath: ({device, path}) => {
+ return dispatch(actions.setHardwareWalletDefaultHdPath({device, path}))
+ },
+ connectHardware: (deviceName, page, hdPath) => {
+ return dispatch(actions.connectHardware(deviceName, page, hdPath))
},
- checkHardwareStatus: (deviceName) => {
- return dispatch(actions.checkHardwareStatus(deviceName))
+ checkHardwareStatus: (deviceName, hdPath) => {
+ return dispatch(actions.checkHardwareStatus(deviceName, hdPath))
},
forgetDevice: (deviceName) => {
return dispatch(actions.forgetDevice(deviceName))
},
- unlockTrezorAccount: index => {
- return dispatch(actions.unlockTrezorAccount(index))
+ unlockHardwareWalletAccount: (index, deviceName, hdPath) => {
+ return dispatch(actions.unlockHardwareWalletAccount(index, deviceName, hdPath))
},
showImportPage: () => dispatch(actions.showImportPage()),
showConnectPage: () => dispatch(actions.showConnectPage()),
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/components/tx-list-item.js b/ui/app/components/tx-list-item.js
index 7513ba267..474d62638 100644
--- a/ui/app/components/tx-list-item.js
+++ b/ui/app/components/tx-list-item.js
@@ -35,6 +35,7 @@ function mapStateToProps (state) {
currentCurrency: getCurrentCurrency(state),
contractExchangeRates: state.metamask.contractExchangeRates,
selectedAddressTxList: state.metamask.selectedAddressTxList,
+ networkNonce: state.appState.networkNonce,
}
}
@@ -209,6 +210,7 @@ TxListItem.prototype.showRetryButton = function () {
selectedAddressTxList,
transactionId,
txParams,
+ networkNonce,
} = this.props
if (!txParams) {
return false
@@ -222,11 +224,7 @@ TxListItem.prototype.showRetryButton = function () {
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transactionId
if (currentSubmittedTxs.length > 0) {
- const earliestSubmitted = currentSubmittedTxs.reduce((tx1, tx2) => {
- if (tx1.submittedTime < tx2.submittedTime) return tx1
- return tx2
- })
- currentTxSharesEarliestNonce = currentNonce === earliestSubmitted.txParams.nonce
+ currentTxSharesEarliestNonce = currentNonce === networkNonce
}
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js
index 554febcff..d8c4a9d19 100644
--- a/ui/app/components/tx-list.js
+++ b/ui/app/components/tx-list.js
@@ -8,7 +8,7 @@ const selectors = require('../selectors')
const TxListItem = require('./tx-list-item')
const ShiftListItem = require('./shift-list-item')
const { formatDate } = require('../util')
-const { showConfTxPage } = require('../actions')
+const { showConfTxPage, updateNetworkNonce } = require('../actions')
const classnames = require('classnames')
const { tokenInfoGetter } = require('../token-util')
const { withRouter } = require('react-router-dom')
@@ -28,12 +28,14 @@ function mapStateToProps (state) {
return {
txsToRender: selectors.transactionsSelector(state),
conversionRate: selectors.conversionRateSelector(state),
+ selectedAddress: selectors.getSelectedAddress(state),
}
}
function mapDispatchToProps (dispatch) {
return {
showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
+ updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
}
}
@@ -44,6 +46,20 @@ function TxList () {
TxList.prototype.componentWillMount = function () {
this.tokenInfoGetter = tokenInfoGetter()
+ this.props.updateNetworkNonce(this.props.selectedAddress)
+}
+
+TxList.prototype.componentDidUpdate = function (prevProps) {
+ const oldTxsToRender = prevProps.txsToRender
+ const {
+ txsToRender: newTxsToRender,
+ selectedAddress,
+ updateNetworkNonce,
+ } = this.props
+
+ if (newTxsToRender.length > oldTxsToRender.length) {
+ updateNetworkNonce(selectedAddress)
+ }
}
TxList.prototype.render = function () {
diff --git a/ui/app/components/wallet-view.js b/ui/app/components/wallet-view.js
index 20c2be0f1..8e092364c 100644
--- a/ui/app/components/wallet-view.js
+++ b/ui/app/components/wallet-view.js
@@ -118,8 +118,18 @@ WalletView.prototype.render = function () {
return kr.accounts.includes(selectedAddress)
})
- const type = keyring.type
- const isLoose = type !== 'HD Key Tree'
+ let label = ''
+ let type
+ if (keyring) {
+ type = keyring.type
+ if (type !== 'HD Key Tree') {
+ if (type.toLowerCase().search('hardware') !== -1) {
+ label = this.context.t('hardware')
+ } else {
+ label = this.context.t('imported')
+ }
+ }
+ }
return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
style: {},
@@ -133,7 +143,7 @@ WalletView.prototype.render = function () {
onClick: hideSidebar,
}),
- h('div.wallet-view__keyring-label.allcaps', isLoose ? this.context.t('imported') : ''),
+ h('div.wallet-view__keyring-label.allcaps', label),
h('div.flex-column.flex-center.wallet-view__name-container', {
style: { margin: '0 auto' },
diff --git a/ui/app/constants/error-keys.js b/ui/app/constants/error-keys.js
index 1b89be62e..f70ed3b19 100644
--- a/ui/app/constants/error-keys.js
+++ b/ui/app/constants/error-keys.js
@@ -1,3 +1,3 @@
export const INSUFFICIENT_FUNDS_ERROR_KEY = 'insufficientFunds'
export const GAS_LIMIT_TOO_LOW_ERROR_KEY = 'gasLimitTooLow'
-export const TRANSACTION_ERROR = 'transactionError'
+export const TRANSACTION_ERROR_KEY = 'transactionError'
diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js
index a7a226cc5..38f5f1c50 100644
--- a/ui/app/conversion-util.js
+++ b/ui/app/conversion-util.js
@@ -46,7 +46,7 @@ const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
// Setter Maps
const toBigNumber = {
hex: n => new BigNumber(stripHexPrefix(n), 16),
- dec: n => new BigNumber(n, 10),
+ dec: n => new BigNumber(String(n), 10),
BN: n => new BigNumber(n.toString(16), 16),
}
const toNormalizedDenomination = {
@@ -154,7 +154,7 @@ const subtractCurrencies = (a, b, options = {}) => {
bBase,
...conversionOptions
} = options
- const value = (new BigNumber(a, aBase)).minus(b, bBase)
+ const value = (new BigNumber(String(a), aBase)).minus(b, bBase)
return converter({
value,
diff --git a/ui/app/css/itcss/components/index.scss b/ui/app/css/itcss/components/index.scss
index 96ad5fe64..821a6b612 100644
--- a/ui/app/css/itcss/components/index.scss
+++ b/ui/app/css/itcss/components/index.scss
@@ -1,7 +1,5 @@
@import './buttons.scss';
-@import './header.scss';
-
@import './footer.scss';
@import './network.scss';
diff --git a/ui/app/css/itcss/components/new-account.scss b/ui/app/css/itcss/components/new-account.scss
index b12afb124..e4c7a4e0d 100644
--- a/ui/app/css/itcss/components/new-account.scss
+++ b/ui/app/css/itcss/components/new-account.scss
@@ -162,19 +162,99 @@
}
.hw-connect {
+ width: 100%;
+
&__header {
&__title {
margin-top: 5px;
margin-bottom: 15px;
font-size: 22px;
- text-align: center;
}
&__msg {
font-size: 14px;
color: #9b9b9b;
margin-top: 10px;
- margin-bottom: 0px;
+ margin-bottom: 20px;
+ }
+ }
+
+ &__btn-wrapper {
+ flex: 1;
+ flex-direction: row;
+ display: flex;
+ }
+
+ &__connect-btn {
+ background-color: #259De5;
+ color: #fff;
+ border: none;
+ width: 315px;
+ min-height: 54px;
+ font-weight: 300;
+ font-size: 14px;
+ margin-bottom: 20px;
+ margin-top: 20px;
+ border-radius: 5px;
+ display: flex;
+ flex: 1;
+ margin-left: 20px;
+ margin-right: 20px;
+ justify-content: center;
+ text-transform: uppercase;
+ }
+
+ &__connect-btn.disabled {
+ cursor: not-allowed;
+ opacity: .5;
+ }
+
+ &__btn {
+ background: #fbfbfb;
+ border: 1px solid #e5e5e5;
+ height: 100px;
+ width: 150px;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 5px;
+
+ &__img {
+ width: 95px;
+ }
+ }
+
+ &__btn.selected {
+ border: 2px solid #00a8ee;
+ width: 149px;
+ }
+
+ &__btn:first-child {
+ margin-right: 15px;
+ margin-left: 20px;
+ }
+
+ &__btn:last-child {
+ margin-right: 20px;
+ }
+
+ &__hdPath {
+ display: flex;
+ flex-direction: row;
+ margin-top: 15px;
+ margin-bottom: 30px;
+ font-size: 14px;
+
+ &__title {
+ display: flex;
+ margin-top: 10px;
+ margin-right: 15px;
+ }
+
+ &__select {
+ display: flex;
+ flex: 1;
}
}
@@ -201,6 +281,13 @@
font-size: 18px;
}
+ &__unlock-title {
+ padding-top: 10px;
+ font-weight: 400;
+ font-size: 22px;
+ margin-bottom: 15px;
+ }
+
&__msg {
font-size: 14px;
color: #9b9b9b;
@@ -213,8 +300,6 @@
}
&__footer {
- width: 100%;
-
&__title {
padding-top: 15px;
padding-bottom: 12px;
@@ -228,6 +313,9 @@
color: #9b9b9b;
margin-top: 12px;
margin-bottom: 27px;
+ width: 100%;
+ display: block;
+ margin-left: 20px;
}
&__link {
@@ -236,10 +324,10 @@
}
}
- &__get-trezor {
+ &__get-hw {
width: 100%;
- padding-bottom: 20px;
- padding-top: 20px;
+ padding-bottom: 10px;
+ padding-top: 10px;
&__msg {
font-size: 14px;
@@ -390,6 +478,8 @@
&.account-list {
height: auto;
+ padding-left: 20px;
+ padding-right: 20px;
}
&__buttons {
@@ -412,6 +502,7 @@
min-height: 54px;
font-weight: 300;
font-size: 14px;
+ margin-bottom: 20px
}
&__button.unlock {
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/first-time/init-menu.js b/ui/app/first-time/init-menu.js
index c20ba2d77..e7bbfb225 100644
--- a/ui/app/first-time/init-menu.js
+++ b/ui/app/first-time/init-menu.js
@@ -130,7 +130,7 @@ class InitializeMenuScreen extends Component {
textDecoration: 'underline',
marginTop: '32px',
},
- }, 'Use classic interface'),
+ }, this.context.t('classicInterface')),
]),
])
diff --git a/ui/app/helpers/confirm-transaction/util.js b/ui/app/helpers/confirm-transaction/util.js
index a37778c19..76e80a8ac 100644
--- a/ui/app/helpers/confirm-transaction/util.js
+++ b/ui/app/helpers/confirm-transaction/util.js
@@ -141,7 +141,7 @@ export function hasUnconfirmedTransactions (state) {
export function roundExponential (value) {
const PRECISION = 4
- const bigNumberValue = new BigNumber(value)
+ const bigNumberValue = new BigNumber(String(value))
// In JS, numbers with exponentials greater than 20 get displayed as an exponential.
return bigNumberValue.e > 20 ? Number(bigNumberValue.toPrecision(PRECISION)) : value
diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js
index f76b73132..7be9b8d40 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: {
@@ -65,6 +66,11 @@ function reduceApp (state, action) {
buyView: {},
isMouseUser: false,
gasIsLoading: false,
+ networkNonce: null,
+ defaultHdPaths: {
+ trezor: `m/44'/60'/0'/0`,
+ ledger: `m/44'/60'/0'/0/0`,
+ },
}, state.appState)
switch (action.type) {
@@ -90,7 +96,7 @@ function reduceApp (state, action) {
sidebarOpen: false,
})
- // sidebar methods
+ // alert methods
case actions.ALERT_OPEN:
return extend(appState, {
alertOpen: true,
@@ -103,6 +109,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
@@ -525,6 +538,15 @@ function reduceApp (state, action) {
warning: '',
})
+ case actions.SET_HARDWARE_WALLET_DEFAULT_HD_PATH:
+ const { device, path } = action.value
+ const newDefaults = {...appState.defaultHdPaths}
+ newDefaults[device] = path
+
+ return extend(appState, {
+ defaultHdPaths: newDefaults,
+ })
+
case actions.SHOW_LOADING:
return extend(appState, {
isLoading: true,
@@ -710,6 +732,11 @@ function reduceApp (state, action) {
gasIsLoading: false,
})
+ case actions.SET_NETWORK_NONCE:
+ return extend(appState, {
+ networkNonce: action.value,
+ })
+
default:
return appState
}
diff --git a/ui/app/selectors/confirm-transaction.js b/ui/app/selectors/confirm-transaction.js
index 9548cf75e..6e760c429 100644
--- a/ui/app/selectors/confirm-transaction.js
+++ b/ui/app/selectors/confirm-transaction.js
@@ -147,14 +147,20 @@ export const tokenAmountAndToAddressSelector = createSelector(
export const approveTokenAmountAndToAddressSelector = createSelector(
tokenDataParamsSelector,
- params => {
+ tokenDecimalsSelector,
+ (params, tokenDecimals) => {
let toAddress = ''
let tokenAmount = 0
if (params && params.length) {
toAddress = params.find(param => param.name === TOKEN_PARAM_SPENDER).value
const value = Number(params.find(param => param.name === TOKEN_PARAM_VALUE).value)
- tokenAmount = roundExponential(value)
+
+ if (tokenDecimals) {
+ tokenAmount = calcTokenAmount(value, tokenDecimals)
+ }
+
+ tokenAmount = roundExponential(tokenAmount)
}
return {
diff --git a/ui/app/token-util.js b/ui/app/token-util.js
index 0d4233766..8798ed266 100644
--- a/ui/app/token-util.js
+++ b/ui/app/token-util.js
@@ -44,7 +44,7 @@ async function getSymbolAndDecimals (tokenAddress, existingTokens = []) {
function calcTokenAmount (value, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
- return new BigNumber(value).div(multiplier).toNumber()
+ return new BigNumber(String(value)).div(multiplier).toNumber()
}
diff --git a/ui/app/util.js b/ui/app/util.js
index 8b194e0c7..ade4fec8a 100644
--- a/ui/app/util.js
+++ b/ui/app/util.js
@@ -271,9 +271,9 @@ function getContractAtAddress (tokenAddress) {
return global.eth.contract(abi).at(tokenAddress)
}
-function exportAsFile (filename, data) {
+function exportAsFile (filename, data, type = 'text/csv') {
// source: https://stackoverflow.com/a/33542499 by Ludovic Feltz
- const blob = new Blob([data], {type: 'text/csv'})
+ const blob = new Blob([data], {type})
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveBlob(blob, filename)
} else {