mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Merge branch 'master' into ImportAccountMessageV2
This commit is contained in:
commit
a4bd7992cd
@ -4,11 +4,16 @@
|
||||
|
||||
- Change Loose label to Imported.
|
||||
- Add Imported Account disclaimer.
|
||||
- Allow adding custom tokens to classic ui when balance is 0
|
||||
- Allow editing of symbol and decimal info when adding custom token in new-ui
|
||||
- NewUI shapeshift form can select all coins (not just BTC)
|
||||
- Add most of Microsoft Edge support.
|
||||
|
||||
## 4.1.3 2018-2-28
|
||||
|
||||
- Ensure MetaMask's inpage provider is named MetamaskInpageProvider to keep some sites from breaking.
|
||||
- Add retry transaction button back into classic ui.
|
||||
- Add network dropdown styles to support long custom RPC urls
|
||||
|
||||
## 4.1.2 2018-2-28
|
||||
|
||||
|
@ -16,6 +16,7 @@ const firstTimeState = require('./first-time-state')
|
||||
const setupRaven = require('./lib/setupRaven')
|
||||
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
|
||||
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
|
||||
const EdgeEncryptor = require('./edge-encryptor')
|
||||
|
||||
|
||||
const STORAGE_KEY = 'metamask-config'
|
||||
@ -32,6 +33,12 @@ global.METAMASK_NOTIFIER = notificationManager
|
||||
const release = platform.getVersion()
|
||||
const raven = setupRaven({ release })
|
||||
|
||||
// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||
// Internet Explorer 6-11
|
||||
const isIE = !!document.documentMode
|
||||
// Edge 20+
|
||||
const isEdge = !isIE && !!window.StyleMedia
|
||||
|
||||
let popupIsOpen = false
|
||||
let openMetamaskTabsIDs = {}
|
||||
|
||||
@ -81,6 +88,7 @@ function setupController (initState) {
|
||||
initState,
|
||||
// platform specific api
|
||||
platform,
|
||||
encryptor: isEdge ? new EdgeEncryptor() : undefined,
|
||||
})
|
||||
global.metamaskController = controller
|
||||
|
||||
|
69
app/scripts/edge-encryptor.js
Normal file
69
app/scripts/edge-encryptor.js
Normal file
@ -0,0 +1,69 @@
|
||||
const asmcrypto = require('asmcrypto.js')
|
||||
const Unibabel = require('browserify-unibabel')
|
||||
|
||||
class EdgeEncryptor {
|
||||
|
||||
encrypt (password, dataObject) {
|
||||
|
||||
var salt = this._generateSalt()
|
||||
return this._keyFromPassword(password, salt)
|
||||
.then(function (key) {
|
||||
|
||||
var data = JSON.stringify(dataObject)
|
||||
var dataBuffer = Unibabel.utf8ToBuffer(data)
|
||||
var vector = global.crypto.getRandomValues(new Uint8Array(16))
|
||||
var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector)
|
||||
|
||||
var buffer = new Uint8Array(resultbuffer)
|
||||
var vectorStr = Unibabel.bufferToBase64(vector)
|
||||
var vaultStr = Unibabel.bufferToBase64(buffer)
|
||||
return JSON.stringify({
|
||||
data: vaultStr,
|
||||
iv: vectorStr,
|
||||
salt: salt,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
decrypt (password, text) {
|
||||
|
||||
const payload = JSON.parse(text)
|
||||
const salt = payload.salt
|
||||
return this._keyFromPassword(password, salt)
|
||||
.then(function (key) {
|
||||
const encryptedData = Unibabel.base64ToBuffer(payload.data)
|
||||
const vector = Unibabel.base64ToBuffer(payload.iv)
|
||||
return new Promise((resolve, reject) => {
|
||||
var result
|
||||
try {
|
||||
result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector)
|
||||
} catch (err) {
|
||||
return reject(new Error('Incorrect password'))
|
||||
}
|
||||
const decryptedData = new Uint8Array(result)
|
||||
const decryptedStr = Unibabel.bufferToUtf8(decryptedData)
|
||||
const decryptedObj = JSON.parse(decryptedStr)
|
||||
resolve(decryptedObj)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_keyFromPassword (password, salt) {
|
||||
|
||||
var passBuffer = Unibabel.utf8ToBuffer(password)
|
||||
var saltBuffer = Unibabel.base64ToBuffer(salt)
|
||||
return new Promise((resolve) => {
|
||||
var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000)
|
||||
resolve(key)
|
||||
})
|
||||
}
|
||||
|
||||
_generateSalt (byteCount = 32) {
|
||||
var view = new Uint8Array(byteCount)
|
||||
global.crypto.getRandomValues(view)
|
||||
var b64encoded = btoa(String.fromCharCode.apply(null, view))
|
||||
return b64encoded
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EdgeEncryptor
|
@ -1,13 +1,12 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {connect} from 'react-redux'
|
||||
import LoadingScreen from './loading-screen'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
createNewVaultAndRestore,
|
||||
hideWarning,
|
||||
displayWarning,
|
||||
unMarkPasswordForgotten,
|
||||
clearNotices,
|
||||
} from '../../../../ui/app/actions'
|
||||
|
||||
class ImportSeedPhraseScreen extends Component {
|
||||
@ -17,8 +16,8 @@ class ImportSeedPhraseScreen extends Component {
|
||||
next: PropTypes.func.isRequired,
|
||||
createNewVaultAndRestore: PropTypes.func.isRequired,
|
||||
hideWarning: PropTypes.func.isRequired,
|
||||
isLoading: PropTypes.bool.isRequired,
|
||||
displayWarning: PropTypes.func,
|
||||
leaveImportSeedScreenState: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -27,98 +26,130 @@ class ImportSeedPhraseScreen extends Component {
|
||||
confirmPassword: '',
|
||||
}
|
||||
|
||||
parseSeedPhrase = (seedPhrase) => {
|
||||
return seedPhrase
|
||||
.match(/\w+/g)
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
onChange = ({ seedPhrase, password, confirmPassword }) => {
|
||||
const {
|
||||
password: prevPassword,
|
||||
confirmPassword: prevConfirmPassword,
|
||||
} = this.state
|
||||
const { displayWarning, hideWarning } = this.props
|
||||
|
||||
let warning = null
|
||||
|
||||
if (seedPhrase && this.parseSeedPhrase(seedPhrase).split(' ').length !== 12) {
|
||||
warning = 'Seed Phrases are 12 words long'
|
||||
} else if (password && password.length < 8) {
|
||||
warning = 'Passwords require a mimimum length of 8'
|
||||
} else if ((password || prevPassword) !== (confirmPassword || prevConfirmPassword)) {
|
||||
warning = 'Confirmed password does not match'
|
||||
}
|
||||
|
||||
if (warning) {
|
||||
displayWarning(warning)
|
||||
} else {
|
||||
hideWarning()
|
||||
}
|
||||
|
||||
seedPhrase && this.setState({ seedPhrase })
|
||||
password && this.setState({ password })
|
||||
confirmPassword && this.setState({ confirmPassword })
|
||||
}
|
||||
|
||||
onClick = () => {
|
||||
const { password, seedPhrase, confirmPassword } = this.state
|
||||
const { createNewVaultAndRestore, next, displayWarning, leaveImportSeedScreenState } = this.props
|
||||
const { password, seedPhrase } = this.state
|
||||
const {
|
||||
createNewVaultAndRestore,
|
||||
next,
|
||||
displayWarning,
|
||||
leaveImportSeedScreenState,
|
||||
} = this.props
|
||||
|
||||
if (seedPhrase.split(' ').length !== 12) {
|
||||
this.warning = 'Seed Phrases are 12 words long'
|
||||
displayWarning(this.warning)
|
||||
return
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
this.warning = 'Passwords require a mimimum length of 8'
|
||||
displayWarning(this.warning)
|
||||
return
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
this.warning = 'Confirmed password does not match'
|
||||
displayWarning(this.warning)
|
||||
return
|
||||
}
|
||||
this.warning = null
|
||||
leaveImportSeedScreenState()
|
||||
createNewVaultAndRestore(password, seedPhrase)
|
||||
createNewVaultAndRestore(password, this.parseSeedPhrase(seedPhrase))
|
||||
.then(next)
|
||||
}
|
||||
|
||||
render () {
|
||||
return this.props.isLoading
|
||||
? <LoadingScreen loadingMessage="Creating your new account" />
|
||||
: (
|
||||
<div className="import-account">
|
||||
<a
|
||||
className="import-account__back-button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
this.props.back()
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
<div className="import-account__title">
|
||||
Import an Account with Seed Phrase
|
||||
</div>
|
||||
<div className="import-account__selector-label">
|
||||
Enter your secret twelve word phrase here to restore your vault.
|
||||
</div>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label className="import-account__input-label">Wallet Seed</label>
|
||||
<textarea
|
||||
className="import-account__secret-phrase"
|
||||
onChange={e => this.setState({seedPhrase: e.target.value})}
|
||||
placeholder="Separate each word with a single space"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="error"
|
||||
>
|
||||
{this.props.warning}
|
||||
</span>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label className="import-account__input-label">New Password</label>
|
||||
<input
|
||||
className="first-time-flow__input"
|
||||
type="password"
|
||||
placeholder="New Password (min 8 characters)"
|
||||
onChange={e => this.setState({password: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label className="import-account__input-label">Confirm Password</label>
|
||||
<input
|
||||
className="first-time-flow__input"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
onChange={e => this.setState({confirmPassword: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={this.onClick}
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
const { seedPhrase, password, confirmPassword } = this.state
|
||||
const { warning } = this.props
|
||||
const importDisabled = warning || !seedPhrase || !password || !confirmPassword
|
||||
return (
|
||||
<div className="import-account">
|
||||
<a
|
||||
className="import-account__back-button"
|
||||
onClick={e => {
|
||||
e.preventDefault()
|
||||
this.props.back()
|
||||
}}
|
||||
href="#"
|
||||
>
|
||||
{`< Back`}
|
||||
</a>
|
||||
<div className="import-account__title">
|
||||
Import an Account with Seed Phrase
|
||||
</div>
|
||||
)
|
||||
<div className="import-account__selector-label">
|
||||
Enter your secret twelve word phrase here to restore your vault.
|
||||
</div>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label className="import-account__input-label">Wallet Seed</label>
|
||||
<textarea
|
||||
className="import-account__secret-phrase"
|
||||
onChange={e => this.onChange({seedPhrase: e.target.value})}
|
||||
value={this.state.seedPhrase}
|
||||
placeholder="Separate each word with a single space"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className="error"
|
||||
>
|
||||
{this.props.warning}
|
||||
</span>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label className="import-account__input-label">New Password</label>
|
||||
<input
|
||||
className="first-time-flow__input"
|
||||
type="password"
|
||||
placeholder="New Password (min 8 characters)"
|
||||
onChange={e => this.onChange({password: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
<div className="import-account__input-wrapper">
|
||||
<label
|
||||
className="import-account__input-label"
|
||||
className={classnames('import-account__input-label', {
|
||||
'import-account__input-label__disabled': password.length < 8,
|
||||
})}
|
||||
>Confirm Password</label>
|
||||
<input
|
||||
className={classnames('first-time-flow__input', {
|
||||
'first-time-flow__input__disabled': password.length < 8,
|
||||
})}
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
onChange={e => this.onChange({confirmPassword: e.target.value})}
|
||||
disabled={password.length < 8}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="first-time-flow__button"
|
||||
onClick={() => !importDisabled && this.onClick()}
|
||||
disabled={importDisabled}
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
({ appState: { isLoading, warning } }) => ({ isLoading, warning }),
|
||||
({ appState: { warning } }) => ({ warning }),
|
||||
dispatch => ({
|
||||
leaveImportSeedScreenState: () => {
|
||||
dispatch(unMarkPasswordForgotten())
|
||||
|
@ -1,11 +1,11 @@
|
||||
|
||||
.first-time-flow {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
background-color: #FFF;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.alpha-warning {
|
||||
@ -45,12 +45,12 @@
|
||||
.buy-ether {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 67px 0 50px 146px;
|
||||
margin: 67px 0 50px;
|
||||
max-width: 35rem;
|
||||
}
|
||||
|
||||
.create-password {
|
||||
margin: 67px 0 50px 0px;
|
||||
margin: 67px 0 50px;
|
||||
}
|
||||
|
||||
.import-account {
|
||||
@ -418,6 +418,10 @@ button.backup-phrase__confirm-seed-option:hover {
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.import-account__input-label__disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.import-account__input {
|
||||
width: 325px !important;
|
||||
}
|
||||
@ -564,6 +568,10 @@ button.backup-phrase__confirm-seed-option:hover {
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
.first-time-flow__input__disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.first-time-flow__input::placeholder {
|
||||
color: #9B9B9B;
|
||||
font-weight: 200;
|
||||
|
@ -54,6 +54,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"abi-decoder": "^1.0.9",
|
||||
"asmcrypto.js": "0.22.0",
|
||||
"async": "^2.5.0",
|
||||
"await-semaphore": "^0.1.1",
|
||||
"babel-runtime": "^6.23.0",
|
||||
@ -64,6 +65,7 @@
|
||||
"boron": "^0.2.3",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"browserify-derequire": "^0.9.4",
|
||||
"browserify-unibabel": "^3.0.0",
|
||||
"classnames": "^2.2.5",
|
||||
"client-sw-ready-event": "^3.3.0",
|
||||
"clone": "^2.1.1",
|
||||
@ -78,11 +80,11 @@
|
||||
"eslint-plugin-react": "^7.4.0",
|
||||
"eth-bin-to-ops": "^1.0.1",
|
||||
"eth-block-tracker": "^2.3.0",
|
||||
"eth-contract-metadata": "^1.1.5",
|
||||
"eth-hd-keyring": "^1.2.1",
|
||||
"eth-json-rpc-filters": "^1.2.5",
|
||||
"eth-json-rpc-infura": "^3.0.0",
|
||||
"eth-keyring-controller": "^2.1.4",
|
||||
"eth-contract-metadata": "^1.1.5",
|
||||
"eth-hd-keyring": "^1.2.1",
|
||||
"eth-phishing-detect": "^1.1.4",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-sig-util": "^1.4.2",
|
||||
|
101
test/unit/edge-encryptor-test.js
Normal file
101
test/unit/edge-encryptor-test.js
Normal file
@ -0,0 +1,101 @@
|
||||
const assert = require('assert')
|
||||
|
||||
const EdgeEncryptor = require('../../app/scripts/edge-encryptor')
|
||||
|
||||
var password = 'passw0rd1'
|
||||
var data = 'some random data'
|
||||
|
||||
global.crypto = global.crypto || {
|
||||
getRandomValues: function (array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] = Math.random() * 100
|
||||
}
|
||||
return array
|
||||
}
|
||||
}
|
||||
|
||||
describe('EdgeEncryptor', function () {
|
||||
|
||||
const edgeEncryptor = new EdgeEncryptor()
|
||||
describe('encrypt', function () {
|
||||
|
||||
it('should encrypt the data.', function (done) {
|
||||
edgeEncryptor.encrypt(password, data)
|
||||
.then(function (encryptedData) {
|
||||
assert.notEqual(data, encryptedData)
|
||||
assert.notEqual(encryptedData.length, 0)
|
||||
done()
|
||||
}).catch(function (err) {
|
||||
done(err)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return proper format.', function (done) {
|
||||
edgeEncryptor.encrypt(password, data)
|
||||
.then(function (encryptedData) {
|
||||
let encryptedObject = JSON.parse(encryptedData)
|
||||
assert.ok(encryptedObject.data, 'there is no data')
|
||||
assert.ok(encryptedObject.iv && encryptedObject.iv.length != 0, 'there is no iv')
|
||||
assert.ok(encryptedObject.salt && encryptedObject.salt.length != 0, 'there is no salt')
|
||||
done()
|
||||
}).catch(function (err) {
|
||||
done(err)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not return the same twice.', function (done) {
|
||||
|
||||
const encryptPromises = []
|
||||
encryptPromises.push(edgeEncryptor.encrypt(password, data))
|
||||
encryptPromises.push(edgeEncryptor.encrypt(password, data))
|
||||
|
||||
Promise.all(encryptPromises).then((encryptedData) => {
|
||||
assert.equal(encryptedData.length, 2)
|
||||
assert.notEqual(encryptedData[0], encryptedData[1])
|
||||
assert.notEqual(encryptedData[0].length, 0)
|
||||
assert.notEqual(encryptedData[1].length, 0)
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('decrypt', function () {
|
||||
it('should be able to decrypt the encrypted data.', function (done) {
|
||||
|
||||
edgeEncryptor.encrypt(password, data)
|
||||
.then(function (encryptedData) {
|
||||
edgeEncryptor.decrypt(password, encryptedData)
|
||||
.then(function (decryptedData) {
|
||||
assert.equal(decryptedData, data)
|
||||
done()
|
||||
})
|
||||
.catch(function (err) {
|
||||
done(err)
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
done(err)
|
||||
})
|
||||
})
|
||||
|
||||
it('cannot decrypt the encrypted data with wrong password.', function (done) {
|
||||
|
||||
edgeEncryptor.encrypt(password, data)
|
||||
.then(function (encryptedData) {
|
||||
edgeEncryptor.decrypt('wrong password', encryptedData)
|
||||
.then(function (decryptedData) {
|
||||
assert.fail('could decrypt with wrong password')
|
||||
done()
|
||||
})
|
||||
.catch(function (err) {
|
||||
assert.ok(err instanceof Error)
|
||||
assert.equal(err.message, 'Incorrect password')
|
||||
done()
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
done(err)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -90,6 +90,7 @@ function mapStateToProps (state) {
|
||||
isMouseUser: state.appState.isMouseUser,
|
||||
betaUI: state.metamask.featureFlags.betaUI,
|
||||
isRevealingSeedWords: state.metamask.isRevealingSeedWords,
|
||||
Qr: state.appState.Qr,
|
||||
|
||||
// state needed to get account dropdown temporarily rendering from app bar
|
||||
identities,
|
||||
@ -287,10 +288,10 @@ App.prototype.renderAppBar = function () {
|
||||
}),
|
||||
|
||||
// metamask name
|
||||
h('h1', 'MetaMask'),
|
||||
|
||||
h('div.beta-label', 'BETA'),
|
||||
|
||||
h('.flex-row', [
|
||||
h('h1', 'MetaMask'),
|
||||
h('div.beta-label', 'BETA'),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div.header__right-actions', [
|
||||
@ -368,6 +369,7 @@ App.prototype.renderPrimary = function () {
|
||||
isOnboarding,
|
||||
betaUI,
|
||||
isRevealingSeedWords,
|
||||
Qr,
|
||||
} = props
|
||||
const isMascaraOnboarding = isMascara && isOnboarding
|
||||
const isBetaUIOnboarding = betaUI && isOnboarding && !props.isPopup && !isRevealingSeedWords
|
||||
@ -508,7 +510,7 @@ App.prototype.renderPrimary = function () {
|
||||
width: '285px',
|
||||
},
|
||||
}, [
|
||||
h(QrView, {key: 'qr'}),
|
||||
h(QrView, {key: 'qr', Qr}),
|
||||
]),
|
||||
])
|
||||
|
||||
|
@ -51,8 +51,7 @@ ShapeshiftForm.prototype.componentWillMount = function () {
|
||||
this.props.shapeShiftSubview()
|
||||
}
|
||||
|
||||
ShapeshiftForm.prototype.onCoinChange = function (e) {
|
||||
const coin = e.target.value
|
||||
ShapeshiftForm.prototype.onCoinChange = function (coin) {
|
||||
this.setState({
|
||||
depositCoin: coin,
|
||||
errorMessage: '',
|
||||
@ -133,7 +132,7 @@ ShapeshiftForm.prototype.renderMarketInfo = function () {
|
||||
}
|
||||
|
||||
ShapeshiftForm.prototype.renderQrCode = function () {
|
||||
const { depositAddress, isLoading } = this.state
|
||||
const { depositAddress, isLoading, depositCoin } = this.state
|
||||
const qrImage = qrcode(4, 'M')
|
||||
qrImage.addData(depositAddress)
|
||||
qrImage.make()
|
||||
@ -141,7 +140,7 @@ ShapeshiftForm.prototype.renderQrCode = function () {
|
||||
return h('div.shapeshift-form', {}, [
|
||||
|
||||
h('div.shapeshift-form__deposit-instruction', [
|
||||
'Deposit your BTC to the address below:',
|
||||
`Deposit your ${depositCoin.toUpperCase()} to the address below:`,
|
||||
]),
|
||||
|
||||
h('div', depositAddress),
|
||||
@ -182,7 +181,7 @@ ShapeshiftForm.prototype.render = function () {
|
||||
|
||||
h(SimpleDropdown, {
|
||||
selectedOption: this.state.depositCoin,
|
||||
onSelect: this.onCoinChange,
|
||||
onSelect: (coin) => this.onCoinChange(coin),
|
||||
options: Object.entries(coinOptions).map(([coin]) => ({
|
||||
value: coin.toLowerCase(),
|
||||
displayValue: coin,
|
||||
|
@ -80,11 +80,10 @@
|
||||
font-family: Roboto;
|
||||
text-transform: uppercase;
|
||||
font-weight: 500;
|
||||
font-size: 0.8rem;
|
||||
padding-left: 9px;
|
||||
font-size: .8rem;
|
||||
color: $buttercup;
|
||||
align-self: flex-start;
|
||||
margin-top: 10px;
|
||||
margin-left: 5px;
|
||||
line-height: initial;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
display: none;
|
||||
|
Loading…
Reference in New Issue
Block a user