commit
5fbd256ad9
|
@ -11,9 +11,9 @@ workflows:
|
|||
- prep-docs:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-scss:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
# - prep-scss:
|
||||
# requires:
|
||||
# - prep-deps-npm
|
||||
- test-lint:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
|
@ -42,11 +42,11 @@ workflows:
|
|||
- test-integration-flat-chrome:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-scss
|
||||
# - prep-scss
|
||||
- test-integration-flat-firefox:
|
||||
requires:
|
||||
- prep-deps-npm
|
||||
- prep-scss
|
||||
# - prep-scss
|
||||
- all-tests-pass:
|
||||
requires:
|
||||
- test-lint
|
||||
|
@ -369,12 +369,6 @@ jobs:
|
|||
command: ./.circleci/scripts/firefox-install
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ .Revision }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- run:
|
||||
name: test:integration:flat
|
||||
command: npm run test:flat
|
||||
|
@ -388,12 +382,6 @@ jobs:
|
|||
- checkout
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ .Revision }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- run:
|
||||
name: test:integration:flat
|
||||
command: npm run test:flat
|
||||
|
@ -412,12 +400,6 @@ jobs:
|
|||
command: ./.circleci/scripts/firefox-install
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ .Revision }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- run:
|
||||
name: test:integration:mascara
|
||||
command: npm run test:mascara
|
||||
|
@ -431,12 +413,6 @@ jobs:
|
|||
- checkout
|
||||
- restore_cache:
|
||||
key: dependency-cache-{{ .Revision }}
|
||||
- run:
|
||||
name: Get Scss Cache key
|
||||
# this allows us to checksum against a whole directory
|
||||
command: find ui/app/css -type f -exec md5sum {} \; | sort -k 2 > scss_checksum
|
||||
- restore_cache:
|
||||
key: scss-cache-{{ checksum "scss_checksum" }}
|
||||
- run:
|
||||
name: test:integration:mascara
|
||||
command: npm run test:mascara
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## Current Master
|
||||
|
||||
## 4.11.10 Tue Feb 04 2020
|
||||
|
||||
- [#313](https://github.com/poanetwork/nifty-wallet/pull/313) - Change Ethereum classic RPC endpoint
|
||||
|
||||
## 4.11.9 Thu Aug 22 2019
|
||||
|
||||
- [#303](https://github.com/poanetwork/nifty-wallet/pull/303): (Feature) Add Pocket Network
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "__MSG_appName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "4.11.9",
|
||||
"version": "4.11.10",
|
||||
"manifest_version": 2,
|
||||
"author": "POA Network",
|
||||
"description": "__MSG_appDescription__",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
const injectCss = require('inject-css')
|
||||
const OldMetaMaskUiCss = require('../../old-ui/css')
|
||||
const NewMetaMaskUiCss = require('../../ui/css')
|
||||
const startPopup = require('./popup-core')
|
||||
const PortStream = require('extension-port-stream')
|
||||
const { getEnvironmentType } = require('./lib/util')
|
||||
|
@ -44,27 +43,11 @@ async function start () {
|
|||
// Code commented out until we begin auto adding users to NewUI
|
||||
// const { isMascara, identities = {}, featureFlags = {} } = store.getState().metamask
|
||||
// const firstTime = Object.keys(identities).length === 0
|
||||
const { isMascara, featureFlags = {} } = store.getState().metamask
|
||||
let betaUIState = featureFlags.betaUI
|
||||
|
||||
// Code commented out until we begin auto adding users to NewUI
|
||||
// const useBetaCss = isMascara || firstTime || betaUIState
|
||||
const useBetaCss = isMascara || betaUIState
|
||||
|
||||
let css = useBetaCss ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
|
||||
let deleteInjectedCss = injectCss(css)
|
||||
let newBetaUIState
|
||||
|
||||
store.subscribe(() => {
|
||||
const state = store.getState()
|
||||
newBetaUIState = state.metamask.featureFlags.betaUI
|
||||
if (newBetaUIState !== betaUIState) {
|
||||
deleteInjectedCss()
|
||||
betaUIState = newBetaUIState
|
||||
css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
|
||||
deleteInjectedCss = injectCss(css)
|
||||
}
|
||||
})
|
||||
const css = OldMetaMaskUiCss()
|
||||
injectCss(css)
|
||||
})
|
||||
|
||||
|
||||
|
|
53
gulpfile.js
53
gulpfile.js
|
@ -17,8 +17,6 @@ const fs = require('fs')
|
|||
const path = require('path')
|
||||
const manifest = require('./app/manifest.json')
|
||||
const mkdirp = require('mkdirp')
|
||||
const sass = require('gulp-sass')
|
||||
const autoprefixer = require('gulp-autoprefixer')
|
||||
const gulpStylelint = require('gulp-stylelint')
|
||||
const stylefmt = require('gulp-stylefmt')
|
||||
const uglify = require('gulp-uglify-es').default
|
||||
|
@ -214,51 +212,6 @@ gulp.task('dev:copy',
|
|||
)
|
||||
)
|
||||
|
||||
// scss compilation and autoprefixing tasks
|
||||
|
||||
gulp.task('build:scss', createScssBuildTask({
|
||||
src: 'ui/app/css/index.scss',
|
||||
dest: 'ui/app/css/output',
|
||||
devMode: false,
|
||||
}))
|
||||
|
||||
gulp.task('dev:scss', createScssBuildTask({
|
||||
src: 'ui/app/css/index.scss',
|
||||
dest: 'ui/app/css/output',
|
||||
devMode: true,
|
||||
pattern: 'ui/app/**/*.scss',
|
||||
}))
|
||||
|
||||
function createScssBuildTask ({ src, dest, devMode, pattern }) {
|
||||
return function () {
|
||||
if (devMode) {
|
||||
watch(pattern, async (event) => {
|
||||
const stream = buildScss()
|
||||
await endOfStream(stream)
|
||||
livereload.changed(event.path)
|
||||
})
|
||||
return buildScssWithSourceMaps()
|
||||
}
|
||||
return buildScss()
|
||||
}
|
||||
|
||||
function buildScssWithSourceMaps () {
|
||||
return gulp.src(src)
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(sourcemaps.write())
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest(dest))
|
||||
}
|
||||
|
||||
function buildScss () {
|
||||
return gulp.src(src)
|
||||
.pipe(sass().on('error', sass.logError))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulp.dest(dest))
|
||||
}
|
||||
}
|
||||
|
||||
gulp.task('lint-scss', function () {
|
||||
return gulp
|
||||
.src('ui/app/css/itcss/**/*.scss')
|
||||
|
@ -371,7 +324,6 @@ gulp.task('zip', gulp.parallel('zip:chrome', 'zip:firefox', 'zip:edge', 'zip:ope
|
|||
gulp.task('dev',
|
||||
gulp.series(
|
||||
'clean',
|
||||
'dev:scss',
|
||||
gulp.parallel(
|
||||
'dev:extension:js',
|
||||
'dev:mascara:js',
|
||||
|
@ -384,7 +336,6 @@ gulp.task('dev',
|
|||
gulp.task('dev:extension',
|
||||
gulp.series(
|
||||
'clean',
|
||||
'dev:scss',
|
||||
gulp.parallel(
|
||||
'dev:extension:js',
|
||||
'dev:copy',
|
||||
|
@ -396,7 +347,6 @@ gulp.task('dev:extension',
|
|||
gulp.task('dev:mascara',
|
||||
gulp.series(
|
||||
'clean',
|
||||
'dev:scss',
|
||||
gulp.parallel(
|
||||
'dev:mascara:js',
|
||||
'dev:copy',
|
||||
|
@ -408,7 +358,6 @@ gulp.task('dev:mascara',
|
|||
gulp.task('build',
|
||||
gulp.series(
|
||||
'clean',
|
||||
'build:scss',
|
||||
gulpParallel(
|
||||
'build:extension:js',
|
||||
'build:mascara:js',
|
||||
|
@ -420,7 +369,6 @@ gulp.task('build',
|
|||
gulp.task('build:extension',
|
||||
gulp.series(
|
||||
'clean',
|
||||
'build:scss',
|
||||
gulp.parallel(
|
||||
'build:extension:js',
|
||||
'copy'
|
||||
|
@ -431,7 +379,6 @@ gulp.task('build:extension',
|
|||
gulp.task('build:mascara',
|
||||
gulp.series(
|
||||
'clean',
|
||||
'build:scss',
|
||||
gulp.parallel(
|
||||
'build:mascara:js',
|
||||
'copy'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const injectCss = require('inject-css')
|
||||
const SwController = require('sw-controller')
|
||||
const SwStream = require('sw-stream')
|
||||
const MetaMaskUiCss = require('../../ui/css')
|
||||
const MetaMaskUiCss = require('../../old-ui/css')
|
||||
const MetamascaraPlatform = require('../../app/scripts/platforms/window')
|
||||
const startPopup = require('../../app/scripts/popup-core')
|
||||
|
||||
|
|
|
@ -180,26 +180,6 @@ class ConfigScreen extends Component {
|
|||
},
|
||||
}),
|
||||
|
||||
h('p', {
|
||||
style: {
|
||||
fontFamily: 'Nunito Regular',
|
||||
fontSize: '14px',
|
||||
lineHeight: '18px',
|
||||
},
|
||||
}, [
|
||||
'Switch to Decentralized Provider (Pocket)',
|
||||
]),
|
||||
|
||||
h('input', {
|
||||
type: 'checkbox',
|
||||
name: 'pocket-checkbox',
|
||||
checked: this.state.dProvider,
|
||||
onChange: (event) => {
|
||||
event.preventDefault()
|
||||
this.toggleProvider()
|
||||
},
|
||||
}),
|
||||
|
||||
h('p.config-title', `Provider`),
|
||||
|
||||
h('div', {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -117,7 +117,7 @@
|
|||
"eth-keychain-controller": "github:vbaranov/KeyringController#simple-address",
|
||||
"eth-ledger-bridge-keyring": "github:vbaranov/eth-ledger-bridge-keyring#0.1.0-clear-accounts-flag",
|
||||
"eth-method-registry": "^1.0.0",
|
||||
"eth-net-props": "^1.0.24",
|
||||
"eth-net-props": "^1.0.25",
|
||||
"eth-phishing-detect": "^1.1.4",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-sig-util": "^2.2.0",
|
||||
|
@ -141,7 +141,6 @@
|
|||
"fuse.js": "^3.2.0",
|
||||
"gulp": "github:gulpjs/gulp#v4.0.0",
|
||||
"gulp-autoprefixer": "^5.0.0",
|
||||
"gulp-sass": "^4.0.0",
|
||||
"human-standard-token-abi": "^2.0.0",
|
||||
"idb-global": "^2.1.0",
|
||||
"iframe-stream": "^3.0.0",
|
||||
|
@ -232,7 +231,7 @@
|
|||
"brfs": "^1.6.1",
|
||||
"browserify": "^16.2.3",
|
||||
"chai": "^4.1.0",
|
||||
"chromedriver": "^76.0.0",
|
||||
"chromedriver": "^2.41.0",
|
||||
"clipboardy": "^1.2.3",
|
||||
"compression": "^1.7.1",
|
||||
"coveralls": "^3.0.0",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,36 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>E2E Test Dapp</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Contract</div>
|
||||
<div style="display: flex;">
|
||||
<button id="deployButton">Deploy Contract</button>
|
||||
<button id="depositButton">Deposit</button>
|
||||
<button id="withdrawButton">Withdraw</button>
|
||||
</div>
|
||||
<div id="contractStatus" style="display: flex; font-size: 1rem;">
|
||||
Not clicked
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Send eth</div>
|
||||
<div style="display: flex;">
|
||||
<button id="sendButton">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; flex-flow: column;">
|
||||
<div style="display: flex; font-size: 1.25rem;">Send tokens</div>
|
||||
<div id="tokenAddress"></div>
|
||||
<div style="display: flex;">
|
||||
<button id="createToken">Create Token</button>
|
||||
<button id="transferTokens">Transfer Tokens</button>
|
||||
<button id="approveTokens">Approve Tokens</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="contract.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,286 +0,0 @@
|
|||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
const webdriver = require('selenium-webdriver')
|
||||
const { By, until } = webdriver
|
||||
const {
|
||||
delay,
|
||||
buildChromeWebDriver,
|
||||
buildFirefoxWebdriver,
|
||||
installWebExt,
|
||||
getExtensionIdChrome,
|
||||
getExtensionIdFirefox,
|
||||
} = require('../func')
|
||||
const {
|
||||
checkBrowserForConsoleErrors,
|
||||
closeAllWindowHandlesExcept,
|
||||
findElement,
|
||||
findElements,
|
||||
loadExtension,
|
||||
openNewPage,
|
||||
verboseReportOnFailure,
|
||||
waitUntilXWindowHandles,
|
||||
} = require('./helpers')
|
||||
|
||||
describe('MetaMask', function () {
|
||||
let extensionId
|
||||
let driver
|
||||
|
||||
const tinyDelayMs = 200
|
||||
const regularDelayMs = tinyDelayMs * 2
|
||||
const largeDelayMs = regularDelayMs * 2
|
||||
|
||||
this.timeout(0)
|
||||
this.bail(true)
|
||||
|
||||
before(async function () {
|
||||
switch (process.env.SELENIUM_BROWSER) {
|
||||
case 'chrome': {
|
||||
const extPath = path.resolve('dist/chrome')
|
||||
driver = buildChromeWebDriver(extPath)
|
||||
extensionId = await getExtensionIdChrome(driver)
|
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`)
|
||||
break
|
||||
}
|
||||
case 'firefox': {
|
||||
const extPath = path.resolve('dist/firefox')
|
||||
driver = buildFirefoxWebdriver()
|
||||
await installWebExt(driver, extPath)
|
||||
await delay(700)
|
||||
extensionId = await getExtensionIdFirefox(driver)
|
||||
await driver.get(`moz-extension://${extensionId}/popup.html`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
const errors = await checkBrowserForConsoleErrors(driver)
|
||||
if (errors.length) {
|
||||
const errorReports = errors.map(err => err.message)
|
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
|
||||
console.error(new Error(errorMessage))
|
||||
}
|
||||
}
|
||||
if (this.currentTest.state === 'failed') {
|
||||
await verboseReportOnFailure(driver, this.currentTest)
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await driver.quit()
|
||||
})
|
||||
|
||||
|
||||
describe('New UI setup', async function () {
|
||||
it('switches to first tab', async function () {
|
||||
await delay(tinyDelayMs)
|
||||
const [firstTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(firstTab)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('selects the new UI option', async () => {
|
||||
try {
|
||||
const overlay = await findElement(driver, By.css('.full-flex-height'))
|
||||
await driver.wait(until.stalenessOf(overlay))
|
||||
} catch (e) {}
|
||||
|
||||
let button
|
||||
try {
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(largeDelayMs)
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
}
|
||||
await button.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Close all other tabs
|
||||
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(tab0)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
let selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (tab0 && selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab0)
|
||||
} else if (tab1) {
|
||||
await driver.switchTo().window(tab1)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab1)
|
||||
} else if (tab2) {
|
||||
await driver.switchTo().window(tab2)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
|
||||
}
|
||||
} else {
|
||||
throw new Error('popup.html not found')
|
||||
}
|
||||
await delay(regularDelayMs)
|
||||
const [appTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(appTab)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
|
||||
await continueBtn.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Going through the first time flow', () => {
|
||||
it('accepts a secure password', async () => {
|
||||
const passwordBox = await findElement(driver, By.css('.create-password #create-password'))
|
||||
const passwordBoxConfirm = await findElement(driver, By.css('.create-password #confirm-password'))
|
||||
const button = await findElement(driver, By.css('.create-password button'))
|
||||
|
||||
await passwordBox.sendKeys('correct horse battery staple')
|
||||
await passwordBoxConfirm.sendKeys('correct horse battery staple')
|
||||
await button.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the unique image screen', async () => {
|
||||
const nextScreen = await findElement(driver, By.css('.unique-image button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the ToS', async () => {
|
||||
// terms of use
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
assert.equal(canClickThrough, false, 'disabled continue button')
|
||||
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
|
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
|
||||
await delay(regularDelayMs)
|
||||
const acceptTos = await findElement(driver, By.css('.tou button'))
|
||||
driver.wait(until.elementIsEnabled(acceptTos))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the phishing notice', async () => {
|
||||
// phishing notice
|
||||
const noticeElement = await driver.findElement(By.css('.markdown'))
|
||||
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
|
||||
await delay(regularDelayMs)
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
let seedPhrase
|
||||
|
||||
it('reveals the seed phrase', async () => {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
await driver.wait(until.elementLocated(byRevealButton, 10000))
|
||||
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
|
||||
await revealSeedPhraseButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
seedPhrase = await driver.findElement(By.css('.backup-phrase__secret-words')).getText()
|
||||
assert.equal(seedPhrase.split(' ').length, 12)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
async function clickWordAndWait (word) {
|
||||
const xpathClass = 'backup-phrase__confirm-seed-option backup-phrase__confirm-seed-option--unselected'
|
||||
const xpath = `//button[@class='${xpathClass}' and contains(text(), '${word}')]`
|
||||
const word0 = await findElement(driver, By.xpath(xpath), 10000)
|
||||
|
||||
await word0.click()
|
||||
await delay(tinyDelayMs)
|
||||
}
|
||||
|
||||
async function retypeSeedPhrase (words, wasReloaded, count = 0) {
|
||||
try {
|
||||
if (wasReloaded) {
|
||||
const byRevealButton = By.css('.backup-phrase__secret-blocker .backup-phrase__reveal-button')
|
||||
await driver.wait(until.elementLocated(byRevealButton, 10000))
|
||||
const revealSeedPhraseButton = await findElement(driver, byRevealButton, 10000)
|
||||
await revealSeedPhraseButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const nextScreen = await findElement(driver, By.css('.backup-phrase button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await clickWordAndWait(words[i])
|
||||
}
|
||||
} catch (e) {
|
||||
if (count > 2) {
|
||||
throw e
|
||||
} else {
|
||||
await loadExtension(driver, extensionId)
|
||||
await retypeSeedPhrase(words, true, count + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('can retype the seed phrase', async () => {
|
||||
const words = seedPhrase.split(' ')
|
||||
|
||||
await retypeSeedPhrase(words)
|
||||
|
||||
const confirm = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirm.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the deposit modal', async () => {
|
||||
const byBuyModal = By.css('span .modal')
|
||||
const buyModal = await driver.wait(until.elementLocated(byBuyModal))
|
||||
const closeModal = await findElement(driver, By.css('.page-container__header-close'))
|
||||
await closeModal.click()
|
||||
await driver.wait(until.stalenessOf(buyModal))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('switches to localhost', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
|
||||
await localhost.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Drizzle', () => {
|
||||
it('should be able to detect our eth address', async () => {
|
||||
await openNewPage(driver, 'http://127.0.0.1:3000/')
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await waitUntilXWindowHandles(driver, 2)
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
const dapp = windowHandles[1]
|
||||
|
||||
await driver.switchTo().window(dapp)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
|
||||
const addressElement = await findElement(driver, By.css(`.pure-u-1-1 h4`))
|
||||
const addressText = await addressElement.getText()
|
||||
assert(addressText.match(/^0x[a-fA-F0-9]{40}$/))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,408 +0,0 @@
|
|||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
const webdriver = require('selenium-webdriver')
|
||||
const { By, Key, until } = webdriver
|
||||
const {
|
||||
delay,
|
||||
buildChromeWebDriver,
|
||||
buildFirefoxWebdriver,
|
||||
installWebExt,
|
||||
getExtensionIdChrome,
|
||||
getExtensionIdFirefox,
|
||||
} = require('../func')
|
||||
const {
|
||||
checkBrowserForConsoleErrors,
|
||||
closeAllWindowHandlesExcept,
|
||||
verboseReportOnFailure,
|
||||
findElement,
|
||||
findElements,
|
||||
loadExtension,
|
||||
} = require('./helpers')
|
||||
|
||||
|
||||
describe('Using MetaMask with an existing account', function () {
|
||||
let extensionId
|
||||
let driver
|
||||
|
||||
const testSeedPhrase = 'phrase upgrade clock rough situate wedding elder clever doctor stamp excess tent'
|
||||
const testAddress = '0xE18035BF8712672935FDB4e5e431b1a0183d2DFC'
|
||||
const testPrivateKey2 = '14abe6f4aab7f9f626fe981c864d0adeb5685f289ac9270c27b8fd790b4235d6'
|
||||
const tinyDelayMs = 500
|
||||
const regularDelayMs = 1000
|
||||
const largeDelayMs = regularDelayMs * 2
|
||||
|
||||
this.timeout(0)
|
||||
this.bail(true)
|
||||
|
||||
before(async function () {
|
||||
switch (process.env.SELENIUM_BROWSER) {
|
||||
case 'chrome': {
|
||||
const extensionPath = path.resolve('dist/chrome')
|
||||
driver = buildChromeWebDriver(extensionPath)
|
||||
extensionId = await getExtensionIdChrome(driver)
|
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`)
|
||||
await delay(regularDelayMs)
|
||||
break
|
||||
}
|
||||
case 'firefox': {
|
||||
const extensionPath = path.resolve('dist/firefox')
|
||||
driver = buildFirefoxWebdriver()
|
||||
await installWebExt(driver, extensionPath)
|
||||
await delay(regularDelayMs)
|
||||
extensionId = await getExtensionIdFirefox(driver)
|
||||
await driver.get(`moz-extension://${extensionId}/popup.html`)
|
||||
await delay(regularDelayMs)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async function () {
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
const errors = await checkBrowserForConsoleErrors(driver)
|
||||
if (errors.length) {
|
||||
const errorReports = errors.map(err => err.message)
|
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
|
||||
console.error(new Error(errorMessage))
|
||||
}
|
||||
}
|
||||
if (this.currentTest.state === 'failed') {
|
||||
await verboseReportOnFailure(driver, this.currentTest)
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await driver.quit()
|
||||
})
|
||||
|
||||
describe('New UI setup', async function () {
|
||||
it('switches to first tab', async function () {
|
||||
await delay(tinyDelayMs)
|
||||
const [firstTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(firstTab)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('selects the new UI option', async () => {
|
||||
try {
|
||||
const overlay = await findElement(driver, By.css('.full-flex-height'))
|
||||
await driver.wait(until.stalenessOf(overlay))
|
||||
} catch (e) {}
|
||||
|
||||
let button
|
||||
try {
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
} catch (e) {
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(largeDelayMs)
|
||||
button = await findElement(driver, By.xpath("//button[contains(text(), 'Try it now')]"))
|
||||
}
|
||||
await button.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Close all other tabs
|
||||
const [tab0, tab1, tab2] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(tab0)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
let selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (tab0 && selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab0)
|
||||
} else if (tab1) {
|
||||
await driver.switchTo().window(tab1)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
await delay(tinyDelayMs)
|
||||
if (selectedUrl.match(/popup.html/)) {
|
||||
await closeAllWindowHandlesExcept(driver, tab1)
|
||||
} else if (tab2) {
|
||||
await driver.switchTo().window(tab2)
|
||||
selectedUrl = await driver.getCurrentUrl()
|
||||
selectedUrl.match(/popup.html/) && await closeAllWindowHandlesExcept(driver, tab2)
|
||||
}
|
||||
} else {
|
||||
throw new Error('popup.html not found')
|
||||
}
|
||||
await delay(regularDelayMs)
|
||||
const [appTab] = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(appTab)
|
||||
await delay(tinyDelayMs)
|
||||
|
||||
await loadExtension(driver, extensionId)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const continueBtn = await findElement(driver, By.css('.welcome-screen__button'))
|
||||
await continueBtn.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('First time flow starting from an existing seed phrase', () => {
|
||||
it('imports a seed phrase', async () => {
|
||||
const [seedPhrase] = await findElements(driver, By.xpath(`//a[contains(text(), 'Import with seed phrase')]`))
|
||||
await seedPhrase.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [seedTextArea] = await findElements(driver, By.css('textarea.import-account__secret-phrase'))
|
||||
await seedTextArea.sendKeys(testSeedPhrase)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [password] = await findElements(driver, By.id('password'))
|
||||
await password.sendKeys('correct horse battery staple')
|
||||
const [confirmPassword] = await findElements(driver, By.id('confirm-password'))
|
||||
confirmPassword.sendKeys('correct horse battery staple')
|
||||
|
||||
const [importButton] = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
|
||||
await importButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the ToS', async () => {
|
||||
// terms of use
|
||||
const canClickThrough = await driver.findElement(By.css('.tou button')).isEnabled()
|
||||
assert.equal(canClickThrough, false, 'disabled continue button')
|
||||
const bottomOfTos = await findElement(driver, By.linkText('Attributions'))
|
||||
await driver.executeScript('arguments[0].scrollIntoView(true)', bottomOfTos)
|
||||
await delay(regularDelayMs)
|
||||
const acceptTos = await findElement(driver, By.css('.tou button'))
|
||||
await acceptTos.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the privacy notice', async () => {
|
||||
// privacy notice
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('clicks through the phishing notice', async () => {
|
||||
// phishing notice
|
||||
const noticeElement = await driver.findElement(By.css('.markdown'))
|
||||
await driver.executeScript('arguments[0].scrollTop = arguments[0].scrollHeight', noticeElement)
|
||||
await delay(regularDelayMs)
|
||||
const nextScreen = await findElement(driver, By.css('.tou button'))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Show account information', () => {
|
||||
it('shows the correct account address', async () => {
|
||||
await driver.findElement(By.css('.wallet-view__details-button')).click()
|
||||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [address] = await findElements(driver, By.css('input.qr-ellip-address'))
|
||||
assert.equal(await address.getAttribute('value'), testAddress)
|
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()")
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('shows a QR code for the account', async () => {
|
||||
await driver.findElement(By.css('.wallet-view__details-button')).click()
|
||||
await driver.findElement(By.css('.qr-wrapper')).isDisplayed()
|
||||
const detailModal = await driver.findElement(By.css('span .modal'))
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await driver.executeScript("document.querySelector('.account-modal-close').click()")
|
||||
await driver.wait(until.stalenessOf(detailModal))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Log out and log back in', () => {
|
||||
it('logs out of the account', async () => {
|
||||
const accountIdenticon = driver.findElement(By.css('.account-menu__icon .identicon'))
|
||||
accountIdenticon.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [logoutButton] = await findElements(driver, By.css('.account-menu__logout-button'))
|
||||
assert.equal(await logoutButton.getText(), 'Log out')
|
||||
await logoutButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('accepts the account password after lock', async () => {
|
||||
await driver.findElement(By.id('password')).sendKeys('correct horse battery staple')
|
||||
await driver.findElement(By.id('password')).sendKeys(Key.ENTER)
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Add an account', () => {
|
||||
it('switches to localhost', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [localhost] = await findElements(driver, By.xpath(`//span[contains(text(), 'Localhost')]`))
|
||||
await localhost.click()
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('choose Create Account from the account menu', async () => {
|
||||
await driver.findElement(By.css('.account-menu__icon')).click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [createAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Create Account')]`))
|
||||
await createAccount.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('set account name', async () => {
|
||||
const [accountName] = await findElements(driver, By.css('.new-account-create-form input'))
|
||||
await accountName.sendKeys('2nd account')
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [createButton] = await findElements(driver, By.xpath(`//button[contains(text(), 'Create')]`))
|
||||
await createButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('should show the correct account name', async () => {
|
||||
const [accountName] = await findElements(driver, By.css('.account-name'))
|
||||
assert.equal(await accountName.getText(), '2nd account')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Switch back to original account', () => {
|
||||
it('chooses the original account from the account menu', async () => {
|
||||
await driver.findElement(By.css('.account-menu__icon')).click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [originalAccountMenuItem] = await findElements(driver, By.css('.account-menu__name'))
|
||||
await originalAccountMenuItem.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Send ETH from inside MetaMask', () => {
|
||||
it('starts to send a transaction', async function () {
|
||||
const sendButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Send')]`))
|
||||
await sendButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const inputAddress = await findElement(driver, By.css('input[placeholder="Recipient Address"]'))
|
||||
const inputAmount = await findElement(driver, By.css('.unit-input__input'))
|
||||
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
|
||||
await inputAmount.sendKeys('1')
|
||||
|
||||
// Set the gas limit
|
||||
const configureGas = await findElement(driver, By.css('.send-v2__gas-fee-display button'))
|
||||
await configureGas.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const gasModal = await driver.findElement(By.css('span .modal'))
|
||||
const save = await findElement(driver, By.xpath(`//button[contains(text(), 'Save')]`))
|
||||
await save.click()
|
||||
await driver.wait(until.stalenessOf(gasModal))
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// Continue to next screen
|
||||
const nextScreen = await findElement(driver, By.xpath(`//button[contains(text(), 'Next')]`))
|
||||
await nextScreen.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('confirms the transaction', async function () {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
assert.equal(txValues.length, 1)
|
||||
assert.equal(await txValues[0].getText(), '-1 ETH')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Imports an account with private key', () => {
|
||||
it('choose Create Account from the account menu', async () => {
|
||||
await driver.findElement(By.css('.account-menu__icon')).click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [importAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Import Account')]`))
|
||||
await importAccount.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('enter private key', async () => {
|
||||
const privateKeyInput = await findElement(driver, By.css('#private-key-box'))
|
||||
await privateKeyInput.sendKeys(testPrivateKey2)
|
||||
await delay(regularDelayMs)
|
||||
const importButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Import')]`))
|
||||
await importButtons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('should show the correct account name', async () => {
|
||||
const [accountName] = await findElements(driver, By.css('.account-name'))
|
||||
assert.equal(await accountName.getText(), 'Account 3')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('should show the imported label', async () => {
|
||||
const [importedLabel] = await findElements(driver, By.css('.wallet-view__keyring-label'))
|
||||
assert.equal(await importedLabel.getText(), 'IMPORTED')
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Connects to a Hardware wallet', () => {
|
||||
it('choose Connect Hardware Wallet from the account menu', async () => {
|
||||
await driver.findElement(By.css('.account-menu__icon')).click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [connectAccount] = await findElements(driver, By.xpath(`//div[contains(text(), 'Connect Hardware Wallet')]`))
|
||||
await connectAccount.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('should open the TREZOR Connect popup', async () => {
|
||||
const trezorButton = await findElements(driver, By.css('.hw-connect__btn'))
|
||||
await trezorButton[1].click()
|
||||
await delay(regularDelayMs)
|
||||
const connectButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
|
||||
await connectButtons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
const allWindows = await driver.getAllWindowHandles()
|
||||
switch (process.env.SELENIUM_BROWSER) {
|
||||
case 'chrome':
|
||||
assert.equal(allWindows.length, 2)
|
||||
break
|
||||
default:
|
||||
assert.equal(allWindows.length, 1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should show the "Browser not supported" screen for non Chrome browsers', async () => {
|
||||
if (process.env.SELENIUM_BROWSER !== 'chrome') {
|
||||
const title = await findElements(driver, By.xpath(`//h3[contains(text(), 'Your Browser is not supported...')]`))
|
||||
assert.equal(title.length, 1)
|
||||
|
||||
const downloadChromeButtons = await findElements(driver, By.xpath(`//button[contains(text(), 'Download Google Chrome')]`))
|
||||
assert.equal(downloadChromeButtons.length, 1)
|
||||
|
||||
await downloadChromeButtons[0].click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [newUITab, downloadChromeTab] = await driver.getAllWindowHandles()
|
||||
|
||||
await driver.switchTo().window(downloadChromeTab)
|
||||
await delay(regularDelayMs)
|
||||
const tabUrl = await driver.getCurrentUrl()
|
||||
assert.equal(tabUrl, 'https://www.google.com/chrome/')
|
||||
await driver.close()
|
||||
await delay(regularDelayMs)
|
||||
await driver.switchTo().window(newUITab)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,132 +0,0 @@
|
|||
const fs = require('fs')
|
||||
const mkdirp = require('mkdirp')
|
||||
const pify = require('pify')
|
||||
const assert = require('assert')
|
||||
const { delay } = require('../func')
|
||||
const { until } = require('selenium-webdriver')
|
||||
|
||||
module.exports = {
|
||||
assertElementNotPresent,
|
||||
checkBrowserForConsoleErrors,
|
||||
closeAllWindowHandlesExcept,
|
||||
findElement,
|
||||
findElements,
|
||||
loadExtension,
|
||||
openNewPage,
|
||||
switchToWindowWithTitle,
|
||||
verboseReportOnFailure,
|
||||
waitUntilXWindowHandles,
|
||||
}
|
||||
|
||||
async function loadExtension (driver, extensionId) {
|
||||
switch (process.env.SELENIUM_BROWSER) {
|
||||
case 'chrome': {
|
||||
await driver.get(`chrome-extension://${extensionId}/home.html`)
|
||||
break
|
||||
}
|
||||
case 'firefox': {
|
||||
await driver.get(`moz-extension://${extensionId}/home.html`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function checkBrowserForConsoleErrors (driver) {
|
||||
const ignoredLogTypes = ['WARNING']
|
||||
const ignoredErrorMessages = [
|
||||
// React throws error warnings on "dataset", but still sets the data-* properties correctly
|
||||
'Warning: Unknown prop `dataset` on ',
|
||||
// Third-party Favicon 404s show up as errors
|
||||
'favicon.ico - Failed to load resource: the server responded with a status of 404 (Not Found)',
|
||||
// React Development build - known issue blocked by test build sys
|
||||
'Warning: It looks like you\'re using a minified copy of the development build of React.',
|
||||
// Redux Development build - known issue blocked by test build sys
|
||||
'This means that you are running a slower development build of Redux.',
|
||||
]
|
||||
const browserLogs = await driver.manage().logs().get('browser')
|
||||
const errorEntries = browserLogs.filter(entry => !ignoredLogTypes.includes(entry.level.toString()))
|
||||
const errorObjects = errorEntries.map(entry => entry.toJSON())
|
||||
return errorObjects.filter(entry => !ignoredErrorMessages.some(message => entry.message.includes(message)))
|
||||
}
|
||||
|
||||
async function verboseReportOnFailure (driver, test) {
|
||||
let artifactDir
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
artifactDir = `./test-artifacts/chrome/${test.title}`
|
||||
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
|
||||
artifactDir = `./test-artifacts/firefox/${test.title}`
|
||||
}
|
||||
const filepathBase = `${artifactDir}/test-failure`
|
||||
await pify(mkdirp)(artifactDir)
|
||||
const screenshot = await driver.takeScreenshot()
|
||||
await pify(fs.writeFile)(`${filepathBase}-screenshot.png`, screenshot, { encoding: 'base64' })
|
||||
const htmlSource = await driver.getPageSource()
|
||||
await pify(fs.writeFile)(`${filepathBase}-dom.html`, htmlSource)
|
||||
}
|
||||
|
||||
async function findElement (driver, by, timeout = 10000) {
|
||||
return driver.wait(until.elementLocated(by), timeout)
|
||||
}
|
||||
|
||||
async function findElements (driver, by, timeout = 10000) {
|
||||
return driver.wait(until.elementsLocated(by), timeout)
|
||||
}
|
||||
|
||||
async function openNewPage (driver, url) {
|
||||
await driver.executeScript('window.open()')
|
||||
await delay(1000)
|
||||
|
||||
const handles = await driver.getAllWindowHandles()
|
||||
const lastHandle = handles[handles.length - 1]
|
||||
await driver.switchTo().window(lastHandle)
|
||||
|
||||
await driver.get(url)
|
||||
await delay(1000)
|
||||
}
|
||||
|
||||
async function waitUntilXWindowHandles (driver, x) {
|
||||
const windowHandles = await driver.getAllWindowHandles()
|
||||
if (windowHandles.length === x) return
|
||||
await delay(1000)
|
||||
return await waitUntilXWindowHandles(driver, x)
|
||||
}
|
||||
|
||||
async function switchToWindowWithTitle (driver, title, windowHandles) {
|
||||
if (!windowHandles) {
|
||||
windowHandles = await driver.getAllWindowHandles()
|
||||
} else if (windowHandles.length === 0) {
|
||||
throw new Error('No window with title: ' + title)
|
||||
}
|
||||
const firstHandle = windowHandles[0]
|
||||
await driver.switchTo().window(firstHandle)
|
||||
const handleTitle = await driver.getTitle()
|
||||
|
||||
if (handleTitle === title) {
|
||||
return firstHandle
|
||||
} else {
|
||||
return await switchToWindowWithTitle(driver, title, windowHandles.slice(1))
|
||||
}
|
||||
}
|
||||
|
||||
async function closeAllWindowHandlesExcept (driver, exceptions, windowHandles) {
|
||||
exceptions = typeof exceptions === 'string' ? [ exceptions ] : exceptions
|
||||
windowHandles = windowHandles || await driver.getAllWindowHandles()
|
||||
const lastWindowHandle = windowHandles.pop()
|
||||
if (!exceptions.includes(lastWindowHandle)) {
|
||||
await driver.switchTo().window(lastWindowHandle)
|
||||
await delay(1000)
|
||||
await driver.close()
|
||||
await delay(1000)
|
||||
}
|
||||
return windowHandles.length && await closeAllWindowHandlesExcept(driver, exceptions, windowHandles)
|
||||
}
|
||||
|
||||
async function assertElementNotPresent (webdriver, driver, by) {
|
||||
let dataTab
|
||||
try {
|
||||
dataTab = await findElement(driver, by, 4000)
|
||||
} catch (err) {
|
||||
assert(err instanceof webdriver.error.NoSuchElementError || err instanceof webdriver.error.TimeoutError)
|
||||
}
|
||||
assert.ok(!dataTab, 'Found element that should not be present')
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
export PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
shell-parallel -s 'npm run ganache:start -- -b 2' -x 'sleep 5 && static-server test/e2e/beta/contract-test --port 8080' -x 'sleep 5 && mocha test/e2e/beta/metamask-beta-ui.spec'
|
||||
shell-parallel -s 'npm run ganache:start -- -d -b 2' -x 'sleep 5 && mocha test/e2e/beta/from-import-beta-ui.spec'
|
|
@ -1,20 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
export PATH="$PATH:./node_modules/.bin"
|
||||
|
||||
npm run ganache:start -- -b 2 >> /dev/null 2>&1 &
|
||||
sleep 5
|
||||
cd test/e2e/beta/
|
||||
rm -rf drizzle-test
|
||||
mkdir drizzle-test && cd drizzle-test
|
||||
npm install truffle
|
||||
truffle unbox drizzle
|
||||
echo "Deploying contracts for Drizzle test..."
|
||||
truffle compile && truffle migrate
|
||||
BROWSER=none npm start >> /dev/null 2>&1 &
|
||||
cd ../../../../
|
||||
mocha test/e2e/beta/drizzle.spec
|
|
@ -271,14 +271,14 @@ module.exports = {
|
|||
iconCopy: By.className('clipboard cursor-pointer'),
|
||||
},
|
||||
settings: {
|
||||
currentNetwork: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(1) > span:nth-child(2)'),
|
||||
currentNetwork: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(1) > div.config-description'),
|
||||
fieldNewRPC: By.id('new_rpc'),
|
||||
buttonSave: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(2) > button'),
|
||||
buttonSave: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > button'),
|
||||
titleText: 'Settings',
|
||||
title: By.css('#app-content > div > div.app-primary.from-right > div > div.section-title.flex-row.flex-center > h2'),
|
||||
buttons: {
|
||||
arrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
|
||||
changePassword: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(10) > button:nth-child(5)'),
|
||||
changePassword: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(14) > button:nth-child(5)'),
|
||||
delete: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(1) > button'),
|
||||
},
|
||||
error: By.className('error'),
|
||||
|
|
|
@ -53,15 +53,19 @@ class Functions {
|
|||
return { driver, extensionId, extensionUri }
|
||||
}
|
||||
|
||||
static async buildChromeWebDriver (extPath) {
|
||||
static async buildChromeWebDriver (extPath, opts = {}) {
|
||||
const tmpProfile = fs.mkdtempSync(path.join(os.tmpdir(), 'mm-chrome-profile'))
|
||||
const args = [
|
||||
`load-extension=${extPath}`,
|
||||
`user-data-dir=${tmpProfile}`,
|
||||
]
|
||||
if (opts.responsive) {
|
||||
args.push('--auto-open-devtools-for-tabs')
|
||||
}
|
||||
return new webdriver.Builder()
|
||||
.withCapabilities({
|
||||
chromeOptions: {
|
||||
args: [
|
||||
`load-extension=${extPath}`,
|
||||
`user-data-dir=${tmpProfile}`,
|
||||
],
|
||||
args,
|
||||
binary: process.env.SELENIUM_CHROME_BINARY,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,146 +1,146 @@
|
|||
// const path = require('path')
|
||||
// const Func = require('./func').Functions
|
||||
// const account1 = '0x2E428ABd9313D256d64D1f69fe3929C3BE18fD1f'
|
||||
// const account1RSK = '0x2E428aBd9313D256d64D1f69fe3929c3Be18Fd1F'
|
||||
// const account2 = '0xd7b7AFeCa35e32594e29504771aC847E2a803742'
|
||||
// const testsFolder = './test-cases'
|
||||
// const setup = require(`${testsFolder}/setup.spec`)
|
||||
// const login = require(`${testsFolder}/login.spec`)
|
||||
// const { accountCreation, getCreatedAccounts } = require(`${testsFolder}/account-creation.spec`)
|
||||
// const connectHDWallet = require(`${testsFolder}/connect-hd-wallet.spec`)
|
||||
// const importAccount = require(`${testsFolder}/import-account.spec`)
|
||||
// const importContractAccount = require(`${testsFolder}/import-contract-account.spec`)
|
||||
// const deleteImportedAccount = require(`${testsFolder}/delete-imported-account.spec`)
|
||||
// const signData = require(`${testsFolder}/sign-data.spec`)
|
||||
// const exportPrivateKey = require(`${testsFolder}/export-private-key.spec`)
|
||||
// const importGanacheSeedPhrase = require(`${testsFolder}/import-ganache-seed-phrase.spec`)
|
||||
// const RSKNetworkTests = require(`${testsFolder}/RSK-network-tests.js`)
|
||||
// const checkEmittedEvents = require(`${testsFolder}/check-emitted-events.spec`)
|
||||
// // const addCustomToken = require(`${testsFolder}/add-token-custom.spec`)
|
||||
// const changePassword = require(`${testsFolder}/change-password.spec`)
|
||||
// const addTokeFromSearch = require(`${testsFolder}/add-token-search.spec`)
|
||||
// const customRPC = require(`${testsFolder}/custom-rpc.spec`)
|
||||
const path = require('path')
|
||||
const Func = require('./func').Functions
|
||||
const account1 = '0x2E428ABd9313D256d64D1f69fe3929C3BE18fD1f'
|
||||
const account1RSK = '0x2E428aBd9313D256d64D1f69fe3929c3Be18Fd1F'
|
||||
const account2 = '0xd7b7AFeCa35e32594e29504771aC847E2a803742'
|
||||
const testsFolder = './test-cases'
|
||||
const setup = require(`${testsFolder}/setup.spec`)
|
||||
const login = require(`${testsFolder}/login.spec`)
|
||||
const { accountCreation, getCreatedAccounts } = require(`${testsFolder}/account-creation.spec`)
|
||||
const connectHDWallet = require(`${testsFolder}/connect-hd-wallet.spec`)
|
||||
const importAccount = require(`${testsFolder}/import-account.spec`)
|
||||
const importContractAccount = require(`${testsFolder}/import-contract-account.spec`)
|
||||
const deleteImportedAccount = require(`${testsFolder}/delete-imported-account.spec`)
|
||||
const signData = require(`${testsFolder}/sign-data.spec`)
|
||||
const exportPrivateKey = require(`${testsFolder}/export-private-key.spec`)
|
||||
const importGanacheSeedPhrase = require(`${testsFolder}/import-ganache-seed-phrase.spec`)
|
||||
const RSKNetworkTests = require(`${testsFolder}/RSK-network-tests.js`)
|
||||
const checkEmittedEvents = require(`${testsFolder}/check-emitted-events.spec`)
|
||||
// const addCustomToken = require(`${testsFolder}/add-token-custom.spec`)
|
||||
const changePassword = require(`${testsFolder}/change-password.spec`)
|
||||
const addTokeFromSearch = require(`${testsFolder}/add-token-search.spec`)
|
||||
const customRPC = require(`${testsFolder}/custom-rpc.spec`)
|
||||
|
||||
// describe('Metamask popup page', async function () {
|
||||
describe('Metamask popup page', async function () {
|
||||
|
||||
// this.timeout(15 * 60 * 1000)
|
||||
// const f = new Func()
|
||||
// let driver, extensionId
|
||||
// const password = '123456789'
|
||||
// const newPassword = {
|
||||
// correct: 'abcDEF123!@#',
|
||||
// short: '123',
|
||||
// incorrect: '1234567890',
|
||||
// }
|
||||
this.timeout(15 * 60 * 1000)
|
||||
const f = new Func()
|
||||
let driver, extensionId
|
||||
const password = '123456789'
|
||||
const newPassword = {
|
||||
correct: 'abcDEF123!@#',
|
||||
short: '123',
|
||||
incorrect: '1234567890',
|
||||
}
|
||||
|
||||
// before(async function () {
|
||||
// if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
// const extPath = path.resolve('dist/chrome')
|
||||
// driver = await Func.buildChromeWebDriver(extPath)
|
||||
// f.driver = driver
|
||||
// extensionId = await f.getExtensionIdChrome()
|
||||
// f.extensionId = extensionId
|
||||
// await driver.get(`chrome-extension://${extensionId}/popup.html`)
|
||||
before(async function () {
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
const extPath = path.resolve('dist/chrome')
|
||||
driver = await Func.buildChromeWebDriver(extPath)
|
||||
f.driver = driver
|
||||
extensionId = await f.getExtensionIdChrome()
|
||||
f.extensionId = extensionId
|
||||
await driver.get(`chrome-extension://${extensionId}/popup.html`)
|
||||
|
||||
// } else if (process.env.SELENIUM_BROWSER === 'firefox') {
|
||||
// const extPath = path.resolve('dist/firefox')
|
||||
// driver = await Func.buildFirefoxWebdriver()
|
||||
// f.driver = driver
|
||||
// await f.installWebExt(extPath)
|
||||
// await f.delay(700)
|
||||
// extensionId = await f.getExtensionIdFirefox()
|
||||
// f.extensionId = extensionId
|
||||
// await driver.get(`moz-extension://${extensionId}/popup.html`)
|
||||
// }
|
||||
} else if (process.env.SELENIUM_BROWSER === 'firefox') {
|
||||
const extPath = path.resolve('dist/firefox')
|
||||
driver = await Func.buildFirefoxWebdriver()
|
||||
f.driver = driver
|
||||
await f.installWebExt(extPath)
|
||||
await f.delay(700)
|
||||
extensionId = await f.getExtensionIdFirefox()
|
||||
f.extensionId = extensionId
|
||||
await driver.get(`moz-extension://${extensionId}/popup.html`)
|
||||
}
|
||||
|
||||
// })
|
||||
})
|
||||
|
||||
// afterEach(async function () {
|
||||
// // logs command not supported in firefox
|
||||
// // https://github.com/SeleniumHQ/selenium/issues/2910
|
||||
// if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
// // check for console errors
|
||||
// const errors = await f.checkBrowserForConsoleErrors(driver)
|
||||
// if (errors.length) {
|
||||
// const errorReports = errors.map(err => err.message)
|
||||
// const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
|
||||
// console.log(errorMessage)
|
||||
// }
|
||||
// }
|
||||
// // gather extra data if test failed
|
||||
// if (this.currentTest.state === 'failed') {
|
||||
// await f.verboseReportOnFailure(this.currentTest)
|
||||
// }
|
||||
// })
|
||||
afterEach(async function () {
|
||||
// logs command not supported in firefox
|
||||
// https://github.com/SeleniumHQ/selenium/issues/2910
|
||||
if (process.env.SELENIUM_BROWSER === 'chrome') {
|
||||
// check for console errors
|
||||
const errors = await f.checkBrowserForConsoleErrors(driver)
|
||||
if (errors.length) {
|
||||
const errorReports = errors.map(err => err.message)
|
||||
const errorMessage = `Errors found in browser console:\n${errorReports.join('\n')}`
|
||||
console.log(errorMessage)
|
||||
}
|
||||
}
|
||||
// gather extra data if test failed
|
||||
if (this.currentTest.state === 'failed') {
|
||||
await f.verboseReportOnFailure(this.currentTest)
|
||||
}
|
||||
})
|
||||
|
||||
// after(async function () {
|
||||
// await driver.quit()
|
||||
// })
|
||||
after(async function () {
|
||||
await driver.quit()
|
||||
})
|
||||
|
||||
// describe('Setup', async () => {
|
||||
// await setup(f)
|
||||
// })
|
||||
describe('Setup', async () => {
|
||||
await setup(f)
|
||||
})
|
||||
|
||||
// describe('Log In', async () => {
|
||||
// await login(f, password)
|
||||
// })
|
||||
describe('Log In', async () => {
|
||||
await login(f, password)
|
||||
})
|
||||
|
||||
// describe('Account Creation', async () => {
|
||||
// await accountCreation(f, password)
|
||||
// })
|
||||
describe('Account Creation', async () => {
|
||||
await accountCreation(f, password)
|
||||
})
|
||||
|
||||
// describe('Connect Hardware Wallet', async () => {
|
||||
// await connectHDWallet(f)
|
||||
// })
|
||||
describe('Connect Hardware Wallet', async () => {
|
||||
await connectHDWallet(f)
|
||||
})
|
||||
|
||||
// describe('Import Account', async () => {
|
||||
// await importAccount(f)
|
||||
// })
|
||||
describe('Import Account', async () => {
|
||||
await importAccount(f)
|
||||
})
|
||||
|
||||
// describe('Import Contract account', async () => {
|
||||
// await importContractAccount(f, account1, getCreatedAccounts)
|
||||
// })
|
||||
describe('Import Contract account', async () => {
|
||||
await importContractAccount(f, account1, getCreatedAccounts)
|
||||
})
|
||||
|
||||
// describe('Delete Imported Account', async () => {
|
||||
// await deleteImportedAccount(f)
|
||||
// })
|
||||
describe('Delete Imported Account', async () => {
|
||||
await deleteImportedAccount(f)
|
||||
})
|
||||
|
||||
// describe('Sign Data', async () => {
|
||||
// await signData(f)
|
||||
// })
|
||||
describe('Sign Data', async () => {
|
||||
await signData(f)
|
||||
})
|
||||
|
||||
// describe('Export private key', async () => {
|
||||
// await exportPrivateKey(f, password)
|
||||
// })
|
||||
describe('Export private key', async () => {
|
||||
await exportPrivateKey(f, password)
|
||||
})
|
||||
|
||||
// describe('Import Ganache seed phrase', async () => {
|
||||
// await importGanacheSeedPhrase(f, account2, password)
|
||||
// })
|
||||
describe('Import Ganache seed phrase', async () => {
|
||||
await importGanacheSeedPhrase(f, account2, password)
|
||||
})
|
||||
|
||||
// describe('RSK network tests', async () => {
|
||||
// await RSKNetworkTests(f, account1RSK)
|
||||
// })
|
||||
describe('RSK network tests', async () => {
|
||||
await RSKNetworkTests(f, account1RSK)
|
||||
})
|
||||
|
||||
// describe('Check the filter of emitted events', async () => {
|
||||
// await checkEmittedEvents(f, account1, account2)
|
||||
// })
|
||||
describe('Check the filter of emitted events', async () => {
|
||||
await checkEmittedEvents(f, account1, account2)
|
||||
})
|
||||
|
||||
// // todo: it works locally, but doesn't work in CI
|
||||
// // describe('Add Token: Custom', async () => {
|
||||
// // await addCustomToken(f, account1, account2)
|
||||
// // })
|
||||
// todo: it works locally, but doesn't work in CI
|
||||
// describe('Add Token: Custom', async () => {
|
||||
// await addCustomToken(f, account1, account2)
|
||||
// })
|
||||
|
||||
// describe('Change password', async () => {
|
||||
// await changePassword(f, password, newPassword)
|
||||
// })
|
||||
describe('Change password', async () => {
|
||||
await changePassword(f, password, newPassword)
|
||||
})
|
||||
|
||||
// describe('Add Token:Search', async () => {
|
||||
// await addTokeFromSearch(f)
|
||||
// })
|
||||
describe('Add Token:Search', async () => {
|
||||
await addTokeFromSearch(f)
|
||||
})
|
||||
|
||||
// describe('Custom RPC', async () => {
|
||||
// await customRPC(f)
|
||||
// })
|
||||
// })
|
||||
describe('Custom RPC', async () => {
|
||||
await customRPC(f)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ const { screens, elements, NETWORKS } = require('../elements')
|
|||
const [tab0, tab1] = await f.driver.getAllWindowHandles()
|
||||
await f.driver.switchTo().window(tab1)
|
||||
const faucetLink = await f.driver.getCurrentUrl()
|
||||
assert.equal(faucetLink, 'https://faucet.testnet.rsk.co/', 'Incorrect faucet link for RSK network')
|
||||
assert.equal(faucetLink, 'https://faucet.rsk.co/', 'Incorrect faucet link for RSK network')
|
||||
await f.driver.close()
|
||||
await f.driver.switchTo().window(tab0)
|
||||
const arrow = await f.waitUntilShowUp(elements.buttonArrow)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const assert = require('assert')
|
||||
const currencyFormatter = require('currency-formatter')
|
||||
const infuraConversion = require('../../ui/app/infura-conversion.json')
|
||||
const infuraConversion = require('../../old-ui/app/infura-conversion.json')
|
||||
|
||||
describe('currencyFormatting', function () {
|
||||
it('be able to format any infura currency', function (done) {
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const PropTypes = require('prop-types')
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../actions')
|
||||
const { getCurrentViewContext } = require('../../selectors')
|
||||
const classnames = require('classnames')
|
||||
|
||||
const NewAccountCreateForm = require('./create-form')
|
||||
const NewAccountImportForm = require('../import')
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
displayedForm: getCurrentViewContext(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
displayForm: form => dispatch(actions.setNewAccountForm(form)),
|
||||
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
|
||||
showExportPrivateKeyModal: () => {
|
||||
dispatch(actions.showModal({ name: 'EXPORT_PRIVATE_KEY' }))
|
||||
},
|
||||
hideModal: () => dispatch(actions.hideModal()),
|
||||
setAccountLabel: (address, label) => dispatch(actions.setAccountLabel(address, label)),
|
||||
}
|
||||
}
|
||||
|
||||
inherits(AccountDetailsModal, Component)
|
||||
function AccountDetailsModal (props) {
|
||||
Component.call(this)
|
||||
|
||||
this.state = {
|
||||
displayedForm: props.displayedForm,
|
||||
}
|
||||
}
|
||||
|
||||
AccountDetailsModal.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(AccountDetailsModal)
|
||||
|
||||
|
||||
AccountDetailsModal.prototype.render = function () {
|
||||
const { displayedForm, displayForm } = this.props
|
||||
|
||||
return h('div.new-account', {}, [
|
||||
|
||||
h('div.new-account__header', [
|
||||
|
||||
h('div.new-account__title', this.context.t('newAccount')),
|
||||
|
||||
h('div.new-account__tabs', [
|
||||
|
||||
h('div.new-account__tabs__tab', {
|
||||
className: classnames('new-account__tabs__tab', {
|
||||
'new-account__tabs__selected': displayedForm === 'CREATE',
|
||||
'new-account__tabs__unselected cursor-pointer': displayedForm !== 'CREATE',
|
||||
}),
|
||||
onClick: () => displayForm('CREATE'),
|
||||
}, this.context.t('createDen')),
|
||||
|
||||
h('div.new-account__tabs__tab', {
|
||||
className: classnames('new-account__tabs__tab', {
|
||||
'new-account__tabs__selected': displayedForm === 'IMPORT',
|
||||
'new-account__tabs__unselected cursor-pointer': displayedForm !== 'IMPORT',
|
||||
}),
|
||||
onClick: () => displayForm('IMPORT'),
|
||||
}, this.context.t('import')),
|
||||
|
||||
]),
|
||||
|
||||
]),
|
||||
|
||||
h('div.new-account__form', [
|
||||
|
||||
displayedForm === 'CREATE'
|
||||
? h(NewAccountCreateForm)
|
||||
: h(NewAccountImportForm),
|
||||
|
||||
]),
|
||||
|
||||
])
|
||||
}
|
365
ui/app/app.js
365
ui/app/app.js
|
@ -1,365 +0,0 @@
|
|||
const { Component } = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const connect = require('react-redux').connect
|
||||
const { Route, Switch, withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('./actions')
|
||||
const classnames = require('classnames')
|
||||
const log = require('loglevel')
|
||||
const { getMetaMaskAccounts } = require('./selectors')
|
||||
|
||||
// init
|
||||
const InitializeScreen = require('../../mascara/src/app/first-time').default
|
||||
// accounts
|
||||
const SendTransactionScreen = require('./components/send/send.container')
|
||||
const ConfirmTransaction = require('./components/pages/confirm-transaction')
|
||||
|
||||
// slideout menu
|
||||
const Sidebar = require('./components/sidebars').default
|
||||
|
||||
// other views
|
||||
import Home from './components/pages/home'
|
||||
import Settings from './components/pages/settings'
|
||||
const Authenticated = require('./components/pages/authenticated')
|
||||
const Initialized = require('./components/pages/initialized')
|
||||
const RestoreVaultPage = require('./components/pages/keychains/restore-vault').default
|
||||
const RevealSeedConfirmation = require('./components/pages/keychains/reveal-seed')
|
||||
const AddTokenPage = require('./components/pages/add-token')
|
||||
const ConfirmAddTokenPage = require('./components/pages/confirm-add-token')
|
||||
const ConfirmAddSuggestedTokenPage = require('./components/pages/confirm-add-suggested-token')
|
||||
const CreateAccountPage = require('./components/pages/create-account')
|
||||
const NoticeScreen = require('./components/pages/notice')
|
||||
|
||||
const Loading = require('./components/loading-screen')
|
||||
const NetworkDropdown = require('./components/dropdowns/network-dropdown')
|
||||
const AccountMenu = require('./components/account-menu')
|
||||
|
||||
// Global Modals
|
||||
const Modal = require('./components/modals/index').Modal
|
||||
// Global Alert
|
||||
const Alert = require('./components/alert')
|
||||
|
||||
import AppHeader from './components/app-header'
|
||||
import UnlockPage from './components/pages/unlock-page'
|
||||
|
||||
// Routes
|
||||
const {
|
||||
DEFAULT_ROUTE,
|
||||
UNLOCK_ROUTE,
|
||||
SETTINGS_ROUTE,
|
||||
REVEAL_SEED_ROUTE,
|
||||
RESTORE_VAULT_ROUTE,
|
||||
ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_TOKEN_ROUTE,
|
||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
SEND_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
INITIALIZE_ROUTE,
|
||||
NOTICE_ROUTE,
|
||||
} = require('./routes')
|
||||
|
||||
class App extends Component {
|
||||
componentWillMount () {
|
||||
const { currentCurrency, setCurrentCurrencyToUSD } = this.props
|
||||
|
||||
if (!currentCurrency) {
|
||||
setCurrentCurrencyToUSD()
|
||||
}
|
||||
}
|
||||
|
||||
renderRoutes () {
|
||||
const exact = true
|
||||
|
||||
return (
|
||||
h(Switch, [
|
||||
h(Route, { path: INITIALIZE_ROUTE, component: InitializeScreen }),
|
||||
h(Initialized, { path: UNLOCK_ROUTE, exact, component: UnlockPage }),
|
||||
h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }),
|
||||
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
|
||||
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
|
||||
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
|
||||
h(Authenticated, {
|
||||
path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`,
|
||||
component: ConfirmTransaction,
|
||||
}),
|
||||
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
|
||||
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),
|
||||
h(Authenticated, { path: CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE, exact, component: ConfirmAddSuggestedTokenPage }),
|
||||
h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }),
|
||||
h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
isLoading,
|
||||
alertMessage,
|
||||
loadingMessage,
|
||||
network,
|
||||
isMouseUser,
|
||||
provider,
|
||||
frequentRpcList,
|
||||
currentView,
|
||||
setMouseUserState,
|
||||
sidebar,
|
||||
} = this.props
|
||||
const isLoadingNetwork = network === 'loading' && currentView.name !== 'config'
|
||||
const loadMessage = loadingMessage || isLoadingNetwork ?
|
||||
this.getConnectingLabel(loadingMessage) : null
|
||||
log.debug('Main ui render function')
|
||||
|
||||
return (
|
||||
h('.flex-column.full-height', {
|
||||
className: classnames({ 'mouse-user-styles': isMouseUser }),
|
||||
style: {
|
||||
overflowX: 'hidden',
|
||||
position: 'relative',
|
||||
alignItems: 'center',
|
||||
},
|
||||
tabIndex: '0',
|
||||
onClick: () => setMouseUserState(true),
|
||||
onKeyDown: (e) => {
|
||||
if (e.keyCode === 9) {
|
||||
setMouseUserState(false)
|
||||
}
|
||||
},
|
||||
}, [
|
||||
|
||||
// global modal
|
||||
h(Modal, {}, []),
|
||||
|
||||
// global alert
|
||||
h(Alert, {visible: this.props.alertOpen, msg: alertMessage}),
|
||||
|
||||
h(AppHeader),
|
||||
|
||||
// sidebar
|
||||
h(Sidebar, {
|
||||
sidebarOpen: sidebar.isOpen,
|
||||
hideSidebar: this.props.hideSidebar,
|
||||
transitionName: sidebar.transitionName,
|
||||
type: sidebar.type,
|
||||
}),
|
||||
|
||||
// network dropdown
|
||||
h(NetworkDropdown, {
|
||||
provider,
|
||||
frequentRpcList,
|
||||
}, []),
|
||||
|
||||
h(AccountMenu),
|
||||
|
||||
h('div.main-container-wrapper', [
|
||||
(isLoading || isLoadingNetwork) && h(Loading, {
|
||||
loadingMessage: loadMessage,
|
||||
}),
|
||||
|
||||
// content
|
||||
this.renderRoutes(),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
toggleMetamaskActive () {
|
||||
if (!this.props.isUnlocked) {
|
||||
// currently inactive: redirect to password box
|
||||
var passwordBox = document.querySelector('input[type=password]')
|
||||
if (!passwordBox) return
|
||||
passwordBox.focus()
|
||||
} else {
|
||||
// currently active: deactivate
|
||||
this.props.dispatch(actions.lockMetamask(false))
|
||||
}
|
||||
}
|
||||
|
||||
getConnectingLabel = function (loadingMessage) {
|
||||
if (loadingMessage) {
|
||||
return loadingMessage
|
||||
}
|
||||
const { provider } = this.props
|
||||
const providerName = provider.type
|
||||
|
||||
let name
|
||||
|
||||
if (providerName === 'mainnet') {
|
||||
name = this.context.t('connectingToMainnet')
|
||||
} else if (providerName === 'ropsten') {
|
||||
name = this.context.t('connectingToRopsten')
|
||||
} else if (providerName === 'kovan') {
|
||||
name = this.context.t('connectingToKovan')
|
||||
} else if (providerName === 'rinkeby') {
|
||||
name = this.context.t('connectingToRinkeby')
|
||||
} else if (providerName === 'poa') {
|
||||
name = this.context.t('connectingToPOA')
|
||||
} else {
|
||||
name = this.context.t('connectingToUnknown')
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
getNetworkName () {
|
||||
const { provider } = this.props
|
||||
const providerName = provider.type
|
||||
|
||||
let name
|
||||
|
||||
if (providerName === 'mainnet') {
|
||||
name = this.context.t('mainnet')
|
||||
} else if (providerName === 'ropsten') {
|
||||
name = this.context.t('ropsten')
|
||||
} else if (providerName === 'kovan') {
|
||||
name = this.context.t('kovan')
|
||||
} else if (providerName === 'rinkeby') {
|
||||
name = this.context.t('rinkeby')
|
||||
} else if (providerName === 'poa') {
|
||||
name = this.context.t('poa')
|
||||
} else {
|
||||
name = this.context.t('unknownNetwork')
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
App.propTypes = {
|
||||
currentCurrency: PropTypes.string,
|
||||
setCurrentCurrencyToUSD: PropTypes.func,
|
||||
isLoading: PropTypes.bool,
|
||||
loadingMessage: PropTypes.string,
|
||||
alertMessage: PropTypes.string,
|
||||
network: PropTypes.string,
|
||||
provider: PropTypes.object,
|
||||
frequentRpcList: PropTypes.array,
|
||||
currentView: PropTypes.object,
|
||||
sidebar: PropTypes.object,
|
||||
alertOpen: PropTypes.bool,
|
||||
hideSidebar: PropTypes.func,
|
||||
isMascara: PropTypes.bool,
|
||||
isOnboarding: PropTypes.bool,
|
||||
isUnlocked: PropTypes.bool,
|
||||
networkDropdownOpen: PropTypes.bool,
|
||||
showNetworkDropdown: PropTypes.func,
|
||||
hideNetworkDropdown: PropTypes.func,
|
||||
history: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
dispatch: PropTypes.func,
|
||||
toggleAccountMenu: PropTypes.func,
|
||||
selectedAddress: PropTypes.string,
|
||||
noActiveNotices: PropTypes.bool,
|
||||
lostAccounts: PropTypes.array,
|
||||
isInitialized: PropTypes.bool,
|
||||
forgottenPassword: PropTypes.bool,
|
||||
activeAddress: PropTypes.string,
|
||||
unapprovedTxs: PropTypes.object,
|
||||
seedWords: PropTypes.string,
|
||||
unapprovedMsgCount: PropTypes.number,
|
||||
unapprovedPersonalMsgCount: PropTypes.number,
|
||||
unapprovedTypedMessagesCount: PropTypes.number,
|
||||
welcomeScreenSeen: PropTypes.bool,
|
||||
isPopup: PropTypes.bool,
|
||||
betaUI: PropTypes.bool,
|
||||
isMouseUser: PropTypes.bool,
|
||||
setMouseUserState: PropTypes.func,
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { appState, metamask } = state
|
||||
const {
|
||||
networkDropdownOpen,
|
||||
sidebar,
|
||||
alertOpen,
|
||||
alertMessage,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
} = appState
|
||||
|
||||
const accounts = getMetaMaskAccounts(state)
|
||||
|
||||
const {
|
||||
identities,
|
||||
address,
|
||||
keyrings,
|
||||
isInitialized,
|
||||
noActiveNotices,
|
||||
seedWords,
|
||||
unapprovedTxs,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
unapprovedTypedMessagesCount,
|
||||
} = metamask
|
||||
const selected = address || Object.keys(accounts)[0]
|
||||
|
||||
return {
|
||||
// state from plugin
|
||||
networkDropdownOpen,
|
||||
sidebar,
|
||||
alertOpen,
|
||||
alertMessage,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
noActiveNotices,
|
||||
isInitialized,
|
||||
isUnlocked: state.metamask.isUnlocked,
|
||||
selectedAddress: state.metamask.selectedAddress,
|
||||
currentView: state.appState.currentView,
|
||||
activeAddress: state.appState.activeAddress,
|
||||
transForward: state.appState.transForward,
|
||||
isMascara: state.metamask.isMascara,
|
||||
isOnboarding: Boolean(!noActiveNotices || seedWords || !isInitialized),
|
||||
isPopup: state.metamask.isPopup,
|
||||
seedWords: state.metamask.seedWords,
|
||||
unapprovedTxs,
|
||||
unapprovedMsgs: state.metamask.unapprovedMsgs,
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
unapprovedTypedMessagesCount,
|
||||
menuOpen: state.appState.menuOpen,
|
||||
network: state.metamask.network,
|
||||
provider: state.metamask.provider,
|
||||
forgottenPassword: state.appState.forgottenPassword,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
frequentRpcList: state.metamask.frequentRpcList || [],
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
isMouseUser: state.appState.isMouseUser,
|
||||
betaUI: state.metamask.featureFlags.betaUI,
|
||||
isRevealingSeedWords: state.metamask.isRevealingSeedWords,
|
||||
Qr: state.appState.Qr,
|
||||
welcomeScreenSeen: state.metamask.welcomeScreenSeen,
|
||||
|
||||
// state needed to get account dropdown temporarily rendering from app bar
|
||||
identities,
|
||||
selected,
|
||||
keyrings,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch, ownProps) {
|
||||
return {
|
||||
dispatch,
|
||||
hideSidebar: () => dispatch(actions.hideSidebar()),
|
||||
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
|
||||
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
|
||||
setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('usd')),
|
||||
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
|
||||
setMouseUserState: (isMouseUser) => dispatch(actions.setMouseUserState(isMouseUser)),
|
||||
}
|
||||
}
|
||||
|
||||
App.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(App)
|
|
@ -1,324 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../actions')
|
||||
const ethNetProps = require('eth-net-props')
|
||||
const connect = require('react-redux').connect
|
||||
const Dropdown = require('./dropdown').Dropdown
|
||||
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
|
||||
const Identicon = require('./identicon')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const { checksumAddress } = require('../util')
|
||||
|
||||
class AccountDropdowns extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
accountSelectorActive: false,
|
||||
optionsMenuActive: false,
|
||||
}
|
||||
this.accountSelectorToggleClassName = 'accounts-selector'
|
||||
this.optionsMenuToggleClassName = 'fa-ellipsis-h'
|
||||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { identities, selected, keyrings } = this.props
|
||||
|
||||
return Object.keys(identities).map((key, index) => {
|
||||
const identity = identities[key]
|
||||
const isSelected = identity.address === selected
|
||||
|
||||
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||
|
||||
const keyring = keyrings.find((kr) => {
|
||||
return kr.accounts.includes(simpleAddress) ||
|
||||
kr.accounts.includes(identity.address)
|
||||
})
|
||||
|
||||
return h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => {
|
||||
this.props.actions.showAccountDetail(identity.address)
|
||||
},
|
||||
style: {
|
||||
marginTop: index === 0 ? '5px' : '',
|
||||
fontSize: '24px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
Identicon,
|
||||
{
|
||||
address: identity.address,
|
||||
diameter: 32,
|
||||
style: {
|
||||
marginLeft: '10px',
|
||||
},
|
||||
},
|
||||
),
|
||||
this.indicateIfLoose(keyring),
|
||||
h('span', {
|
||||
style: {
|
||||
marginLeft: '20px',
|
||||
fontSize: '24px',
|
||||
maxWidth: '145px',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
}, identity.name || ''),
|
||||
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, isSelected ? h('.check', '✓') : null),
|
||||
]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
indicateIfLoose (keyring) {
|
||||
try { // Sometimes keyrings aren't loaded yet:
|
||||
const type = keyring.type
|
||||
const isLoose = type !== 'HD Key Tree'
|
||||
return isLoose ? h('.keyring-label.allcaps', this.context.t('loose')) : null
|
||||
} catch (e) { return }
|
||||
}
|
||||
|
||||
renderAccountSelector () {
|
||||
const { actions } = this.props
|
||||
const { accountSelectorActive } = this.state
|
||||
|
||||
return h(
|
||||
Dropdown,
|
||||
{
|
||||
useCssTransition: true, // Hardcoded because account selector is temporarily in app-header
|
||||
style: {
|
||||
marginLeft: '-238px',
|
||||
marginTop: '38px',
|
||||
minWidth: '180px',
|
||||
overflowY: 'auto',
|
||||
maxHeight: '300px',
|
||||
width: '300px',
|
||||
},
|
||||
innerStyle: {
|
||||
padding: '8px 25px',
|
||||
},
|
||||
isOpen: accountSelectorActive,
|
||||
onClickOutside: (event) => {
|
||||
const { classList } = event.target
|
||||
const isNotToggleElement = !classList.contains(this.accountSelectorToggleClassName)
|
||||
if (accountSelectorActive && isNotToggleElement) {
|
||||
this.setState({ accountSelectorActive: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
[
|
||||
...this.renderAccounts(),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => actions.addNewAccount(),
|
||||
},
|
||||
[
|
||||
h(
|
||||
Identicon,
|
||||
{
|
||||
style: {
|
||||
marginLeft: '10px',
|
||||
},
|
||||
diameter: 32,
|
||||
},
|
||||
),
|
||||
h('span', { style: { marginLeft: '20px', fontSize: '24px' } }, this.context.t('createAccount')),
|
||||
],
|
||||
),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => actions.showImportPage(),
|
||||
},
|
||||
[
|
||||
h(
|
||||
Identicon,
|
||||
{
|
||||
style: {
|
||||
marginLeft: '10px',
|
||||
},
|
||||
diameter: 32,
|
||||
},
|
||||
),
|
||||
h('span', {
|
||||
style: {
|
||||
marginLeft: '20px',
|
||||
fontSize: '24px',
|
||||
marginBottom: '5px',
|
||||
},
|
||||
}, this.context.t('importAccount')),
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
renderAccountOptions () {
|
||||
const { actions } = this.props
|
||||
const { optionsMenuActive } = this.state
|
||||
|
||||
return h(
|
||||
Dropdown,
|
||||
{
|
||||
style: {
|
||||
marginLeft: '-215px',
|
||||
minWidth: '180px',
|
||||
},
|
||||
isOpen: optionsMenuActive,
|
||||
onClickOutside: (event) => {
|
||||
const { classList } = event.target
|
||||
const isNotToggleElement = !classList.contains(this.optionsMenuToggleClassName)
|
||||
if (optionsMenuActive && isNotToggleElement) {
|
||||
this.setState({ optionsMenuActive: false })
|
||||
}
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => {
|
||||
const { selected, network } = this.props
|
||||
const url = ethNetProps.explorerLinks.getExplorerAccountLinkFor(selected, network)
|
||||
global.platform.openWindow({ url })
|
||||
},
|
||||
},
|
||||
this.context.t('etherscanView'),
|
||||
),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => {
|
||||
const { selected, identities } = this.props
|
||||
var identity = identities[selected]
|
||||
actions.showQrView(selected, identity ? identity.name : '')
|
||||
},
|
||||
},
|
||||
this.context.t('showQRCode'),
|
||||
),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => {
|
||||
const { selected } = this.props
|
||||
copyToClipboard(checksumAddress(selected))
|
||||
},
|
||||
},
|
||||
this.context.t('copyAddress'),
|
||||
),
|
||||
h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
onClick: () => {
|
||||
actions.requestAccountExport()
|
||||
},
|
||||
},
|
||||
this.context.t('exportPrivateKey'),
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { style, enableAccountsSelector, enableAccountOptions } = this.props
|
||||
const { optionsMenuActive, accountSelectorActive } = this.state
|
||||
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: style,
|
||||
},
|
||||
[
|
||||
enableAccountsSelector && h(
|
||||
// 'i.fa.fa-angle-down',
|
||||
'div.cursor-pointer.color-orange.accounts-selector',
|
||||
{
|
||||
style: {
|
||||
// fontSize: '1.8em',
|
||||
background: 'url(images/switch_acc.svg) white center center no-repeat',
|
||||
height: '25px',
|
||||
width: '25px',
|
||||
transform: 'scale(0.75)',
|
||||
marginRight: '3px',
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.stopPropagation()
|
||||
this.setState({
|
||||
accountSelectorActive: !accountSelectorActive,
|
||||
optionsMenuActive: false,
|
||||
})
|
||||
},
|
||||
},
|
||||
this.renderAccountSelector(),
|
||||
),
|
||||
enableAccountOptions && h(
|
||||
'i.fa.fa-ellipsis-h',
|
||||
{
|
||||
style: {
|
||||
marginRight: '0.5em',
|
||||
fontSize: '1.8em',
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.stopPropagation()
|
||||
this.setState({
|
||||
accountSelectorActive: false,
|
||||
optionsMenuActive: !optionsMenuActive,
|
||||
})
|
||||
},
|
||||
},
|
||||
this.renderAccountOptions()
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AccountDropdowns.defaultProps = {
|
||||
enableAccountsSelector: false,
|
||||
enableAccountOptions: false,
|
||||
}
|
||||
|
||||
AccountDropdowns.propTypes = {
|
||||
identities: PropTypes.objectOf(PropTypes.object),
|
||||
selected: PropTypes.string,
|
||||
keyrings: PropTypes.array,
|
||||
actions: PropTypes.objectOf(PropTypes.func),
|
||||
network: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
enableAccountOptions: PropTypes.bool,
|
||||
enableAccountsSelector: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
actions: {
|
||||
showConfigPage: () => dispatch(actions.showConfigPage()),
|
||||
requestAccountExport: () => dispatch(actions.requestExportAccount()),
|
||||
showAccountDetail: (address) => dispatch(actions.showAccountDetail(address)),
|
||||
addNewAccount: () => dispatch(actions.addNewAccount()),
|
||||
showImportPage: () => dispatch(actions.showImportPage()),
|
||||
showQrView: (selected, identity) => dispatch(actions.showQrView(selected, identity)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
AccountDropdowns.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AccountDropdowns: connect(null, mapDispatchToProps)(AccountDropdowns),
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const PropTypes = require('prop-types')
|
||||
const inherits = require('util').inherits
|
||||
const exportAsFile = require('../util').exportAsFile
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const actions = require('../actions')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
ExportAccountView.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(ExportAccountView)
|
||||
|
||||
|
||||
inherits(ExportAccountView, Component)
|
||||
function ExportAccountView () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
warning: state.appState.warning,
|
||||
}
|
||||
}
|
||||
|
||||
ExportAccountView.prototype.render = function () {
|
||||
const state = this.props
|
||||
const accountDetail = state.accountDetail
|
||||
const nickname = state.identities[state.address].name
|
||||
|
||||
if (!accountDetail) return h('div')
|
||||
const accountExport = accountDetail.accountExport
|
||||
|
||||
const notExporting = accountExport === 'none'
|
||||
const exportRequested = accountExport === 'requested'
|
||||
const accountExported = accountExport === 'completed'
|
||||
|
||||
if (notExporting) return h('div')
|
||||
|
||||
if (exportRequested) {
|
||||
const warning = this.context.t('exportPrivateKeyWarning')
|
||||
return (
|
||||
h('div', {
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('div', {
|
||||
key: 'exporting',
|
||||
style: {
|
||||
margin: '0 20px',
|
||||
},
|
||||
}, [
|
||||
h('p.error', warning),
|
||||
h('input#exportAccount.sizing-input', {
|
||||
type: 'password',
|
||||
placeholder: this.context.t('confirmPassword').toLowerCase(),
|
||||
onKeyPress: this.onExportKeyPress.bind(this),
|
||||
style: {
|
||||
position: 'relative',
|
||||
top: '1.5px',
|
||||
marginBottom: '7px',
|
||||
},
|
||||
}),
|
||||
]),
|
||||
h('div', {
|
||||
key: 'buttons',
|
||||
style: {
|
||||
margin: '0 20px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('button', {
|
||||
onClick: () => this.onExportKeyPress({ key: 'Enter', preventDefault: () => {} }),
|
||||
style: {
|
||||
marginRight: '10px',
|
||||
},
|
||||
}, this.context.t('submit')),
|
||||
h('button', {
|
||||
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
|
||||
}, this.context.t('cancel')),
|
||||
]),
|
||||
(this.props.warning) && (
|
||||
h('span.error', {
|
||||
style: {
|
||||
margin: '20px',
|
||||
},
|
||||
}, this.props.warning.split('-'))
|
||||
),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
if (accountExported) {
|
||||
const plainKey = ethUtil.stripHexPrefix(accountDetail.privateKey)
|
||||
|
||||
return h('div.privateKey', {
|
||||
style: {
|
||||
margin: '0 20px',
|
||||
},
|
||||
}, [
|
||||
h('label', this.context.t('copyPrivateKey') + ':'),
|
||||
h('p.error.cursor-pointer', {
|
||||
style: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
webkitUserSelect: 'text',
|
||||
maxWidth: '275px',
|
||||
},
|
||||
onClick: function (event) {
|
||||
copyToClipboard(ethUtil.stripHexPrefix(accountDetail.privateKey))
|
||||
},
|
||||
}, plainKey),
|
||||
h('button', {
|
||||
onClick: () => this.props.dispatch(actions.backToAccountDetail(this.props.address)),
|
||||
}, this.context.t('done')),
|
||||
h('button', {
|
||||
style: {
|
||||
marginLeft: '10px',
|
||||
},
|
||||
onClick: () => exportAsFile(`Nifty Wallet ${nickname} Private Key`, plainKey),
|
||||
}, this.context.t('saveAsFile')),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
ExportAccountView.prototype.onExportKeyPress = function (event) {
|
||||
if (event.key !== 'Enter') return
|
||||
event.preventDefault()
|
||||
|
||||
const input = document.getElementById('exportAccount').value
|
||||
this.props.dispatch(actions.exportAccount(input, this.props.address))
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const connect = require('react-redux').connect
|
||||
const { compose } = require('recompose')
|
||||
const { withRouter } = require('react-router-dom')
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../../actions')
|
||||
const { Menu, Item, Divider, CloseArea } = require('../dropdowns/components/menu')
|
||||
const Identicon = require('../identicon')
|
||||
const { ENVIRONMENT_TYPE_POPUP } = require('../../../../app/scripts/lib/enums')
|
||||
const { getEnvironmentType } = require('../../../../app/scripts/lib/util')
|
||||
const Tooltip = require('../tooltip')
|
||||
import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display'
|
||||
import { PRIMARY } from '../../constants/common'
|
||||
import { getMetaMaskAccounts } from '../../selectors'
|
||||
|
||||
const {
|
||||
SETTINGS_ROUTE,
|
||||
INFO_ROUTE,
|
||||
NEW_ACCOUNT_ROUTE,
|
||||
IMPORT_ACCOUNT_ROUTE,
|
||||
CONNECT_HARDWARE_ROUTE,
|
||||
DEFAULT_ROUTE,
|
||||
} = require('../../routes')
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(AccountMenu)
|
||||
|
||||
AccountMenu.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
inherits(AccountMenu, Component)
|
||||
function AccountMenu () { Component.call(this) }
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
selectedAddress: state.metamask.selectedAddress,
|
||||
isAccountMenuOpen: state.metamask.isAccountMenuOpen,
|
||||
keyrings: state.metamask.keyrings,
|
||||
identities: state.metamask.identities,
|
||||
accounts: getMetaMaskAccounts(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
|
||||
showAccountDetail: address => {
|
||||
dispatch(actions.showAccountDetail(address))
|
||||
dispatch(actions.hideSidebar())
|
||||
dispatch(actions.toggleAccountMenu())
|
||||
},
|
||||
lockMetamask: () => {
|
||||
dispatch(actions.lockMetamask())
|
||||
dispatch(actions.hideWarning())
|
||||
dispatch(actions.hideSidebar())
|
||||
dispatch(actions.toggleAccountMenu())
|
||||
},
|
||||
showConfigPage: () => {
|
||||
dispatch(actions.showConfigPage())
|
||||
dispatch(actions.hideSidebar())
|
||||
dispatch(actions.toggleAccountMenu())
|
||||
},
|
||||
showInfoPage: () => {
|
||||
dispatch(actions.showInfoPage())
|
||||
dispatch(actions.hideSidebar())
|
||||
dispatch(actions.toggleAccountMenu())
|
||||
},
|
||||
showRemoveAccountConfirmationModal: (identity) => {
|
||||
return dispatch(actions.showModal({ name: 'CONFIRM_REMOVE_ACCOUNT', identity }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
AccountMenu.prototype.render = function () {
|
||||
const {
|
||||
isAccountMenuOpen,
|
||||
toggleAccountMenu,
|
||||
lockMetamask,
|
||||
history,
|
||||
} = this.props
|
||||
|
||||
return h(Menu, { className: 'account-menu', isShowing: isAccountMenuOpen }, [
|
||||
h(CloseArea, { onClick: toggleAccountMenu }),
|
||||
h(Item, {
|
||||
className: 'account-menu__header',
|
||||
}, [
|
||||
this.context.t('myAccounts'),
|
||||
h('button.account-menu__logout-button', {
|
||||
onClick: () => {
|
||||
lockMetamask()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
},
|
||||
}, this.context.t('logout')),
|
||||
]),
|
||||
h(Divider),
|
||||
h('div.account-menu__accounts', this.renderAccounts()),
|
||||
h(Divider),
|
||||
h(Item, {
|
||||
onClick: () => {
|
||||
toggleAccountMenu()
|
||||
history.push(NEW_ACCOUNT_ROUTE)
|
||||
},
|
||||
icon: h('img.account-menu__item-icon', { src: 'images/plus-btn-white.svg' }),
|
||||
text: this.context.t('createAccount'),
|
||||
}),
|
||||
h(Item, {
|
||||
onClick: () => {
|
||||
toggleAccountMenu()
|
||||
history.push(IMPORT_ACCOUNT_ROUTE)
|
||||
},
|
||||
icon: h('img.account-menu__item-icon', { src: 'images/import-account.svg' }),
|
||||
text: this.context.t('importAccount'),
|
||||
}),
|
||||
h(Item, {
|
||||
onClick: () => {
|
||||
toggleAccountMenu()
|
||||
if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) {
|
||||
global.platform.openExtensionInBrowser(CONNECT_HARDWARE_ROUTE)
|
||||
} else {
|
||||
history.push(CONNECT_HARDWARE_ROUTE)
|
||||
}
|
||||
},
|
||||
icon: h('img.account-menu__item-icon', { src: 'images/connect-icon.svg' }),
|
||||
text: this.context.t('connectHardwareWallet'),
|
||||
}),
|
||||
h(Divider),
|
||||
h(Item, {
|
||||
onClick: () => {
|
||||
toggleAccountMenu()
|
||||
history.push(INFO_ROUTE)
|
||||
},
|
||||
icon: h('img', { src: 'images/mm-info-icon.svg' }),
|
||||
text: this.context.t('infoHelp'),
|
||||
}),
|
||||
h(Item, {
|
||||
onClick: () => {
|
||||
toggleAccountMenu()
|
||||
history.push(SETTINGS_ROUTE)
|
||||
},
|
||||
icon: h('img.account-menu__item-icon', { src: 'images/settings.svg' }),
|
||||
text: this.context.t('settings'),
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
AccountMenu.prototype.renderAccounts = function () {
|
||||
const {
|
||||
identities,
|
||||
accounts,
|
||||
selectedAddress,
|
||||
keyrings,
|
||||
showAccountDetail,
|
||||
} = this.props
|
||||
|
||||
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
|
||||
return accountOrder.filter(address => !!identities[address]).map((address) => {
|
||||
|
||||
const identity = identities[address]
|
||||
const isSelected = identity.address === selectedAddress
|
||||
|
||||
const balanceValue = accounts[address] ? accounts[address].balance : ''
|
||||
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||
|
||||
const keyring = keyrings.find((kr) => {
|
||||
return kr.accounts.includes(simpleAddress) ||
|
||||
kr.accounts.includes(identity.address)
|
||||
})
|
||||
|
||||
return h(
|
||||
'div.account-menu__account.menu__item--clickable',
|
||||
{ onClick: () => showAccountDetail(identity.address) },
|
||||
[
|
||||
h('div.account-menu__check-mark', [
|
||||
isSelected ? h('div.account-menu__check-mark-icon') : null,
|
||||
]),
|
||||
|
||||
h(
|
||||
Identicon,
|
||||
{
|
||||
address: identity.address,
|
||||
diameter: 24,
|
||||
},
|
||||
),
|
||||
|
||||
h('div.account-menu__account-info', [
|
||||
h('div.account-menu__name', identity.name || ''),
|
||||
h(UserPreferencedCurrencyDisplay, {
|
||||
className: 'account-menu__balance',
|
||||
value: balanceValue,
|
||||
type: PRIMARY,
|
||||
}),
|
||||
]),
|
||||
|
||||
this.renderKeyringType(keyring),
|
||||
this.renderRemoveAccount(keyring, identity),
|
||||
],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
AccountMenu.prototype.renderRemoveAccount = function (keyring, identity) {
|
||||
// Any account that's not from the HD wallet Keyring can be removed
|
||||
const type = keyring.type
|
||||
const isRemovable = type !== 'HD Key Tree'
|
||||
if (isRemovable) {
|
||||
return h(Tooltip, {
|
||||
title: this.context.t('removeAccount'),
|
||||
position: 'bottom',
|
||||
}, [
|
||||
h('a.remove-account-icon', {
|
||||
onClick: (e) => this.removeAccount(e, identity),
|
||||
}, ''),
|
||||
])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
AccountMenu.prototype.removeAccount = function (e, identity) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const { showRemoveAccountConfirmationModal } = this.props
|
||||
showRemoveAccountConfirmationModal(identity)
|
||||
}
|
||||
|
||||
AccountMenu.prototype.renderKeyringType = function (keyring) {
|
||||
try { // Sometimes keyrings aren't loaded yet:
|
||||
const type = keyring.type
|
||||
let label
|
||||
switch (type) {
|
||||
case 'Trezor Hardware':
|
||||
case 'Ledger Hardware':
|
||||
label = this.context.t('hardware')
|
||||
break
|
||||
case 'Simple Key Pair':
|
||||
label = this.context.t('imported')
|
||||
break
|
||||
default:
|
||||
label = ''
|
||||
}
|
||||
|
||||
return label !== '' ? h('.keyring-label.allcaps', label) : null
|
||||
|
||||
} catch (e) { return }
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const Identicon = require('./identicon')
|
||||
const formatBalance = require('../util').formatBalance
|
||||
const addressSummary = require('../util').addressSummary
|
||||
|
||||
module.exports = AccountPanel
|
||||
|
||||
|
||||
inherits(AccountPanel, Component)
|
||||
function AccountPanel () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
AccountPanel.prototype.render = function () {
|
||||
var state = this.props
|
||||
var identity = state.identity || {}
|
||||
var account = state.account || {}
|
||||
var isFauceting = state.isFauceting
|
||||
|
||||
var panelState = {
|
||||
key: `accountPanel${identity.address}`,
|
||||
identiconKey: identity.address,
|
||||
identiconLabel: identity.name || '',
|
||||
attributes: [
|
||||
{
|
||||
key: 'ADDRESS',
|
||||
value: addressSummary(identity.address),
|
||||
},
|
||||
balanceOrFaucetingIndication(account, isFauceting),
|
||||
],
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
h('.identity-panel.flex-row.flex-space-between', {
|
||||
style: {
|
||||
flex: '1 0 auto',
|
||||
cursor: panelState.onClick ? 'pointer' : undefined,
|
||||
},
|
||||
onClick: panelState.onClick,
|
||||
}, [
|
||||
|
||||
// account identicon
|
||||
h('.identicon-wrapper.flex-column.select-none', [
|
||||
h(Identicon, {
|
||||
address: panelState.identiconKey,
|
||||
imageify: state.imageifyIdenticons,
|
||||
}),
|
||||
h('span.font-small', panelState.identiconLabel.substring(0, 7) + '...'),
|
||||
]),
|
||||
|
||||
// account address, balance
|
||||
h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [
|
||||
|
||||
panelState.attributes.map((attr) => {
|
||||
return h('.flex-row.flex-space-between', {
|
||||
key: '' + Math.round(Math.random() * 1000000),
|
||||
}, [
|
||||
h('label.font-small.no-select', attr.key),
|
||||
h('span.font-small', attr.value),
|
||||
])
|
||||
}),
|
||||
]),
|
||||
|
||||
])
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
function balanceOrFaucetingIndication (account, isFauceting) {
|
||||
// Temporarily deactivating isFauceting indication
|
||||
// because it shows fauceting for empty restored accounts.
|
||||
if (/* isFauceting*/ false) {
|
||||
return {
|
||||
key: 'Account is auto-funding.',
|
||||
value: 'Please wait.',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
key: 'BALANCE',
|
||||
value: formatBalance(account.balance),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
export default class AddTokenButton extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
onClick: () => {},
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { onClick } = this.props
|
||||
|
||||
return (
|
||||
<div className="add-token-button">
|
||||
<h1 className="add-token-button__help-header">{t('missingYourTokens')}</h1>
|
||||
<p className="add-token-button__help-desc">{t('clickToAdd', [t('addToken')])}</p>
|
||||
<div
|
||||
className="add-token-button__button"
|
||||
onClick={onClick}
|
||||
>
|
||||
{t('addToken')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default } from './add-token-button.component'
|
|
@ -1,26 +0,0 @@
|
|||
.add-token-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: lighten($scorpion, 25%);
|
||||
width: 185px;
|
||||
margin: 36px auto;
|
||||
text-align: center;
|
||||
|
||||
&__help-header {
|
||||
font-weight: bold;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&__help-desc {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__button {
|
||||
font-size: 0.75rem;
|
||||
margin: 1rem;
|
||||
text-transform: uppercase;
|
||||
color: $curious-blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
const { Component } = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
|
||||
class Alert extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
visble: false,
|
||||
msg: false,
|
||||
className: '',
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (!this.props.visible && nextProps.visible) {
|
||||
this.animateIn(nextProps)
|
||||
} else if (this.props.visible && !nextProps.visible) {
|
||||
this.animateOut(nextProps)
|
||||
}
|
||||
}
|
||||
|
||||
animateIn (props) {
|
||||
this.setState({
|
||||
msg: props.msg,
|
||||
visible: true,
|
||||
className: '.visible',
|
||||
})
|
||||
}
|
||||
|
||||
animateOut (props) {
|
||||
this.setState({
|
||||
msg: null,
|
||||
className: '.hidden',
|
||||
})
|
||||
|
||||
setTimeout(_ => {
|
||||
this.setState({visible: false})
|
||||
}, 500)
|
||||
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.state.visible) {
|
||||
return (
|
||||
h(`div.global-alert${this.state.className}`, {},
|
||||
h('a.msg', {}, this.state.msg)
|
||||
)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Alert.propTypes = {
|
||||
visible: PropTypes.bool.isRequired,
|
||||
msg: PropTypes.string,
|
||||
}
|
||||
module.exports = Alert
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { matchPath } from 'react-router-dom'
|
||||
|
||||
const {
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
} = require('../../../../app/scripts/lib/enums')
|
||||
const { DEFAULT_ROUTE, INITIALIZE_ROUTE, CONFIRM_TRANSACTION_ROUTE } = require('../../routes')
|
||||
const Identicon = require('../identicon')
|
||||
const NetworkIndicator = require('../network')
|
||||
|
||||
export default class AppHeader extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
network: PropTypes.string,
|
||||
provider: PropTypes.object,
|
||||
networkDropdownOpen: PropTypes.bool,
|
||||
showNetworkDropdown: PropTypes.func,
|
||||
hideNetworkDropdown: PropTypes.func,
|
||||
toggleAccountMenu: PropTypes.func,
|
||||
selectedAddress: PropTypes.string,
|
||||
isUnlocked: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
handleNetworkIndicatorClick (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const { networkDropdownOpen, showNetworkDropdown, hideNetworkDropdown } = this.props
|
||||
|
||||
return networkDropdownOpen === false
|
||||
? showNetworkDropdown()
|
||||
: hideNetworkDropdown()
|
||||
}
|
||||
|
||||
isConfirming () {
|
||||
const { location } = this.props
|
||||
|
||||
return Boolean(matchPath(location.pathname, {
|
||||
path: CONFIRM_TRANSACTION_ROUTE, exact: false,
|
||||
}))
|
||||
}
|
||||
|
||||
renderAccountMenu () {
|
||||
const { isUnlocked, toggleAccountMenu, selectedAddress } = this.props
|
||||
|
||||
return isUnlocked && (
|
||||
<div
|
||||
className={classnames('account-menu__icon', {
|
||||
'account-menu__icon--disabled': this.isConfirming(),
|
||||
})}
|
||||
onClick={() => this.isConfirming() || toggleAccountMenu()}
|
||||
>
|
||||
<Identicon
|
||||
address={selectedAddress}
|
||||
diameter={32}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
hideAppHeader () {
|
||||
const { location } = this.props
|
||||
|
||||
const isInitializing = Boolean(matchPath(location.pathname, {
|
||||
path: INITIALIZE_ROUTE, exact: false,
|
||||
}))
|
||||
|
||||
if (isInitializing) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_NOTIFICATION) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (window.METAMASK_UI_TYPE === ENVIRONMENT_TYPE_POPUP && this.isConfirming()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
network,
|
||||
provider,
|
||||
history,
|
||||
isUnlocked,
|
||||
} = this.props
|
||||
|
||||
if (this.hideAppHeader()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('app-header', { 'app-header--back-drop': isUnlocked })}>
|
||||
<div className="app-header__contents">
|
||||
<div
|
||||
className="app-header__logo-container"
|
||||
onClick={() => history.push(DEFAULT_ROUTE)}
|
||||
>
|
||||
<img
|
||||
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>
|
||||
<div className="app-header__account-menu-container">
|
||||
<div className="app-header__network-component-wrapper">
|
||||
<NetworkIndicator
|
||||
network={network}
|
||||
provider={provider}
|
||||
onClick={event => this.handleNetworkIndicatorClick(event)}
|
||||
disabled={this.isConfirming()}
|
||||
/>
|
||||
</div>
|
||||
{ this.renderAccountMenu() }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
|
||||
import AppHeader from './app-header.component'
|
||||
const actions = require('../../actions')
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { appState, metamask } = state
|
||||
const { networkDropdownOpen } = appState
|
||||
const {
|
||||
network,
|
||||
provider,
|
||||
selectedAddress,
|
||||
isUnlocked,
|
||||
} = metamask
|
||||
|
||||
return {
|
||||
networkDropdownOpen,
|
||||
network,
|
||||
provider,
|
||||
selectedAddress,
|
||||
isUnlocked,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
showNetworkDropdown: () => dispatch(actions.showNetworkDropdown()),
|
||||
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
|
||||
toggleAccountMenu: () => dispatch(actions.toggleAccountMenu()),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(AppHeader)
|
|
@ -1 +0,0 @@
|
|||
export { default } from './app-header.container'
|
|
@ -1,90 +0,0 @@
|
|||
.app-header {
|
||||
align-items: center;
|
||||
background: $gallery;
|
||||
position: relative;
|
||||
z-index: $header-z-index;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
padding: 12px;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, .08);
|
||||
z-index: $mobile-header-z-index;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
height: 75px;
|
||||
justify-content: center;
|
||||
|
||||
&--back-drop {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
background: $gallery;
|
||||
bottom: -32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__metafox-logo {
|
||||
cursor: pointer;
|
||||
|
||||
&--icon {
|
||||
@media screen and (min-width: $break-large) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--horizontal {
|
||||
@media screen and (max-width: $break-small) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__contents {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 576px) {
|
||||
width: 85vw;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
width: 80vw;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1281px) {
|
||||
width: 62vw;
|
||||
}
|
||||
}
|
||||
|
||||
&__logo-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__account-menu-container {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__network-component-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const connect = require('react-redux').connect
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const TokenBalance = require('./token-balance')
|
||||
const Identicon = require('./identicon')
|
||||
import UserPreferencedCurrencyDisplay from './user-preferenced-currency-display'
|
||||
import { PRIMARY, SECONDARY } from '../constants/common'
|
||||
const { getAssetImages, conversionRateSelector, getCurrentCurrency, getMetaMaskAccounts } = require('../selectors')
|
||||
|
||||
const { formatBalance } = require('../util')
|
||||
|
||||
module.exports = connect(mapStateToProps)(BalanceComponent)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const accounts = getMetaMaskAccounts(state)
|
||||
const network = state.metamask.network
|
||||
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
|
||||
const account = accounts[selectedAddress]
|
||||
|
||||
return {
|
||||
account,
|
||||
network,
|
||||
conversionRate: conversionRateSelector(state),
|
||||
currentCurrency: getCurrentCurrency(state),
|
||||
assetImages: getAssetImages(state),
|
||||
}
|
||||
}
|
||||
|
||||
inherits(BalanceComponent, Component)
|
||||
function BalanceComponent () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { token, network, assetImages } = props
|
||||
const address = token && token.address
|
||||
const image = assetImages && address ? assetImages[token.address] : undefined
|
||||
|
||||
return h('div.balance-container', {}, [
|
||||
|
||||
// TODO: balance icon needs to be passed in
|
||||
// h('img.balance-icon', {
|
||||
// src: '../images/eth_logo.svg',
|
||||
// style: {},
|
||||
// }),
|
||||
h(Identicon, {
|
||||
diameter: 50,
|
||||
address,
|
||||
network,
|
||||
image,
|
||||
}),
|
||||
|
||||
token ? this.renderTokenBalance() : this.renderBalance(),
|
||||
])
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.renderTokenBalance = function () {
|
||||
const { token } = this.props
|
||||
|
||||
return h('div.flex-column.balance-display', [
|
||||
h('div.token-amount', [ h(TokenBalance, { token }) ]),
|
||||
])
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.renderBalance = function () {
|
||||
const props = this.props
|
||||
const { account } = props
|
||||
const balanceValue = account && account.balance
|
||||
const needsParse = 'needsParse' in props ? props.needsParse : true
|
||||
const formattedBalance = balanceValue ? formatBalance(balanceValue, 6, needsParse) : '...'
|
||||
const showFiat = 'showFiat' in props ? props.showFiat : true
|
||||
|
||||
if (formattedBalance === 'None' || formattedBalance === '...') {
|
||||
return h('div.flex-column.balance-display', {}, [
|
||||
h('div.token-amount', {
|
||||
style: {},
|
||||
}, formattedBalance),
|
||||
])
|
||||
}
|
||||
|
||||
return h('div.flex-column.balance-display', {}, [
|
||||
h('div.token-amount', {}, h(UserPreferencedCurrencyDisplay, {
|
||||
value: balanceValue,
|
||||
type: PRIMARY,
|
||||
ethNumberOfDecimals: 3,
|
||||
})),
|
||||
|
||||
showFiat && h(UserPreferencedCurrencyDisplay, {
|
||||
value: balanceValue,
|
||||
type: SECONDARY,
|
||||
ethNumberOfDecimals: 3,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.getFiatDisplayNumber = function (formattedBalance, conversionRate) {
|
||||
if (formattedBalance === 'None') return formattedBalance
|
||||
if (conversionRate === 0) return 'N/A'
|
||||
|
||||
const splitBalance = formattedBalance.split(' ')
|
||||
|
||||
const convertedNumber = (Number(splitBalance[0]) * conversionRate)
|
||||
const wholePart = Math.floor(convertedNumber)
|
||||
const decimalPart = convertedNumber - wholePart
|
||||
|
||||
return wholePart + Number(decimalPart.toPrecision(2))
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const BN = ethUtil.BN
|
||||
const extend = require('xtend')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
BnAsDecimalInput.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(BnAsDecimalInput)
|
||||
|
||||
|
||||
inherits(BnAsDecimalInput, Component)
|
||||
function BnAsDecimalInput () {
|
||||
this.state = { invalid: null }
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
/* Bn as Decimal Input
|
||||
*
|
||||
* A component for allowing easy, decimal editing
|
||||
* of a passed in bn string value.
|
||||
*
|
||||
* On change, calls back its `onChange` function parameter
|
||||
* and passes it an updated bn string.
|
||||
*/
|
||||
|
||||
BnAsDecimalInput.prototype.render = function () {
|
||||
const props = this.props
|
||||
const state = this.state
|
||||
|
||||
const { value, scale, precision, onChange, min, max } = props
|
||||
|
||||
const suffix = props.suffix
|
||||
const style = props.style
|
||||
const valueString = value.toString(10)
|
||||
const newMin = min && this.downsize(min.toString(10), scale)
|
||||
const newMax = max && this.downsize(max.toString(10), scale)
|
||||
const newValue = this.downsize(valueString, scale)
|
||||
|
||||
return (
|
||||
h('.flex-column', [
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
alignItems: 'flex-end',
|
||||
lineHeight: '13px',
|
||||
fontFamily: 'Montserrat Light',
|
||||
textRendering: 'geometricPrecision',
|
||||
},
|
||||
}, [
|
||||
h('input.hex-input', {
|
||||
type: 'number',
|
||||
step: 'any',
|
||||
required: true,
|
||||
min: newMin,
|
||||
max: newMax,
|
||||
style: extend({
|
||||
display: 'block',
|
||||
textAlign: 'right',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid #bdbdbd',
|
||||
|
||||
}, style),
|
||||
value: newValue,
|
||||
onBlur: (event) => {
|
||||
this.updateValidity(event)
|
||||
},
|
||||
onChange: (event) => {
|
||||
this.updateValidity(event)
|
||||
const value = (event.target.value === '') ? '' : event.target.value
|
||||
|
||||
|
||||
const scaledNumber = this.upsize(value, scale, precision)
|
||||
const precisionBN = new BN(scaledNumber, 10)
|
||||
onChange(precisionBN, event.target.checkValidity())
|
||||
},
|
||||
onInvalid: (event) => {
|
||||
const msg = this.constructWarning()
|
||||
if (msg === state.invalid) {
|
||||
return
|
||||
}
|
||||
this.setState({ invalid: msg })
|
||||
event.preventDefault()
|
||||
return false
|
||||
},
|
||||
}),
|
||||
h('div', {
|
||||
style: {
|
||||
color: ' #AEAEAE',
|
||||
fontSize: '12px',
|
||||
marginLeft: '5px',
|
||||
marginRight: '6px',
|
||||
width: '20px',
|
||||
},
|
||||
}, suffix),
|
||||
]),
|
||||
|
||||
state.invalid ? h('span.error', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
right: '0px',
|
||||
textAlign: 'right',
|
||||
transform: 'translateY(26px)',
|
||||
padding: '3px',
|
||||
background: 'rgba(255,255,255,0.85)',
|
||||
zIndex: '1',
|
||||
textTransform: 'capitalize',
|
||||
border: '2px solid #E20202',
|
||||
},
|
||||
}, state.invalid) : null,
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
BnAsDecimalInput.prototype.setValid = function (message) {
|
||||
this.setState({ invalid: null })
|
||||
}
|
||||
|
||||
BnAsDecimalInput.prototype.updateValidity = function (event) {
|
||||
const target = event.target
|
||||
const value = this.props.value
|
||||
const newValue = target.value
|
||||
|
||||
if (value === newValue) {
|
||||
return
|
||||
}
|
||||
|
||||
const valid = target.checkValidity()
|
||||
|
||||
if (valid) {
|
||||
this.setState({ invalid: null })
|
||||
}
|
||||
}
|
||||
|
||||
BnAsDecimalInput.prototype.constructWarning = function () {
|
||||
const { name, min, max, scale, suffix } = this.props
|
||||
const newMin = min && this.downsize(min.toString(10), scale)
|
||||
const newMax = max && this.downsize(max.toString(10), scale)
|
||||
let message = name ? name + ' ' : ''
|
||||
|
||||
if (min && max) {
|
||||
message += this.context.t('betweenMinAndMax', [`${newMin} ${suffix}`, `${newMax} ${suffix}`])
|
||||
} else if (min) {
|
||||
message += this.context.t('greaterThanMin', [`${newMin} ${suffix}`])
|
||||
} else if (max) {
|
||||
message += this.context.t('lessThanMax', [`${newMax} ${suffix}`])
|
||||
} else {
|
||||
message += this.context.t('invalidInput')
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
|
||||
BnAsDecimalInput.prototype.downsize = function (number, scale) {
|
||||
// if there is no scaling, simply return the number
|
||||
if (scale === 0) {
|
||||
return Number(number)
|
||||
} else {
|
||||
// if the scale is the same as the precision, account for this edge case.
|
||||
var adjustedNumber = number
|
||||
while (adjustedNumber.length < scale) {
|
||||
adjustedNumber = '0' + adjustedNumber
|
||||
}
|
||||
return Number(adjustedNumber.slice(0, -scale) + '.' + adjustedNumber.slice(-scale))
|
||||
}
|
||||
}
|
||||
|
||||
BnAsDecimalInput.prototype.upsize = function (number, scale, precision) {
|
||||
var stringArray = number.toString().split('.')
|
||||
var decimalLength = stringArray[1] ? stringArray[1].length : 0
|
||||
var newString = stringArray[0]
|
||||
|
||||
// If there is scaling and decimal parts exist, integrate them in.
|
||||
if ((scale !== 0) && (decimalLength !== 0)) {
|
||||
newString += stringArray[1].slice(0, precision)
|
||||
}
|
||||
|
||||
// Add 0s to account for the upscaling.
|
||||
for (var i = decimalLength; i < scale; i++) {
|
||||
newString += '0'
|
||||
}
|
||||
return newString
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class ButtonGroup extends PureComponent {
|
||||
static propTypes = {
|
||||
defaultActiveButtonIndex: PropTypes.number,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.array,
|
||||
className: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
className: 'button-group',
|
||||
}
|
||||
|
||||
state = {
|
||||
activeButtonIndex: this.props.defaultActiveButtonIndex || 0,
|
||||
}
|
||||
|
||||
handleButtonClick (activeButtonIndex) {
|
||||
this.setState({ activeButtonIndex })
|
||||
}
|
||||
|
||||
renderButtons () {
|
||||
const { children, disabled } = this.props
|
||||
|
||||
return React.Children.map(children, (child, index) => {
|
||||
return child && (
|
||||
<button
|
||||
className={classnames(
|
||||
'button-group__button',
|
||||
{ 'button-group__button--active': index === this.state.activeButtonIndex },
|
||||
)}
|
||||
onClick={() => {
|
||||
this.handleButtonClick(index)
|
||||
child.props.onClick && child.props.onClick()
|
||||
}}
|
||||
disabled={disabled || child.props.disabled}
|
||||
key={index}
|
||||
>
|
||||
{ child.props.children }
|
||||
</button>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, style } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
{ this.renderButtons() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import React from 'react'
|
||||
import { storiesOf } from '@storybook/react'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import ButtonGroup from './'
|
||||
import Button from '../button'
|
||||
import { text, boolean } from '@storybook/addon-knobs/react'
|
||||
|
||||
storiesOf('ButtonGroup', module)
|
||||
.add('with Buttons', () =>
|
||||
<ButtonGroup
|
||||
style={{ width: '300px' }}
|
||||
disabled={boolean('Disabled', false)}
|
||||
defaultActiveButtonIndex={1}
|
||||
>
|
||||
<Button
|
||||
onClick={action('cheap')}
|
||||
>
|
||||
{text('Button1', 'Cheap')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={action('average')}
|
||||
>
|
||||
{text('Button2', 'Average')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={action('fast')}
|
||||
>
|
||||
{text('Button3', 'Fast')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)
|
||||
.add('with a disabled Button', () =>
|
||||
<ButtonGroup
|
||||
style={{ width: '300px' }}
|
||||
disabled={boolean('Disabled', false)}
|
||||
>
|
||||
<Button
|
||||
onClick={action('enabled')}
|
||||
>
|
||||
{text('Button1', 'Enabled')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={action('disabled')}
|
||||
disabled
|
||||
>
|
||||
{text('Button2', 'Disabled')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
export { default } from './button-group.component'
|
|
@ -1,38 +0,0 @@
|
|||
.button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&__button {
|
||||
font-family: Roboto;
|
||||
font-size: 1rem;
|
||||
color: $tundora;
|
||||
border-style: solid;
|
||||
border-color: $alto;
|
||||
border-width: 1px 1px 1px;
|
||||
border-left: 0;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:first-child {
|
||||
border-left: 1px solid $alto;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background-color: $dodger-blue;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import ButtonGroup from '../button-group.component.js'
|
||||
|
||||
const childButtonSpies = {
|
||||
onClick: sinon.spy(),
|
||||
}
|
||||
|
||||
sinon.spy(ButtonGroup.prototype, 'handleButtonClick')
|
||||
sinon.spy(ButtonGroup.prototype, 'renderButtons')
|
||||
|
||||
const mockButtons = [
|
||||
<button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>,
|
||||
<button onClick={childButtonSpies.onClick} key={'b'}></button>,
|
||||
<button onClick={childButtonSpies.onClick} key={'c'}></button>,
|
||||
]
|
||||
|
||||
describe('ButtonGroup Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<ButtonGroup
|
||||
defaultActiveButtonIndex={1}
|
||||
disabled={false}
|
||||
className="someClassName"
|
||||
style={ { color: 'red' } }
|
||||
>{mockButtons}</ButtonGroup>)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
childButtonSpies.onClick.resetHistory()
|
||||
ButtonGroup.prototype.handleButtonClick.resetHistory()
|
||||
ButtonGroup.prototype.renderButtons.resetHistory()
|
||||
})
|
||||
|
||||
describe('handleButtonClick', () => {
|
||||
it('should set the activeButtonIndex', () => {
|
||||
assert.equal(wrapper.state('activeButtonIndex'), 1)
|
||||
wrapper.instance().handleButtonClick(2)
|
||||
assert.equal(wrapper.state('activeButtonIndex'), 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderButtons', () => {
|
||||
it('should render a button for each child', () => {
|
||||
const childButtons = wrapper.find('.button-group__button')
|
||||
assert.equal(childButtons.length, 3)
|
||||
})
|
||||
|
||||
it('should render the correct button with an active state', () => {
|
||||
const childButtons = wrapper.find('.button-group__button')
|
||||
const activeChildButton = wrapper.find('.button-group__button--active')
|
||||
assert.deepEqual(childButtons.get(1), activeChildButton.get(0))
|
||||
})
|
||||
|
||||
it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => {
|
||||
assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0)
|
||||
assert.equal(childButtonSpies.onClick.callCount, 0)
|
||||
const childButtons = wrapper.find('.button-group__button')
|
||||
childButtons.at(0).props().onClick()
|
||||
childButtons.at(1).props().onClick()
|
||||
childButtons.at(2).props().onClick()
|
||||
assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3)
|
||||
assert.equal(childButtonSpies.onClick.callCount, 3)
|
||||
})
|
||||
|
||||
it('should render all child buttons as disabled if props.disabled is true', () => {
|
||||
const childButtons = wrapper.find('.button-group__button')
|
||||
childButtons.forEach(button => {
|
||||
assert.equal(button.props().disabled, undefined)
|
||||
})
|
||||
wrapper.setProps({ disabled: true })
|
||||
const disabledChildButtons = wrapper.find('[disabled=true]')
|
||||
assert.equal(disabledChildButtons.length, 3)
|
||||
})
|
||||
|
||||
it('should render the children of the button', () => {
|
||||
const mockClass = wrapper.find('.mockClass')
|
||||
assert.equal(mockClass.length, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a div with the expected class and style', () => {
|
||||
assert.equal(wrapper.find('div').at(0).props().className, 'someClassName')
|
||||
assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' })
|
||||
})
|
||||
|
||||
it('should call renderButtons when rendering', () => {
|
||||
assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1)
|
||||
wrapper.instance().render()
|
||||
assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,25 +0,0 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class Card extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
overrideClassName: PropTypes.bool,
|
||||
title: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, overrideClassName, title } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames({ 'card': !overrideClassName }, className)}>
|
||||
<div className="card__title">
|
||||
{ title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default } from './card.component'
|
|
@ -1,11 +0,0 @@
|
|||
.card {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
|
||||
padding: 8px;
|
||||
|
||||
&__title {
|
||||
border-bottom: 1px solid #d8d8d8;
|
||||
padding-bottom: 4px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import Card from '../card.component'
|
||||
|
||||
describe('Card Component', () => {
|
||||
it('should render a card with a title and child element', () => {
|
||||
const wrapper = shallow(
|
||||
<Card
|
||||
title="Test"
|
||||
className="card-test-class"
|
||||
>
|
||||
<div className="child-test-class">Child</div>
|
||||
</Card>
|
||||
)
|
||||
|
||||
assert.ok(wrapper.hasClass('card-test-class'))
|
||||
const title = wrapper.find('.card__title')
|
||||
assert.ok(title)
|
||||
assert.equal(title.text(), 'Test')
|
||||
const child = wrapper.find('.child-test-class')
|
||||
assert.ok(child)
|
||||
assert.equal(child.text(), 'Child')
|
||||
})
|
||||
})
|
|
@ -1,69 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../actions')
|
||||
|
||||
CoinbaseForm.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(CoinbaseForm)
|
||||
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
warning: state.appState.warning,
|
||||
}
|
||||
}
|
||||
|
||||
inherits(CoinbaseForm, Component)
|
||||
|
||||
function CoinbaseForm () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
CoinbaseForm.prototype.render = function () {
|
||||
var props = this.props
|
||||
|
||||
return h('.flex-column', {
|
||||
style: {
|
||||
marginTop: '35px',
|
||||
padding: '25px',
|
||||
width: '100%',
|
||||
},
|
||||
}, [
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
justifyContent: 'space-around',
|
||||
margin: '33px',
|
||||
marginTop: '0px',
|
||||
},
|
||||
}, [
|
||||
h('button.btn-green', {
|
||||
onClick: this.toCoinbase.bind(this),
|
||||
}, this.context.t('continueToCoinbase')),
|
||||
|
||||
h('button.btn-red', {
|
||||
onClick: () => props.dispatch(actions.goHome()),
|
||||
}, this.context.t('cancel')),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
CoinbaseForm.prototype.toCoinbase = function () {
|
||||
const props = this.props
|
||||
const address = props.buyView.buyAddress
|
||||
props.dispatch(actions.buyEth({ network: '1', address, amount: 0 }))
|
||||
}
|
||||
|
||||
CoinbaseForm.prototype.renderLoading = function () {
|
||||
return h('img', {
|
||||
style: {
|
||||
width: '27px',
|
||||
marginRight: '-27px',
|
||||
},
|
||||
src: 'images/loading.svg',
|
||||
})
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'
|
||||
import { PRIMARY, SECONDARY } from '../../../constants/common'
|
||||
|
||||
const ConfirmDetailRow = props => {
|
||||
const {
|
||||
label,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
onHeaderClick,
|
||||
primaryValueTextColor,
|
||||
headerText,
|
||||
headerTextClassName,
|
||||
value,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className="confirm-detail-row">
|
||||
<div className="confirm-detail-row__label">
|
||||
{ label }
|
||||
</div>
|
||||
<div className="confirm-detail-row__details">
|
||||
<div
|
||||
className={classnames('confirm-detail-row__header-text', headerTextClassName)}
|
||||
onClick={() => onHeaderClick && onHeaderClick()}
|
||||
>
|
||||
{ headerText }
|
||||
</div>
|
||||
{
|
||||
primaryText
|
||||
? (
|
||||
<div
|
||||
className="confirm-detail-row__primary"
|
||||
style={{ color: primaryValueTextColor }}
|
||||
>
|
||||
{ primaryText }
|
||||
</div>
|
||||
) : (
|
||||
<UserPreferencedCurrencyDisplay
|
||||
className="confirm-detail-row__primary"
|
||||
type={PRIMARY}
|
||||
value={value}
|
||||
showEthLogo
|
||||
ethLogoHeight="18"
|
||||
style={{ color: primaryValueTextColor }}
|
||||
hideLabel
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
secondaryText
|
||||
? (
|
||||
<div className="confirm-detail-row__secondary">
|
||||
{ secondaryText }
|
||||
</div>
|
||||
) : (
|
||||
<UserPreferencedCurrencyDisplay
|
||||
className="confirm-detail-row__secondary"
|
||||
type={SECONDARY}
|
||||
value={value}
|
||||
showEthLogo
|
||||
hideLabel
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmDetailRow.propTypes = {
|
||||
headerText: PropTypes.string,
|
||||
headerTextClassName: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
onHeaderClick: PropTypes.func,
|
||||
primaryValueTextColor: PropTypes.string,
|
||||
primaryText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||
secondaryText: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ConfirmDetailRow
|
|
@ -1 +0,0 @@
|
|||
export { default } from './confirm-detail-row.component'
|
|
@ -1,46 +0,0 @@
|
|||
.confirm-detail-row {
|
||||
padding: 14px 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
color: $scorpion;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__details {
|
||||
flex: 1;
|
||||
text-align: end;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__primary {
|
||||
font-size: 1.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__secondary {
|
||||
color: $oslo-gray;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__header-text {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
color: $scorpion;
|
||||
|
||||
&--edit {
|
||||
color: $curious-blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--total {
|
||||
font-size: .625rem;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import ConfirmDetailRow from '../confirm-detail-row.component.js'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const propsMethodSpies = {
|
||||
onHeaderClick: sinon.spy(),
|
||||
}
|
||||
|
||||
describe('Confirm Detail Row Component', function () {
|
||||
let wrapper
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(
|
||||
<ConfirmDetailRow
|
||||
errorType={'mockErrorType'}
|
||||
label={'mockLabel'}
|
||||
showError={false}
|
||||
primaryText = {'mockFiatText'}
|
||||
secondaryText = {'mockEthText'}
|
||||
primaryValueTextColor= {'mockColor'}
|
||||
onHeaderClick= {propsMethodSpies.onHeaderClick}
|
||||
headerText = {'mockHeaderText'}
|
||||
headerTextClassName = {'mockHeaderClass'}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
describe('render', () => {
|
||||
it('should render a div with a confirm-detail-row class', () => {
|
||||
assert.equal(wrapper.find('div.confirm-detail-row').length, 1)
|
||||
})
|
||||
|
||||
it('should render the label as a child of the confirm-detail-row__label', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row > .confirm-detail-row__label').childAt(0).text(), 'mockLabel')
|
||||
})
|
||||
|
||||
it('should render the headerText as a child of the confirm-detail-row__header-text', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText')
|
||||
})
|
||||
|
||||
it('should render the primaryText as a child of the confirm-detail-row__primary', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__primary').childAt(0).text(), 'mockFiatText')
|
||||
})
|
||||
|
||||
it('should render the ethText as a child of the confirm-detail-row__secondary', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__secondary').childAt(0).text(), 'mockEthText')
|
||||
})
|
||||
|
||||
it('should set the fiatTextColor on confirm-detail-row__primary', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__primary').props().style.color, 'mockColor')
|
||||
})
|
||||
|
||||
it('should assure the confirm-detail-row__header-text classname is correct', () => {
|
||||
assert.equal(wrapper.find('.confirm-detail-row__header-text').props().className, 'confirm-detail-row__header-text mockHeaderClass')
|
||||
})
|
||||
|
||||
it('should call onHeaderClick when headerText div gets clicked', () => {
|
||||
wrapper.find('.confirm-detail-row__header-text').props().onClick()
|
||||
assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,110 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { Tabs, Tab } from '../../tabs'
|
||||
import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from './'
|
||||
import ErrorMessage from '../../error-message'
|
||||
|
||||
export default class ConfirmPageContainerContent extends Component {
|
||||
static propTypes = {
|
||||
action: PropTypes.string,
|
||||
dataComponent: PropTypes.node,
|
||||
detailsComponent: PropTypes.node,
|
||||
errorKey: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
assetImage: PropTypes.string,
|
||||
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
subtitleComponent: PropTypes.node,
|
||||
summaryComponent: PropTypes.node,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
titleComponent: PropTypes.node,
|
||||
warning: PropTypes.string,
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { detailsComponent, dataComponent } = this.props
|
||||
|
||||
if (detailsComponent && dataComponent) {
|
||||
return this.renderTabs()
|
||||
} else {
|
||||
return detailsComponent || dataComponent
|
||||
}
|
||||
}
|
||||
|
||||
renderTabs () {
|
||||
const { detailsComponent, dataComponent } = this.props
|
||||
|
||||
return (
|
||||
<Tabs>
|
||||
<Tab name="Details">
|
||||
{ detailsComponent }
|
||||
</Tab>
|
||||
<Tab name="Data">
|
||||
{ dataComponent }
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
action,
|
||||
errorKey,
|
||||
errorMessage,
|
||||
title,
|
||||
titleComponent,
|
||||
subtitle,
|
||||
subtitleComponent,
|
||||
hideSubtitle,
|
||||
identiconAddress,
|
||||
nonce,
|
||||
assetImage,
|
||||
summaryComponent,
|
||||
detailsComponent,
|
||||
dataComponent,
|
||||
warning,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-content">
|
||||
{
|
||||
warning && (
|
||||
<ConfirmPageContainerWarning warning={warning} />
|
||||
)
|
||||
}
|
||||
{
|
||||
summaryComponent || (
|
||||
<ConfirmPageContainerSummary
|
||||
className={classnames({
|
||||
'confirm-page-container-summary--border': !detailsComponent || !dataComponent,
|
||||
})}
|
||||
action={action}
|
||||
title={title}
|
||||
titleComponent={titleComponent}
|
||||
subtitle={subtitle}
|
||||
subtitleComponent={subtitleComponent}
|
||||
hideSubtitle={hideSubtitle}
|
||||
identiconAddress={identiconAddress}
|
||||
nonce={nonce}
|
||||
assetImage={assetImage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{ this.renderContent() }
|
||||
{
|
||||
(errorKey || errorMessage) && (
|
||||
<div className="confirm-page-container-content__error-container">
|
||||
<ErrorMessage
|
||||
errorMessage={errorMessage}
|
||||
errorKey={errorKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Identicon from '../../../identicon'
|
||||
|
||||
const ConfirmPageContainerSummary = props => {
|
||||
const {
|
||||
action,
|
||||
title,
|
||||
titleComponent,
|
||||
subtitle,
|
||||
subtitleComponent,
|
||||
hideSubtitle,
|
||||
className,
|
||||
identiconAddress,
|
||||
nonce,
|
||||
assetImage,
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div className={classnames('confirm-page-container-summary', className)}>
|
||||
<div className="confirm-page-container-summary__action-row">
|
||||
<div className="confirm-page-container-summary__action">
|
||||
{ action }
|
||||
</div>
|
||||
{
|
||||
nonce && (
|
||||
<div className="confirm-page-container-summary__nonce">
|
||||
{ `#${nonce}` }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="confirm-page-container-summary__title">
|
||||
{
|
||||
identiconAddress && (
|
||||
<Identicon
|
||||
className="confirm-page-container-summary__identicon"
|
||||
diameter={36}
|
||||
address={identiconAddress}
|
||||
image={assetImage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="confirm-page-container-summary__title-text">
|
||||
{ titleComponent || title }
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
hideSubtitle || <div className="confirm-page-container-summary__subtitle">
|
||||
{ subtitleComponent || subtitle }
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmPageContainerSummary.propTypes = {
|
||||
action: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
titleComponent: PropTypes.node,
|
||||
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
subtitleComponent: PropTypes.node,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
assetImage: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ConfirmPageContainerSummary
|
|
@ -1 +0,0 @@
|
|||
export { default } from './confirm-page-container-summary.component'
|
|
@ -1,54 +0,0 @@
|
|||
.confirm-page-container-summary {
|
||||
padding: 16px 24px 0;
|
||||
background-color: #f9fafa;
|
||||
height: 133px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&__action-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__action {
|
||||
text-transform: uppercase;
|
||||
color: $oslo-gray;
|
||||
font-size: .75rem;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid $oslo-gray;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__nonce {
|
||||
color: $oslo-gray;
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding: 4px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&__title-text {
|
||||
font-size: 2.25rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: $oslo-gray;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&--border {
|
||||
border-bottom: 1px solid $geyser;
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ConfirmPageContainerWarning = props => {
|
||||
return (
|
||||
<div className="confirm-page-container-warning">
|
||||
<img
|
||||
className="confirm-page-container-warning__icon"
|
||||
src="/images/alert.svg"
|
||||
/>
|
||||
<div className="confirm-page-container-warning__warning">
|
||||
{ props.warning }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ConfirmPageContainerWarning.propTypes = {
|
||||
warning: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ConfirmPageContainerWarning
|
|
@ -1 +0,0 @@
|
|||
export { default } from './confirm-page-container-warning.component'
|
|
@ -1,18 +0,0 @@
|
|||
.confirm-page-container-warning {
|
||||
background-color: #fffcdb;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 12px 24px;
|
||||
|
||||
&__icon {
|
||||
flex: 0 0 auto;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
&__warning {
|
||||
font-size: .75rem;
|
||||
color: #5f5922;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export { default } from './confirm-page-container-content.component'
|
||||
export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary'
|
||||
export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning'
|
|
@ -1,64 +0,0 @@
|
|||
@import './confirm-page-container-warning/index';
|
||||
|
||||
@import './confirm-page-container-summary/index';
|
||||
|
||||
.confirm-page-container-content {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
&__error-container {
|
||||
padding: 0 16px 16px 16px;
|
||||
}
|
||||
|
||||
&__details {
|
||||
box-sizing: border-box;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
&__data {
|
||||
padding: 16px;
|
||||
color: $oslo-gray;
|
||||
}
|
||||
|
||||
&__data-box {
|
||||
background-color: #f9fafa;
|
||||
padding: 12px;
|
||||
font-size: .75rem;
|
||||
margin-bottom: 16px;
|
||||
word-wrap: break-word;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
||||
&-label {
|
||||
text-transform: uppercase;
|
||||
padding: 8px 0 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__data-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
&-label {
|
||||
font-weight: 500;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__gas-fee {
|
||||
border-bottom: 1px solid $geyser;
|
||||
}
|
||||
|
||||
&__function-type {
|
||||
font-size: .875rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
color: $black;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
ENVIRONMENT_TYPE_POPUP,
|
||||
ENVIRONMENT_TYPE_NOTIFICATION,
|
||||
} from '../../../../../app/scripts/lib/enums'
|
||||
import NetworkDisplay from '../../network-display'
|
||||
|
||||
export default class ConfirmPageContainer extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
showEdit: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
renderTop () {
|
||||
const { onEdit, showEdit } = this.props
|
||||
const windowType = window.METAMASK_UI_TYPE
|
||||
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION &&
|
||||
windowType !== ENVIRONMENT_TYPE_POPUP
|
||||
|
||||
if (!showEdit && isFullScreen) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-header__row">
|
||||
<div
|
||||
className="confirm-page-container-header__back-button-container"
|
||||
style={{
|
||||
visibility: showEdit ? 'initial' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/images/caret-left.svg"
|
||||
/>
|
||||
<span
|
||||
className="confirm-page-container-header__back-button"
|
||||
onClick={() => onEdit()}
|
||||
>
|
||||
{ this.context.t('edit') }
|
||||
</span>
|
||||
</div>
|
||||
{ !isFullScreen && <NetworkDisplay /> }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<div className="confirm-page-container-header">
|
||||
{ this.renderTop() }
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default } from './confirm-page-container-header.component'
|
|
@ -1,27 +0,0 @@
|
|||
.confirm-page-container-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 13px 13px 13px 24px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__back-button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__back-button {
|
||||
color: #2f9ae0;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SenderToRecipient from '../sender-to-recipient'
|
||||
import { PageContainerFooter } from '../page-container'
|
||||
import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './'
|
||||
|
||||
export default class ConfirmPageContainer extends Component {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
// Header
|
||||
action: PropTypes.string,
|
||||
hideSubtitle: PropTypes.bool,
|
||||
onEdit: PropTypes.func,
|
||||
showEdit: PropTypes.bool,
|
||||
subtitle: PropTypes.string,
|
||||
subtitleComponent: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
titleComponent: PropTypes.node,
|
||||
// Sender to Recipient
|
||||
fromAddress: PropTypes.string,
|
||||
fromName: PropTypes.string,
|
||||
toAddress: PropTypes.string,
|
||||
toName: PropTypes.string,
|
||||
// Content
|
||||
contentComponent: PropTypes.node,
|
||||
errorKey: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
fiatTransactionAmount: PropTypes.string,
|
||||
fiatTransactionFee: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
ethTransactionAmount: PropTypes.string,
|
||||
ethTransactionFee: PropTypes.string,
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
onEditGas: PropTypes.func,
|
||||
dataComponent: PropTypes.node,
|
||||
detailsComponent: PropTypes.node,
|
||||
identiconAddress: PropTypes.string,
|
||||
nonce: PropTypes.string,
|
||||
assetImage: PropTypes.string,
|
||||
summaryComponent: PropTypes.node,
|
||||
warning: PropTypes.string,
|
||||
unapprovedTxCount: PropTypes.number,
|
||||
// Footer
|
||||
onCancelAll: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
showEdit,
|
||||
onEdit,
|
||||
fromName,
|
||||
fromAddress,
|
||||
toName,
|
||||
toAddress,
|
||||
disabled,
|
||||
errorKey,
|
||||
errorMessage,
|
||||
contentComponent,
|
||||
action,
|
||||
title,
|
||||
titleComponent,
|
||||
subtitle,
|
||||
subtitleComponent,
|
||||
hideSubtitle,
|
||||
summaryComponent,
|
||||
detailsComponent,
|
||||
dataComponent,
|
||||
onCancelAll,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
identiconAddress,
|
||||
nonce,
|
||||
unapprovedTxCount,
|
||||
assetImage,
|
||||
warning,
|
||||
} = this.props
|
||||
const renderAssetImage = contentComponent || (!contentComponent && !identiconAddress)
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<ConfirmPageContainerHeader
|
||||
showEdit={showEdit}
|
||||
onEdit={() => onEdit()}
|
||||
>
|
||||
<SenderToRecipient
|
||||
senderName={fromName}
|
||||
senderAddress={fromAddress}
|
||||
recipientName={toName}
|
||||
recipientAddress={toAddress}
|
||||
assetImage={renderAssetImage ? assetImage : undefined}
|
||||
/>
|
||||
</ConfirmPageContainerHeader>
|
||||
{
|
||||
contentComponent || (
|
||||
<ConfirmPageContainerContent
|
||||
action={action}
|
||||
title={title}
|
||||
titleComponent={titleComponent}
|
||||
subtitle={subtitle}
|
||||
subtitleComponent={subtitleComponent}
|
||||
hideSubtitle={hideSubtitle}
|
||||
summaryComponent={summaryComponent}
|
||||
detailsComponent={detailsComponent}
|
||||
dataComponent={dataComponent}
|
||||
errorMessage={errorMessage}
|
||||
errorKey={errorKey}
|
||||
identiconAddress={identiconAddress}
|
||||
nonce={nonce}
|
||||
assetImage={assetImage}
|
||||
warning={warning}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<PageContainerFooter
|
||||
onCancel={() => onCancel()}
|
||||
cancelText={this.context.t('reject')}
|
||||
onSubmit={() => onSubmit()}
|
||||
submitText={this.context.t('confirm')}
|
||||
submitButtonType="confirm"
|
||||
disabled={disabled}
|
||||
>
|
||||
{unapprovedTxCount > 1 && (
|
||||
<a onClick={() => onCancelAll()}>
|
||||
{this.context.t('rejectTxsN', [unapprovedTxCount])}
|
||||
</a>
|
||||
)}
|
||||
</PageContainerFooter>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export { default } from './confirm-page-container.component'
|
||||
export { default as ConfirmPageContainerHeader } from './confirm-page-container-header'
|
||||
export { default as ConfirmDetailRow } from './confirm-detail-row'
|
||||
export {
|
||||
default as ConfirmPageContainerContent,
|
||||
ConfirmPageContainerSummary,
|
||||
ConfirmPageContainerError,
|
||||
} from './confirm-page-container-content'
|
|
@ -1,5 +0,0 @@
|
|||
@import './confirm-page-container-content/index';
|
||||
|
||||
@import './confirm-page-container-header/index';
|
||||
|
||||
@import './confirm-detail-row/index';
|
|
@ -1,66 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
const Tooltip = require('./tooltip')
|
||||
|
||||
CopyButton.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(CopyButton)
|
||||
|
||||
|
||||
inherits(CopyButton, Component)
|
||||
function CopyButton () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
// As parameters, accepts:
|
||||
// "value", which is the value to copy (mandatory)
|
||||
// "title", which is the text to show on hover (optional, defaults to 'Copy')
|
||||
CopyButton.prototype.render = function () {
|
||||
const props = this.props
|
||||
const state = this.state || {}
|
||||
|
||||
const value = props.value
|
||||
const copied = state.copied
|
||||
|
||||
const message = copied ? this.context.t('copiedButton') : props.title || this.context.t('copyButton')
|
||||
|
||||
return h('.copy-button', {
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
h(Tooltip, {
|
||||
title: message,
|
||||
}, [
|
||||
h('i.fa.fa-clipboard.cursor-pointer.color-orange', {
|
||||
style: {
|
||||
margin: '5px',
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
copyToClipboard(value)
|
||||
this.debounceRestore()
|
||||
},
|
||||
}),
|
||||
]),
|
||||
|
||||
])
|
||||
}
|
||||
|
||||
CopyButton.prototype.debounceRestore = function () {
|
||||
this.setState({ copied: true })
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({ copied: false })
|
||||
}, 850)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
|
||||
const Tooltip = require('./tooltip')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
Copyable.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(Copyable)
|
||||
|
||||
|
||||
inherits(Copyable, Component)
|
||||
function Copyable () {
|
||||
Component.call(this)
|
||||
this.state = {
|
||||
copied: false,
|
||||
}
|
||||
}
|
||||
|
||||
Copyable.prototype.render = function () {
|
||||
const props = this.props
|
||||
const state = this.state
|
||||
const { value, children } = props
|
||||
const { copied } = state
|
||||
|
||||
return h(Tooltip, {
|
||||
title: copied ? this.context.t('copiedExclamation') : this.context.t('copy'),
|
||||
position: 'bottom',
|
||||
}, h('span', {
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
copyToClipboard(value)
|
||||
this.debounceRestore()
|
||||
},
|
||||
}, children))
|
||||
}
|
||||
|
||||
Copyable.prototype.debounceRestore = function () {
|
||||
this.setState({ copied: true })
|
||||
clearTimeout(this.timeout)
|
||||
this.timeout = setTimeout(() => {
|
||||
this.setState({ copied: false })
|
||||
}, 850)
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import { ETH, GWEI } from '../../constants/common'
|
||||
|
||||
export default class CurrencyDisplay extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
displayValue: PropTypes.string,
|
||||
prefix: PropTypes.string,
|
||||
prefixComponent: PropTypes.node,
|
||||
style: PropTypes.object,
|
||||
// Used in container
|
||||
currency: PropTypes.oneOf([ETH]),
|
||||
denomination: PropTypes.oneOf([GWEI]),
|
||||
value: PropTypes.string,
|
||||
numberOfDecimals: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
hideLabel: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, displayValue, prefix, prefixComponent, style } = this.props
|
||||
const text = `${prefix || ''}${displayValue}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('currency-display-component', className)}
|
||||
style={style}
|
||||
title={text}
|
||||
>
|
||||
{ prefixComponent}
|
||||
<span className="currency-display-component__text">{ text }</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import { connect } from 'react-redux'
|
||||
import CurrencyDisplay from './currency-display.component'
|
||||
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { currentCurrency, conversionRate } } = state
|
||||
|
||||
return {
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { currentCurrency, conversionRate, ...restStateProps } = stateProps
|
||||
const {
|
||||
value,
|
||||
numberOfDecimals = 2,
|
||||
currency,
|
||||
denomination,
|
||||
hideLabel,
|
||||
...restOwnProps
|
||||
} = ownProps
|
||||
|
||||
const toCurrency = currency || currentCurrency
|
||||
const convertedValue = getValueFromWeiHex({
|
||||
value, toCurrency, conversionRate, numberOfDecimals, toDenomination: denomination,
|
||||
})
|
||||
const formattedValue = formatCurrency(convertedValue, toCurrency)
|
||||
const displayValue = hideLabel ? formattedValue : `${formattedValue} ${toCurrency.toUpperCase()}`
|
||||
|
||||
return {
|
||||
...restStateProps,
|
||||
...dispatchProps,
|
||||
...restOwnProps,
|
||||
displayValue,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(CurrencyDisplay)
|
|
@ -1 +0,0 @@
|
|||
export { default } from './currency-display.container'
|
|
@ -1,10 +0,0 @@
|
|||
.currency-display-component {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import CurrencyDisplay from '../currency-display.component'
|
||||
|
||||
describe('CurrencyDisplay Component', () => {
|
||||
it('should render text with a className', () => {
|
||||
const wrapper = shallow(<CurrencyDisplay
|
||||
displayValue="$123.45"
|
||||
className="currency-display"
|
||||
/>)
|
||||
|
||||
assert.ok(wrapper.hasClass('currency-display'))
|
||||
assert.equal(wrapper.text(), '$123.45')
|
||||
})
|
||||
|
||||
it('should render text with a prefix', () => {
|
||||
const wrapper = shallow(<CurrencyDisplay
|
||||
displayValue="$123.45"
|
||||
className="currency-display"
|
||||
prefix="-"
|
||||
/>)
|
||||
|
||||
assert.ok(wrapper.hasClass('currency-display'))
|
||||
assert.equal(wrapper.text(), '-$123.45')
|
||||
})
|
||||
})
|
|
@ -1,120 +0,0 @@
|
|||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
|
||||
let mapStateToProps, mergeProps
|
||||
|
||||
proxyquire('../currency-display.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md, mp) => {
|
||||
mapStateToProps = ms
|
||||
mergeProps = mp
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe('CurrencyDisplay container', () => {
|
||||
describe('mapStateToProps()', () => {
|
||||
it('should return the correct props', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
},
|
||||
}
|
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeProps()', () => {
|
||||
it('should return the correct props', () => {
|
||||
const mockStateProps = {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
}
|
||||
|
||||
const tests = [
|
||||
{
|
||||
props: {
|
||||
value: '0x2386f26fc10000',
|
||||
numberOfDecimals: 2,
|
||||
currency: 'usd',
|
||||
},
|
||||
result: {
|
||||
displayValue: '$2.80 USD',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
result: {
|
||||
displayValue: '$2.80 USD',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x1193461d01595930',
|
||||
currency: 'ETH',
|
||||
numberOfDecimals: 3,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1.266 ETH',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x1193461d01595930',
|
||||
currency: 'ETH',
|
||||
numberOfDecimals: 3,
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1.266',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x3b9aca00',
|
||||
currency: 'ETH',
|
||||
denomination: 'GWEI',
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x3b9aca00',
|
||||
currency: 'ETH',
|
||||
denomination: 'WEI',
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1000000000',
|
||||
},
|
||||
},
|
||||
{
|
||||
props: {
|
||||
value: '0x3b9aca00',
|
||||
currency: 'ETH',
|
||||
numberOfDecimals: 100,
|
||||
hideLabel: true,
|
||||
},
|
||||
result: {
|
||||
displayValue: '1e-9',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
tests.forEach(({ props, result }) => {
|
||||
assert.deepEqual(mergeProps(mockStateProps, {}, { ...props }), result)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,120 +0,0 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import UnitInput from '../unit-input'
|
||||
import CurrencyDisplay from '../currency-display'
|
||||
import { getValueFromWeiHex, getWeiHexFromDecimalValue } from '../../helpers/conversions.util'
|
||||
import { ETH } from '../../constants/common'
|
||||
|
||||
/**
|
||||
* Component that allows user to enter currency values as a number, and props receive a converted
|
||||
* hex value in WEI. props.value, used as a default or forced value, should be a hex value, which
|
||||
* gets converted into a decimal value depending on the currency (ETH or Fiat).
|
||||
*/
|
||||
export default class CurrencyInput extends PureComponent {
|
||||
static propTypes = {
|
||||
conversionRate: PropTypes.number,
|
||||
currentCurrency: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
suffix: PropTypes.string,
|
||||
useFiat: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const { value: hexValue } = props
|
||||
const decimalValue = hexValue ? this.getDecimalValue(props) : 0
|
||||
|
||||
this.state = {
|
||||
decimalValue,
|
||||
hexValue,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { value: prevPropsHexValue } = prevProps
|
||||
const { value: propsHexValue } = this.props
|
||||
const { hexValue: stateHexValue } = this.state
|
||||
|
||||
if (prevPropsHexValue !== propsHexValue && propsHexValue !== stateHexValue) {
|
||||
const decimalValue = this.getDecimalValue(this.props)
|
||||
this.setState({ hexValue: propsHexValue, decimalValue })
|
||||
}
|
||||
}
|
||||
|
||||
getDecimalValue (props) {
|
||||
const { value: hexValue, useFiat, currentCurrency, conversionRate } = props
|
||||
const decimalValueString = useFiat
|
||||
? getValueFromWeiHex({
|
||||
value: hexValue, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
|
||||
})
|
||||
: getValueFromWeiHex({
|
||||
value: hexValue, toCurrency: ETH, numberOfDecimals: 6,
|
||||
})
|
||||
|
||||
return Number(decimalValueString) || 0
|
||||
}
|
||||
|
||||
handleChange = decimalValue => {
|
||||
const { useFiat, currentCurrency: fromCurrency, conversionRate, onChange } = this.props
|
||||
|
||||
const hexValue = useFiat
|
||||
? getWeiHexFromDecimalValue({
|
||||
value: decimalValue, fromCurrency, conversionRate, invertConversionRate: true,
|
||||
})
|
||||
: getWeiHexFromDecimalValue({
|
||||
value: decimalValue, fromCurrency: ETH, fromDenomination: ETH, conversionRate,
|
||||
})
|
||||
|
||||
this.setState({ hexValue, decimalValue })
|
||||
onChange(hexValue)
|
||||
}
|
||||
|
||||
handleBlur = () => {
|
||||
this.props.onBlur(this.state.hexValue)
|
||||
}
|
||||
|
||||
renderConversionComponent () {
|
||||
const { useFiat, currentCurrency } = this.props
|
||||
const { hexValue } = this.state
|
||||
let currency, numberOfDecimals
|
||||
|
||||
if (useFiat) {
|
||||
// Display ETH
|
||||
currency = ETH
|
||||
numberOfDecimals = 6
|
||||
} else {
|
||||
// Display Fiat
|
||||
currency = currentCurrency
|
||||
numberOfDecimals = 2
|
||||
}
|
||||
|
||||
return (
|
||||
<CurrencyDisplay
|
||||
className="currency-input__conversion-component"
|
||||
currency={currency}
|
||||
value={hexValue}
|
||||
numberOfDecimals={numberOfDecimals}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { suffix, ...restProps } = this.props
|
||||
const { decimalValue } = this.state
|
||||
|
||||
return (
|
||||
<UnitInput
|
||||
{...restProps}
|
||||
suffix={suffix}
|
||||
onChange={this.handleChange}
|
||||
onBlur={this.handleBlur}
|
||||
value={decimalValue}
|
||||
>
|
||||
{ this.renderConversionComponent() }
|
||||
</UnitInput>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { connect } from 'react-redux'
|
||||
import CurrencyInput from './currency-input.component'
|
||||
import { ETH } from '../../constants/common'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask: { currentCurrency, conversionRate } } = state
|
||||
|
||||
return {
|
||||
currentCurrency,
|
||||
conversionRate,
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { currentCurrency } = stateProps
|
||||
const { useFiat } = ownProps
|
||||
const suffix = useFiat ? currentCurrency.toUpperCase() : ETH
|
||||
|
||||
return {
|
||||
...stateProps,
|
||||
...dispatchProps,
|
||||
...ownProps,
|
||||
suffix,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null, mergeProps)(CurrencyInput)
|
|
@ -1 +0,0 @@
|
|||
export { default } from './currency-input.container'
|
|
@ -1,7 +0,0 @@
|
|||
.currency-input {
|
||||
&__conversion-component {
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
padding-left: 1px;
|
||||
}
|
||||
}
|
|
@ -1,239 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow, mount } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import { Provider } from 'react-redux'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import CurrencyInput from '../currency-input.component'
|
||||
import UnitInput from '../../unit-input'
|
||||
import CurrencyDisplay from '../../currency-display'
|
||||
|
||||
describe('CurrencyInput Component', () => {
|
||||
describe('rendering', () => {
|
||||
it('should render properly without a suffix', () => {
|
||||
const wrapper = shallow(
|
||||
<CurrencyInput />
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.find(UnitInput).length, 1)
|
||||
})
|
||||
|
||||
it('should render properly with a suffix', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 231.06,
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<CurrencyInput
|
||||
suffix="ETH"
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
|
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
|
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 1)
|
||||
})
|
||||
|
||||
it('should render properly with an ETH value', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 231.06,
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<CurrencyInput
|
||||
value="de0b6b3a7640000"
|
||||
suffix="ETH"
|
||||
currentCurrency="usd"
|
||||
conversionRate={231.06}
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
|
||||
assert.equal(currencyInputInstance.state.decimalValue, 1)
|
||||
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
|
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
|
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'ETH')
|
||||
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
|
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD')
|
||||
})
|
||||
|
||||
it('should render properly with a fiat value', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 231.06,
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<CurrencyInput
|
||||
value="f602f2234d0ea"
|
||||
suffix="USD"
|
||||
useFiat
|
||||
currentCurrency="usd"
|
||||
conversionRate={231.06}
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
|
||||
assert.equal(currencyInputInstance.state.decimalValue, 1)
|
||||
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
|
||||
assert.equal(wrapper.find('.unit-input__suffix').length, 1)
|
||||
assert.equal(wrapper.find('.unit-input__suffix').text(), 'USD')
|
||||
assert.equal(wrapper.find('.unit-input__input').props().value, '1')
|
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH')
|
||||
})
|
||||
})
|
||||
|
||||
describe('handling actions', () => {
|
||||
const handleChangeSpy = sinon.spy()
|
||||
const handleBlurSpy = sinon.spy()
|
||||
|
||||
afterEach(() => {
|
||||
handleChangeSpy.resetHistory()
|
||||
handleBlurSpy.resetHistory()
|
||||
})
|
||||
|
||||
it('should call onChange and onBlur on input changes with the hex value for ETH', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 231.06,
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<CurrencyInput
|
||||
onChange={handleChangeSpy}
|
||||
onBlur={handleBlurSpy}
|
||||
suffix="ETH"
|
||||
currentCurrency="usd"
|
||||
conversionRate={231.06}
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(handleChangeSpy.callCount, 0)
|
||||
assert.equal(handleBlurSpy.callCount, 0)
|
||||
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
|
||||
assert.equal(currencyInputInstance.state.decimalValue, 0)
|
||||
assert.equal(currencyInputInstance.state.hexValue, undefined)
|
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$0.00 USD')
|
||||
const input = wrapper.find('input')
|
||||
assert.equal(input.props().value, 0)
|
||||
|
||||
input.simulate('change', { target: { value: 1 } })
|
||||
assert.equal(handleChangeSpy.callCount, 1)
|
||||
assert.ok(handleChangeSpy.calledWith('de0b6b3a7640000'))
|
||||
assert.equal(wrapper.find('.currency-display-component').text(), '$231.06 USD')
|
||||
assert.equal(currencyInputInstance.state.decimalValue, 1)
|
||||
assert.equal(currencyInputInstance.state.hexValue, 'de0b6b3a7640000')
|
||||
|
||||
assert.equal(handleBlurSpy.callCount, 0)
|
||||
input.simulate('blur')
|
||||
assert.equal(handleBlurSpy.callCount, 1)
|
||||
assert.ok(handleBlurSpy.calledWith('de0b6b3a7640000'))
|
||||
})
|
||||
|
||||
it('should call onChange and onBlur on input changes with the hex value for fiat', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 231.06,
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<CurrencyInput
|
||||
onChange={handleChangeSpy}
|
||||
onBlur={handleBlurSpy}
|
||||
suffix="USD"
|
||||
currentCurrency="usd"
|
||||
conversionRate={231.06}
|
||||
useFiat
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(handleChangeSpy.callCount, 0)
|
||||
assert.equal(handleBlurSpy.callCount, 0)
|
||||
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).at(0).instance()
|
||||
assert.equal(currencyInputInstance.state.decimalValue, 0)
|
||||
assert.equal(currencyInputInstance.state.hexValue, undefined)
|
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0 ETH')
|
||||
const input = wrapper.find('input')
|
||||
assert.equal(input.props().value, 0)
|
||||
|
||||
input.simulate('change', { target: { value: 1 } })
|
||||
assert.equal(handleChangeSpy.callCount, 1)
|
||||
assert.ok(handleChangeSpy.calledWith('f602f2234d0ea'))
|
||||
assert.equal(wrapper.find('.currency-display-component').text(), '0.004328 ETH')
|
||||
assert.equal(currencyInputInstance.state.decimalValue, 1)
|
||||
assert.equal(currencyInputInstance.state.hexValue, 'f602f2234d0ea')
|
||||
|
||||
assert.equal(handleBlurSpy.callCount, 0)
|
||||
input.simulate('blur')
|
||||
assert.equal(handleBlurSpy.callCount, 1)
|
||||
assert.ok(handleBlurSpy.calledWith('f602f2234d0ea'))
|
||||
})
|
||||
|
||||
it('should change the state and pass in a new decimalValue when props.value changes', () => {
|
||||
const mockStore = {
|
||||
metamask: {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 231.06,
|
||||
},
|
||||
}
|
||||
const store = configureMockStore()(mockStore)
|
||||
const wrapper = shallow(
|
||||
<Provider store={store}>
|
||||
<CurrencyInput
|
||||
onChange={handleChangeSpy}
|
||||
onBlur={handleBlurSpy}
|
||||
suffix="USD"
|
||||
currentCurrency="usd"
|
||||
conversionRate={231.06}
|
||||
useFiat
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
const currencyInputInstance = wrapper.find(CurrencyInput).dive()
|
||||
assert.equal(currencyInputInstance.state('decimalValue'), 0)
|
||||
assert.equal(currencyInputInstance.state('hexValue'), undefined)
|
||||
assert.equal(currencyInputInstance.find(UnitInput).props().value, 0)
|
||||
|
||||
currencyInputInstance.setProps({ value: '1ec05e43e72400' })
|
||||
currencyInputInstance.update()
|
||||
assert.equal(currencyInputInstance.state('decimalValue'), 2)
|
||||
assert.equal(currencyInputInstance.state('hexValue'), '1ec05e43e72400')
|
||||
assert.equal(currencyInputInstance.find(UnitInput).props().value, 2)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,55 +0,0 @@
|
|||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
|
||||
let mapStateToProps, mergeProps
|
||||
|
||||
proxyquire('../currency-input.container.js', {
|
||||
'react-redux': {
|
||||
connect: (ms, md, mp) => {
|
||||
mapStateToProps = ms
|
||||
mergeProps = mp
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe('CurrencyInput container', () => {
|
||||
describe('mapStateToProps()', () => {
|
||||
it('should return the correct props', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
},
|
||||
}
|
||||
|
||||
assert.deepEqual(mapStateToProps(mockState), {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeProps()', () => {
|
||||
it('should return the correct props', () => {
|
||||
const mockStateProps = {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
}
|
||||
const mockDispatchProps = {}
|
||||
|
||||
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, { useFiat: true }), {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
useFiat: true,
|
||||
suffix: 'USD',
|
||||
})
|
||||
|
||||
assert.deepEqual(mergeProps(mockStateProps, mockDispatchProps, {}), {
|
||||
conversionRate: 280.45,
|
||||
currentCurrency: 'usd',
|
||||
suffix: 'ETH',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,54 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const InputNumber = require('../input-number.js')
|
||||
// const GasSlider = require('./gas-slider.js')
|
||||
|
||||
module.exports = GasModalCard
|
||||
|
||||
inherits(GasModalCard, Component)
|
||||
function GasModalCard () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
GasModalCard.prototype.render = function () {
|
||||
const {
|
||||
// memo,
|
||||
onChange,
|
||||
unitLabel,
|
||||
value,
|
||||
min,
|
||||
// max,
|
||||
step,
|
||||
title,
|
||||
copy,
|
||||
} = this.props
|
||||
|
||||
return h('div.send-v2__gas-modal-card', [
|
||||
|
||||
h('div.send-v2__gas-modal-card__title', {}, title),
|
||||
|
||||
h('div.send-v2__gas-modal-card__copy', {}, copy),
|
||||
|
||||
h(InputNumber, {
|
||||
unitLabel,
|
||||
step,
|
||||
// max,
|
||||
min,
|
||||
placeholder: '0',
|
||||
value,
|
||||
onChange,
|
||||
}),
|
||||
|
||||
// h(GasSlider, {
|
||||
// value,
|
||||
// step,
|
||||
// max,
|
||||
// min,
|
||||
// onChange,
|
||||
// }),
|
||||
|
||||
])
|
||||
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
// const Component = require('react').Component
|
||||
// const h = require('react-hyperscript')
|
||||
// const inherits = require('util').inherits
|
||||
|
||||
// module.exports = GasSlider
|
||||
|
||||
// inherits(GasSlider, Component)
|
||||
// function GasSlider () {
|
||||
// Component.call(this)
|
||||
// }
|
||||
|
||||
// GasSlider.prototype.render = function () {
|
||||
// const {
|
||||
// memo,
|
||||
// identities,
|
||||
// onChange,
|
||||
// unitLabel,
|
||||
// value,
|
||||
// id,
|
||||
// step,
|
||||
// max,
|
||||
// min,
|
||||
// } = this.props
|
||||
|
||||
// return h('div.gas-slider', [
|
||||
|
||||
// h('input.gas-slider__input', {
|
||||
// type: 'range',
|
||||
// step,
|
||||
// max,
|
||||
// min,
|
||||
// value,
|
||||
// id: 'gasSlider',
|
||||
// onChange: event => onChange(event.target.value),
|
||||
// }, []),
|
||||
|
||||
// h('div.gas-slider__bar', [
|
||||
|
||||
// h('div.gas-slider__low'),
|
||||
|
||||
// h('div.gas-slider__mid'),
|
||||
|
||||
// h('div.gas-slider__high'),
|
||||
|
||||
// ]),
|
||||
|
||||
// ])
|
||||
|
||||
// }
|
||||
|
|
@ -1,374 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../actions')
|
||||
const GasModalCard = require('./gas-modal-card')
|
||||
import Button from '../button'
|
||||
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
|
||||
import {
|
||||
updateSendErrors,
|
||||
} from '../../ducks/send.duck'
|
||||
|
||||
const {
|
||||
MIN_GAS_PRICE_DEC,
|
||||
MIN_GAS_LIMIT_DEC,
|
||||
MIN_GAS_PRICE_GWEI,
|
||||
} = require('../send/send.constants')
|
||||
|
||||
const {
|
||||
isBalanceSufficient,
|
||||
} = require('../send/send.utils')
|
||||
|
||||
const {
|
||||
conversionUtil,
|
||||
multiplyCurrencies,
|
||||
conversionGreaterThan,
|
||||
conversionMax,
|
||||
subtractCurrencies,
|
||||
} = require('../../conversion-util')
|
||||
|
||||
const {
|
||||
getGasIsLoading,
|
||||
getForceGasMin,
|
||||
conversionRateSelector,
|
||||
getSendAmount,
|
||||
getSelectedToken,
|
||||
getSendFrom,
|
||||
getCurrentAccountWithSendEtherInfo,
|
||||
getSelectedTokenToFiatRate,
|
||||
getSendMaxModeState,
|
||||
} = require('../../selectors')
|
||||
|
||||
const {
|
||||
getGasPrice,
|
||||
getGasLimit,
|
||||
} = require('../send/send.selectors')
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const selectedToken = getSelectedToken(state)
|
||||
const currentAccount = getSendFrom(state) || getCurrentAccountWithSendEtherInfo(state)
|
||||
const conversionRate = conversionRateSelector(state)
|
||||
|
||||
return {
|
||||
gasPrice: getGasPrice(state),
|
||||
gasLimit: getGasLimit(state),
|
||||
gasIsLoading: getGasIsLoading(state),
|
||||
forceGasMin: getForceGasMin(state),
|
||||
conversionRate,
|
||||
amount: getSendAmount(state),
|
||||
maxModeOn: getSendMaxModeState(state),
|
||||
balance: currentAccount.balance,
|
||||
primaryCurrency: selectedToken && selectedToken.symbol,
|
||||
selectedToken,
|
||||
amountConversionRate: selectedToken ? getSelectedTokenToFiatRate(state) : conversionRate,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
hideModal: () => dispatch(actions.hideModal()),
|
||||
setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)),
|
||||
setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)),
|
||||
setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)),
|
||||
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
|
||||
updateSendErrors: error => dispatch(updateSendErrors(error)),
|
||||
}
|
||||
}
|
||||
|
||||
function getFreshState (props) {
|
||||
const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC
|
||||
const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC
|
||||
|
||||
const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 16,
|
||||
})
|
||||
|
||||
return {
|
||||
gasPrice,
|
||||
gasLimit,
|
||||
gasTotal,
|
||||
error: null,
|
||||
priceSigZeros: '',
|
||||
priceSigDec: '',
|
||||
}
|
||||
}
|
||||
|
||||
inherits(CustomizeGasModal, Component)
|
||||
function CustomizeGasModal (props) {
|
||||
Component.call(this)
|
||||
|
||||
const originalState = getFreshState(props)
|
||||
this.state = {
|
||||
...originalState,
|
||||
originalState,
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeGasModal.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
|
||||
|
||||
CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) {
|
||||
const currentState = getFreshState(this.props)
|
||||
const {
|
||||
gasPrice: currentGasPrice,
|
||||
gasLimit: currentGasLimit,
|
||||
} = currentState
|
||||
const newState = getFreshState(nextProps)
|
||||
const {
|
||||
gasPrice: newGasPrice,
|
||||
gasLimit: newGasLimit,
|
||||
gasTotal: newGasTotal,
|
||||
} = newState
|
||||
const gasPriceChanged = currentGasPrice !== newGasPrice
|
||||
const gasLimitChanged = currentGasLimit !== newGasLimit
|
||||
|
||||
if (gasPriceChanged) {
|
||||
this.setState({
|
||||
gasPrice: newGasPrice,
|
||||
gasTotal: newGasTotal,
|
||||
priceSigZeros: '',
|
||||
priceSigDec: '',
|
||||
})
|
||||
}
|
||||
if (gasLimitChanged) {
|
||||
this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal })
|
||||
}
|
||||
if (gasLimitChanged || gasPriceChanged) {
|
||||
this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal })
|
||||
}
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
|
||||
const {
|
||||
setGasPrice,
|
||||
setGasLimit,
|
||||
hideModal,
|
||||
setGasTotal,
|
||||
maxModeOn,
|
||||
selectedToken,
|
||||
balance,
|
||||
updateSendAmount,
|
||||
updateSendErrors,
|
||||
} = this.props
|
||||
|
||||
if (maxModeOn && !selectedToken) {
|
||||
const maxAmount = subtractCurrencies(
|
||||
ethUtil.addHexPrefix(balance),
|
||||
ethUtil.addHexPrefix(gasTotal),
|
||||
{ toNumericBase: 'hex' }
|
||||
)
|
||||
updateSendAmount(maxAmount)
|
||||
}
|
||||
|
||||
setGasPrice(ethUtil.addHexPrefix(gasPrice))
|
||||
setGasLimit(ethUtil.addHexPrefix(gasLimit))
|
||||
setGasTotal(ethUtil.addHexPrefix(gasTotal))
|
||||
updateSendErrors({ insufficientFunds: false })
|
||||
hideModal()
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.revert = function () {
|
||||
this.setState(this.state.originalState)
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) {
|
||||
const {
|
||||
amount,
|
||||
balance,
|
||||
selectedToken,
|
||||
amountConversionRate,
|
||||
conversionRate,
|
||||
maxModeOn,
|
||||
} = this.props
|
||||
|
||||
let error = null
|
||||
|
||||
const balanceIsSufficient = isBalanceSufficient({
|
||||
amount: selectedToken || maxModeOn ? '0' : amount,
|
||||
gasTotal,
|
||||
balance,
|
||||
selectedToken,
|
||||
amountConversionRate,
|
||||
conversionRate,
|
||||
})
|
||||
|
||||
if (!balanceIsSufficient) {
|
||||
error = this.context.t('balanceIsInsufficientGas')
|
||||
}
|
||||
|
||||
const gasLimitTooLow = gasLimit && conversionGreaterThan(
|
||||
{
|
||||
value: MIN_GAS_LIMIT_DEC,
|
||||
fromNumericBase: 'dec',
|
||||
conversionRate,
|
||||
},
|
||||
{
|
||||
value: gasLimit,
|
||||
fromNumericBase: 'hex',
|
||||
},
|
||||
)
|
||||
|
||||
if (gasLimitTooLow) {
|
||||
error = this.context.t('gasLimitTooLow')
|
||||
}
|
||||
|
||||
this.setState({ error })
|
||||
return error
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.convertAndSetGasLimit = function (newGasLimit) {
|
||||
const { gasPrice } = this.state
|
||||
|
||||
const gasLimit = conversionUtil(newGasLimit, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'hex',
|
||||
})
|
||||
|
||||
const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 16,
|
||||
})
|
||||
|
||||
this.validate({ gasTotal, gasLimit })
|
||||
|
||||
this.setState({ gasTotal, gasLimit })
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
|
||||
const { gasLimit } = this.state
|
||||
const sigZeros = String(newGasPrice).match(/^\d+[.]\d*?(0+)$/)
|
||||
const sigDec = String(newGasPrice).match(/^\d+([.])0*$/)
|
||||
|
||||
this.setState({
|
||||
priceSigZeros: sigZeros && sigZeros[1] || '',
|
||||
priceSigDec: sigDec && sigDec[1] || '',
|
||||
})
|
||||
|
||||
const gasPrice = conversionUtil(newGasPrice, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'hex',
|
||||
fromDenomination: 'GWEI',
|
||||
toDenomination: 'WEI',
|
||||
})
|
||||
|
||||
const gasTotal = multiplyCurrencies(gasLimit, gasPrice, {
|
||||
toNumericBase: 'hex',
|
||||
multiplicandBase: 16,
|
||||
multiplierBase: 16,
|
||||
})
|
||||
|
||||
this.validate({ gasTotal })
|
||||
|
||||
this.setState({ gasTotal, gasPrice })
|
||||
}
|
||||
|
||||
CustomizeGasModal.prototype.render = function () {
|
||||
const { hideModal, forceGasMin, gasIsLoading } = this.props
|
||||
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
|
||||
|
||||
let convertedGasPrice = conversionUtil(gasPrice, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromDenomination: 'WEI',
|
||||
toDenomination: 'GWEI',
|
||||
})
|
||||
|
||||
convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}`
|
||||
|
||||
let newGasPrice = gasPrice
|
||||
if (forceGasMin) {
|
||||
const convertedMinPrice = conversionUtil(forceGasMin, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
})
|
||||
convertedGasPrice = conversionMax(
|
||||
{ value: convertedMinPrice, fromNumericBase: 'dec' },
|
||||
{ value: convertedGasPrice, fromNumericBase: 'dec' }
|
||||
)
|
||||
newGasPrice = conversionMax(
|
||||
{ value: gasPrice, fromNumericBase: 'hex' },
|
||||
{ value: forceGasMin, fromNumericBase: 'hex' }
|
||||
)
|
||||
}
|
||||
|
||||
const convertedGasLimit = conversionUtil(gasLimit, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
})
|
||||
|
||||
return !gasIsLoading && h('div.send-v2__customize-gas', {}, [
|
||||
h('div.send-v2__customize-gas__content', {
|
||||
}, [
|
||||
h('div.send-v2__customize-gas__header', {}, [
|
||||
|
||||
h('div.send-v2__customize-gas__title', this.context.t('customGas')),
|
||||
|
||||
h('div.send-v2__customize-gas__close', {
|
||||
onClick: hideModal,
|
||||
}),
|
||||
|
||||
]),
|
||||
|
||||
h('div.send-v2__customize-gas__body', {}, [
|
||||
|
||||
h(GasModalCard, {
|
||||
value: convertedGasPrice,
|
||||
min: forceGasMin || MIN_GAS_PRICE_GWEI,
|
||||
step: 1,
|
||||
onChange: value => this.convertAndSetGasPrice(value),
|
||||
title: this.context.t('gasPrice'),
|
||||
copy: this.context.t('gasPriceCalculation'),
|
||||
gasIsLoading,
|
||||
}),
|
||||
|
||||
h(GasModalCard, {
|
||||
value: convertedGasLimit,
|
||||
min: 1,
|
||||
step: 1,
|
||||
onChange: value => this.convertAndSetGasLimit(value),
|
||||
title: this.context.t('gasLimit'),
|
||||
copy: this.context.t('gasLimitCalculation'),
|
||||
gasIsLoading,
|
||||
}),
|
||||
|
||||
]),
|
||||
|
||||
h('div.send-v2__customize-gas__footer', {}, [
|
||||
|
||||
error && h('div.send-v2__customize-gas__error-message', [
|
||||
error,
|
||||
]),
|
||||
|
||||
h('div.send-v2__customize-gas__revert', {
|
||||
onClick: () => this.revert(),
|
||||
}, [this.context.t('revert')]),
|
||||
|
||||
h('div.send-v2__customize-gas__buttons', [
|
||||
h(Button, {
|
||||
type: 'default',
|
||||
className: 'send-v2__customize-gas__cancel',
|
||||
onClick: this.props.hideModal,
|
||||
}, [this.context.t('cancel')]),
|
||||
h(Button, {
|
||||
type: 'primary',
|
||||
className: 'send-v2__customize-gas__save',
|
||||
onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
|
||||
disabled: error,
|
||||
}, [this.context.t('save')]),
|
||||
]),
|
||||
|
||||
]),
|
||||
|
||||
]),
|
||||
])
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
const { Component } = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const classnames = require('classnames')
|
||||
|
||||
class EditableLabel extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isEditing: false,
|
||||
value: props.defaultValue || '',
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit () {
|
||||
const { value } = this.state
|
||||
|
||||
if (value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
Promise.resolve(this.props.onSubmit(value))
|
||||
.then(() => this.setState({ isEditing: false }))
|
||||
}
|
||||
|
||||
saveIfEnter (event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
renderEditing () {
|
||||
const { value } = this.state
|
||||
|
||||
return ([
|
||||
h('input.large-input.editable-label__input', {
|
||||
type: 'text',
|
||||
required: true,
|
||||
value: this.state.value,
|
||||
onKeyPress: (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
this.handleSubmit()
|
||||
}
|
||||
},
|
||||
onChange: event => this.setState({ value: event.target.value }),
|
||||
className: classnames({ 'editable-label__input--error': value === '' }),
|
||||
}),
|
||||
h('div.editable-label__icon-wrapper', [
|
||||
h('i.fa.fa-check.editable-label__icon', {
|
||||
onClick: () => this.handleSubmit(),
|
||||
}),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
renderReadonly () {
|
||||
return ([
|
||||
h('div.editable-label__value', this.state.value),
|
||||
h('div.editable-label__icon-wrapper', [
|
||||
h('i.fa.fa-pencil.editable-label__icon', {
|
||||
onClick: () => this.setState({ isEditing: true }),
|
||||
}),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isEditing } = this.state
|
||||
const { className } = this.props
|
||||
|
||||
return (
|
||||
h('div.editable-label', { className: classnames(className) },
|
||||
isEditing
|
||||
? this.renderEditing()
|
||||
: this.renderReadonly()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EditableLabel.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
defaultValue: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
module.exports = EditableLabel
|
|
@ -1,181 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const extend = require('xtend')
|
||||
const debounce = require('debounce')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const ENS = require('ethjs-ens')
|
||||
const networkMap = require('ethjs-ens/lib/network-map.json')
|
||||
const ensRE = /.+\..+$/
|
||||
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
|
||||
const connect = require('react-redux').connect
|
||||
const ToAutoComplete = require('./send/to-autocomplete').default
|
||||
const log = require('loglevel')
|
||||
const { isValidENSAddress } = require('../util')
|
||||
|
||||
EnsInput.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(EnsInput)
|
||||
|
||||
|
||||
inherits(EnsInput, Component)
|
||||
function EnsInput () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
EnsInput.prototype.onChange = function (recipient) {
|
||||
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
|
||||
this.props.onChange({ toAddress: recipient })
|
||||
|
||||
if (!networkHasEnsSupport) return
|
||||
|
||||
if (recipient.match(ensRE) === null) {
|
||||
return this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: null,
|
||||
ensFailure: null,
|
||||
toError: null,
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loadingEns: true,
|
||||
})
|
||||
this.checkName(recipient)
|
||||
}
|
||||
|
||||
EnsInput.prototype.render = function () {
|
||||
const props = this.props
|
||||
const opts = extend(props, {
|
||||
list: 'addresses',
|
||||
onChange: this.onChange.bind(this),
|
||||
qrScanner: true,
|
||||
})
|
||||
return h('div', {
|
||||
style: { width: '100%', position: 'relative' },
|
||||
}, [
|
||||
h(ToAutoComplete, { ...opts }),
|
||||
this.ensIcon(),
|
||||
])
|
||||
}
|
||||
|
||||
EnsInput.prototype.componentDidMount = function () {
|
||||
const network = this.props.network
|
||||
const networkHasEnsSupport = getNetworkEnsSupport(network)
|
||||
this.setState({ ensResolution: ZERO_ADDRESS })
|
||||
|
||||
if (networkHasEnsSupport) {
|
||||
const provider = global.ethereumProvider
|
||||
this.ens = new ENS({ provider, network })
|
||||
this.checkName = debounce(this.lookupEnsName.bind(this), 200)
|
||||
}
|
||||
}
|
||||
|
||||
EnsInput.prototype.lookupEnsName = function (recipient) {
|
||||
const { ensResolution } = this.state
|
||||
|
||||
log.info(`ENS attempting to resolve name: ${recipient}`)
|
||||
this.ens.lookup(recipient.trim())
|
||||
.then((address) => {
|
||||
if (address === ZERO_ADDRESS) throw new Error(this.context.t('noAddressForName'))
|
||||
if (address !== ensResolution) {
|
||||
this.setState({
|
||||
loadingEns: false,
|
||||
ensResolution: address,
|
||||
nickname: recipient.trim(),
|
||||
hoverText: address + '\n' + this.context.t('clickCopy'),
|
||||
ensFailure: false,
|
||||
toError: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
const setStateObj = {
|
||||
loadingEns: false,
|
||||
ensResolution: recipient,
|
||||
ensFailure: true,
|
||||
toError: null,
|
||||
}
|
||||
if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') {
|
||||
setStateObj.hoverText = this.context.t('ensNameNotFound')
|
||||
setStateObj.toError = 'ensNameNotFound'
|
||||
setStateObj.ensFailure = false
|
||||
} else {
|
||||
log.error(reason)
|
||||
setStateObj.hoverText = reason.message
|
||||
}
|
||||
|
||||
return this.setState(setStateObj)
|
||||
})
|
||||
}
|
||||
|
||||
EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) {
|
||||
const state = this.state || {}
|
||||
const ensResolution = state.ensResolution
|
||||
// If an address is sent without a nickname, meaning not from ENS or from
|
||||
// the user's own accounts, a default of a one-space string is used.
|
||||
const nickname = state.nickname || ' '
|
||||
if (prevProps.network !== this.props.network) {
|
||||
const provider = global.ethereumProvider
|
||||
this.ens = new ENS({ provider, network: this.props.network })
|
||||
this.onChange(ensResolution)
|
||||
}
|
||||
if (prevState && ensResolution && this.props.onChange &&
|
||||
ensResolution !== prevState.ensResolution) {
|
||||
this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError })
|
||||
}
|
||||
}
|
||||
|
||||
EnsInput.prototype.ensIcon = function (recipient) {
|
||||
const { hoverText } = this.state || {}
|
||||
return h('span.#ensIcon', {
|
||||
title: hoverText,
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
left: '-25px',
|
||||
},
|
||||
}, this.ensIconContents(recipient))
|
||||
}
|
||||
|
||||
EnsInput.prototype.ensIconContents = function (recipient) {
|
||||
const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS }
|
||||
|
||||
if (toError) return
|
||||
|
||||
if (loadingEns) {
|
||||
return h('img', {
|
||||
src: 'images/loading.svg',
|
||||
style: {
|
||||
width: '30px',
|
||||
height: '30px',
|
||||
transform: 'translateY(-6px)',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (ensFailure) {
|
||||
return h('i.fa.fa-warning.fa-lg.warning')
|
||||
}
|
||||
|
||||
if (ensResolution && (ensResolution !== ZERO_ADDRESS)) {
|
||||
return h('i.fa.fa-check-circle.fa-lg.cursor-pointer', {
|
||||
style: { color: 'green' },
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
copyToClipboard(ensResolution)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getNetworkEnsSupport (network) {
|
||||
return Boolean(networkMap[network])
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const ErrorMessage = (props, context) => {
|
||||
const { errorMessage, errorKey } = props
|
||||
const error = errorKey ? context.t(errorKey) : errorMessage
|
||||
|
||||
return (
|
||||
<div className="error-message">
|
||||
<img
|
||||
src="/images/alert-red.svg"
|
||||
className="error-message__icon"
|
||||
/>
|
||||
<div className="error-message__text">
|
||||
{ `ALERT: ${error}` }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
ErrorMessage.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
errorKey: PropTypes.string,
|
||||
}
|
||||
|
||||
ErrorMessage.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
export default ErrorMessage
|
|
@ -1 +0,0 @@
|
|||
export { default } from './error-message.component'
|
|
@ -1,21 +0,0 @@
|
|||
.error-message {
|
||||
min-height: 32px;
|
||||
border: 1px solid $monzo;
|
||||
color: $monzo;
|
||||
background: lighten($monzo, 56%);
|
||||
border-radius: 4px;
|
||||
font-size: .75rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
&__text {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import ErrorMessage from '../error-message.component'
|
||||
|
||||
describe('ErrorMessage Component', () => {
|
||||
const t = key => `translate ${key}`
|
||||
|
||||
it('should render a message from props.errorMessage', () => {
|
||||
const wrapper = shallow(
|
||||
<ErrorMessage
|
||||
errorMessage="This is an error."
|
||||
/>,
|
||||
{ context: { t }}
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.find('.error-message').length, 1)
|
||||
assert.equal(wrapper.find('.error-message__icon').length, 1)
|
||||
assert.equal(wrapper.find('.error-message__text').text(), 'ALERT: This is an error.')
|
||||
})
|
||||
|
||||
it('should render a message translated from props.errorKey', () => {
|
||||
const wrapper = shallow(
|
||||
<ErrorMessage
|
||||
errorKey="testKey"
|
||||
/>,
|
||||
{ context: { t }}
|
||||
)
|
||||
|
||||
assert.ok(wrapper)
|
||||
assert.equal(wrapper.find('.error-message').length, 1)
|
||||
assert.equal(wrapper.find('.error-message__icon').length, 1)
|
||||
assert.equal(wrapper.find('.error-message__text').text(), 'ALERT: translate testKey')
|
||||
})
|
||||
})
|
|
@ -1,96 +0,0 @@
|
|||
const { Component } = require('react')
|
||||
const h = require('react-hyperscript')
|
||||
const { inherits } = require('util')
|
||||
const {
|
||||
formatBalance,
|
||||
generateBalanceObject,
|
||||
} = require('../util')
|
||||
const Tooltip = require('./tooltip.js')
|
||||
const FiatValue = require('./fiat-value.js')
|
||||
|
||||
module.exports = EthBalanceComponent
|
||||
|
||||
inherits(EthBalanceComponent, Component)
|
||||
function EthBalanceComponent () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
EthBalanceComponent.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { value, style, width, needsParse = true } = props
|
||||
|
||||
const formattedValue = value ? formatBalance(value, 6, needsParse) : '...'
|
||||
|
||||
return (
|
||||
|
||||
h('.ether-balance.ether-balance-amount', {
|
||||
style,
|
||||
}, [
|
||||
h('div', {
|
||||
style: {
|
||||
display: 'inline',
|
||||
width,
|
||||
},
|
||||
}, this.renderBalance(formattedValue)),
|
||||
])
|
||||
|
||||
)
|
||||
}
|
||||
EthBalanceComponent.prototype.renderBalance = function (value) {
|
||||
if (value === 'None') return value
|
||||
if (value === '...') return value
|
||||
|
||||
const {
|
||||
conversionRate,
|
||||
shorten,
|
||||
incoming,
|
||||
currentCurrency,
|
||||
hideTooltip,
|
||||
styleOveride = {},
|
||||
showFiat = true,
|
||||
} = this.props
|
||||
const { fontSize, color, fontFamily, lineHeight } = styleOveride
|
||||
|
||||
const { shortBalance, balance, label } = generateBalanceObject(value, shorten ? 1 : 3)
|
||||
const balanceToRender = shorten ? shortBalance : balance
|
||||
|
||||
const [ethNumber, ethSuffix] = value.split(' ')
|
||||
const containerProps = hideTooltip ? {} : {
|
||||
position: 'bottom',
|
||||
title: `${ethNumber} ${ethSuffix}`,
|
||||
}
|
||||
|
||||
return (
|
||||
h(hideTooltip ? 'div' : Tooltip,
|
||||
containerProps,
|
||||
h('div.flex-column', [
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
alignItems: 'flex-end',
|
||||
lineHeight: lineHeight || '13px',
|
||||
fontFamily: fontFamily || 'Montserrat Light',
|
||||
textRendering: 'geometricPrecision',
|
||||
},
|
||||
}, [
|
||||
h('div', {
|
||||
style: {
|
||||
width: '100%',
|
||||
textAlign: 'right',
|
||||
fontSize: fontSize || 'inherit',
|
||||
color: color || 'inherit',
|
||||
},
|
||||
}, incoming ? `+${balanceToRender}` : balanceToRender),
|
||||
h('div', {
|
||||
style: {
|
||||
color: color || '#AEAEAE',
|
||||
fontSize: fontSize || '12px',
|
||||
marginLeft: '5px',
|
||||
},
|
||||
}, label),
|
||||
]),
|
||||
|
||||
showFiat ? h(FiatValue, { value: this.props.value, conversionRate, currentCurrency }) : null,
|
||||
])
|
||||
)
|
||||
)
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
const { Component } = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const { exportAsFile } = require('../../util')
|
||||
|
||||
class ExportTextContainer extends Component {
|
||||
render () {
|
||||
const { text = '', filename = '' } = this.props
|
||||
const { t } = this.context
|
||||
|
||||
return (
|
||||
h('.export-text-container', [
|
||||
h('.export-text-container__text-container', [
|
||||
h('.export-text-container__text', text),
|
||||
]),
|
||||
h('.export-text-container__buttons-container', [
|
||||
h('.export-text-container__button.export-text-container__button--copy', {
|
||||
onClick: () => copyToClipboard(text),
|
||||
}, [
|
||||
h('img', { src: 'images/copy-to-clipboard.svg' }),
|
||||
h('.export-text-container__button-text', t('copyToClipboard')),
|
||||
]),
|
||||
h('.export-text-container__button', {
|
||||
onClick: () => exportAsFile(filename, text),
|
||||
}, [
|
||||
h('img', { src: 'images/download.svg' }),
|
||||
h('.export-text-container__button-text', t('saveAsCsvFile')),
|
||||
]),
|
||||
]),
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ExportTextContainer.propTypes = {
|
||||
text: PropTypes.string,
|
||||
filename: PropTypes.string,
|
||||
}
|
||||
|
||||
ExportTextContainer.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = ExportTextContainer
|
|
@ -1,2 +0,0 @@
|
|||
const ExportTextContainer = require('./export-text-container.component')
|
||||
module.exports = ExportTextContainer
|
|
@ -1,52 +0,0 @@
|
|||
.export-text-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 1px solid $alto;
|
||||
border-radius: 4px;
|
||||
font-weight: 400;
|
||||
|
||||
&__text-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
background: $alabaster;
|
||||
}
|
||||
|
||||
&__text {
|
||||
resize: none;
|
||||
border: none;
|
||||
background: $alabaster;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-top: 1px solid $alto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__button {
|
||||
padding: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: $curious-blue;
|
||||
|
||||
&--copy {
|
||||
border-right: 1px solid $alto;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-text {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const formatBalance = require('../util').formatBalance
|
||||
|
||||
module.exports = FiatValue
|
||||
|
||||
inherits(FiatValue, Component)
|
||||
function FiatValue () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
FiatValue.prototype.render = function () {
|
||||
const props = this.props
|
||||
const { conversionRate, currentCurrency, style } = props
|
||||
const renderedCurrency = currentCurrency || ''
|
||||
|
||||
const value = formatBalance(props.value, 6)
|
||||
|
||||
if (value === 'None') return value
|
||||
var fiatDisplayNumber, fiatTooltipNumber
|
||||
var splitBalance = value.split(' ')
|
||||
|
||||
if (conversionRate !== 0) {
|
||||
fiatTooltipNumber = Number(splitBalance[0]) * conversionRate
|
||||
fiatDisplayNumber = fiatTooltipNumber.toFixed(2)
|
||||
} else {
|
||||
fiatDisplayNumber = 'N/A'
|
||||
fiatTooltipNumber = 'Unknown'
|
||||
}
|
||||
|
||||
return fiatDisplay(fiatDisplayNumber, renderedCurrency.toUpperCase(), style)
|
||||
}
|
||||
|
||||
function fiatDisplay (fiatDisplayNumber, fiatSuffix, styleOveride = {}) {
|
||||
const { fontSize, color, fontFamily, lineHeight } = styleOveride
|
||||
|
||||
if (fiatDisplayNumber !== 'N/A') {
|
||||
return h('.flex-row', {
|
||||
style: {
|
||||
alignItems: 'flex-end',
|
||||
lineHeight: lineHeight || '13px',
|
||||
fontFamily: fontFamily || 'Montserrat Light',
|
||||
textRendering: 'geometricPrecision',
|
||||
},
|
||||
}, [
|
||||
h('div', {
|
||||
style: {
|
||||
width: '100%',
|
||||
textAlign: 'right',
|
||||
fontSize: fontSize || '12px',
|
||||
color: color || '#333333',
|
||||
},
|
||||
}, fiatDisplayNumber),
|
||||
h('div', {
|
||||
style: {
|
||||
color: color || '#AEAEAE',
|
||||
marginLeft: '5px',
|
||||
fontSize: fontSize || '12px',
|
||||
},
|
||||
}, fiatSuffix),
|
||||
])
|
||||
} else {
|
||||
return h('div')
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { hexToDecimal } from '../../helpers/conversions.util'
|
||||
|
||||
export default class HexToDecimal extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, value } = this.props
|
||||
const decimalValue = hexToDecimal(value)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{ decimalValue }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { default } from './hex-to-decimal.component'
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import HexToDecimal from '../hex-to-decimal.component'
|
||||
|
||||
describe('HexToDecimal Component', () => {
|
||||
it('should render a prefixed hex as a decimal with a className', () => {
|
||||
const wrapper = shallow(<HexToDecimal
|
||||
value="0x3039"
|
||||
className="hex-to-decimal"
|
||||
/>)
|
||||
|
||||
assert.ok(wrapper.hasClass('hex-to-decimal'))
|
||||
assert.equal(wrapper.text(), '12345')
|
||||
})
|
||||
|
||||
it('should render an unprefixed hex as a decimal with a className', () => {
|
||||
const wrapper = shallow(<HexToDecimal
|
||||
value="1A85"
|
||||
className="hex-to-decimal"
|
||||
/>)
|
||||
|
||||
assert.ok(wrapper.hasClass('hex-to-decimal'))
|
||||
assert.equal(wrapper.text(), '6789')
|
||||
})
|
||||
})
|
|
@ -1,59 +0,0 @@
|
|||
@import './app-header/index';
|
||||
|
||||
@import './add-token-button/index';
|
||||
|
||||
@import './button-group/index';
|
||||
|
||||
@import './card/index';
|
||||
|
||||
@import './confirm-page-container/index';
|
||||
|
||||
@import './currency-input/index';
|
||||
|
||||
@import './currency-display/index';
|
||||
|
||||
@import './error-message/index';
|
||||
|
||||
@import './export-text-container/index';
|
||||
|
||||
@import './info-box/index';
|
||||
|
||||
@import './menu-bar/index';
|
||||
|
||||
@import './modal/index';
|
||||
|
||||
@import './modals/index';
|
||||
|
||||
@import './network-display/index';
|
||||
|
||||
@import './page-container/index';
|
||||
|
||||
@import './pages/index';
|
||||
|
||||
@import './selected-account/index';
|
||||
|
||||
@import './sender-to-recipient/index';
|
||||
|
||||
@import './tabs/index';
|
||||
|
||||
@import './transaction-activity-log/index';
|
||||
|
||||
@import './transaction-breakdown/index';
|
||||
|
||||
@import './transaction-view/index';
|
||||
|
||||
@import './transaction-view-balance/index';
|
||||
|
||||
@import './transaction-list/index';
|
||||
|
||||
@import './transaction-list-item/index';
|
||||
|
||||
@import './transaction-list-item-details/index';
|
||||
|
||||
@import './transaction-status/index';
|
||||
|
||||
@import './app-header/index';
|
||||
|
||||
@import './sidebars/index';
|
||||
|
||||
@import './unit-input/index';
|
|
@ -1,2 +0,0 @@
|
|||
import InfoBox from './info-box.component'
|
||||
module.exports = InfoBox
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue