mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
fix conflicts
This commit is contained in:
commit
e743f44150
2
.babelrc
2
.babelrc
@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": [["env"], "react", "stage-0"],
|
||||
"presets": [["env", { "targets": { "browsers": [">0.25%", "not ie 11", "not op_mini all"] } } ], "react", "stage-0"],
|
||||
"plugins": ["transform-runtime", "transform-async-to-generator", "transform-class-properties"]
|
||||
}
|
||||
|
@ -796,7 +796,7 @@
|
||||
"message": "Testovací faucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "Komu: "
|
||||
"message": "Komu"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 na ETH přes ShapeShift",
|
||||
|
@ -384,7 +384,7 @@
|
||||
"infoHelp": {
|
||||
"message": "Info & Hilfe"
|
||||
},
|
||||
"insufficientFunds": {
|
||||
"insufficientFunds": {
|
||||
"message": "Nicht genügend Guthaben."
|
||||
},
|
||||
"insufficientTokens": {
|
||||
@ -572,7 +572,7 @@
|
||||
"description": "Wähle diesen Dateityp um damit einen Account zu importieren"
|
||||
},
|
||||
"privateKeyWarning": {
|
||||
"message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen."
|
||||
"message": "Warnung: Niemals jemanden deinen Private Key mitteilen. Jeder der im Besitz deines Private Keys ist, kann jegliches Guthaben deines Accounts stehlen."
|
||||
},
|
||||
"privateNetwork": {
|
||||
"message": "Privates Netzwerk"
|
||||
@ -775,7 +775,7 @@
|
||||
"message": "Testfaucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "An:"
|
||||
"message": "An"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 an ETH via ShapeShift",
|
||||
|
@ -454,6 +454,9 @@
|
||||
"hideTokenPrompt": {
|
||||
"message": "Hide Token?"
|
||||
},
|
||||
"history": {
|
||||
"message": "History"
|
||||
},
|
||||
"howToDeposit": {
|
||||
"message": "How would you like to deposit Ether?"
|
||||
},
|
||||
@ -654,7 +657,7 @@
|
||||
"message": "No transaction history."
|
||||
},
|
||||
"noTransactions": {
|
||||
"message": "No Transactions"
|
||||
"message": "You have no transactions"
|
||||
},
|
||||
"notFound": {
|
||||
"message": "Not Found"
|
||||
@ -705,6 +708,9 @@
|
||||
"pasteSeed": {
|
||||
"message": "Paste your seed phrase here!"
|
||||
},
|
||||
"pending": {
|
||||
"message": "pending"
|
||||
},
|
||||
"personalAddressDetected": {
|
||||
"message": "Personal address detected. Input the token contract address."
|
||||
},
|
||||
@ -733,6 +739,9 @@
|
||||
"qrCode": {
|
||||
"message": "Show QR Code"
|
||||
},
|
||||
"queue": {
|
||||
"message": "Queue"
|
||||
},
|
||||
"readdToken": {
|
||||
"message": "You can add this token back in the future by going go to “Add token” in your accounts options menu."
|
||||
},
|
||||
@ -900,6 +909,12 @@
|
||||
"sendTokens": {
|
||||
"message": "Send Tokens"
|
||||
},
|
||||
"sentEther": {
|
||||
"message": "sent ether"
|
||||
},
|
||||
"sentTokens": {
|
||||
"message": "sent tokens"
|
||||
},
|
||||
"separateEachWord": {
|
||||
"message": "Separate each word with a single space"
|
||||
},
|
||||
@ -913,6 +928,9 @@
|
||||
"orderOneHere": {
|
||||
"message": "Order a Trezor or Ledger and keep your funds in cold storage"
|
||||
},
|
||||
"outgoing": {
|
||||
"message": "Outgoing"
|
||||
},
|
||||
"searchTokens": {
|
||||
"message": "Search Tokens"
|
||||
},
|
||||
@ -976,6 +994,9 @@
|
||||
"sign": {
|
||||
"message": "Sign"
|
||||
},
|
||||
"signatureRequest": {
|
||||
"message": "Signature Request"
|
||||
},
|
||||
"signed": {
|
||||
"message": "Signed"
|
||||
},
|
||||
@ -1028,7 +1049,7 @@
|
||||
"message": "Test Faucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "To: "
|
||||
"message": "To"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 to ETH via ShapeShift",
|
||||
|
@ -772,7 +772,7 @@
|
||||
"message": "Probar Faucet"
|
||||
},
|
||||
"to": {
|
||||
"message": "Para:"
|
||||
"message": "Para"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 a ETH via ShapeShift",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -784,7 +784,7 @@
|
||||
"message": "Тестовый кран"
|
||||
},
|
||||
"to": {
|
||||
"message": "Получатель: "
|
||||
"message": "Получатель"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$1 в ETH через ShapeShift",
|
||||
|
@ -796,7 +796,7 @@
|
||||
"message": "சோதனை குழாய்"
|
||||
},
|
||||
"to": {
|
||||
"message": "பெறுநர்: "
|
||||
"message": "பெறுநர்"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "$ 1 முதல் ETH வரை வடிவம்",
|
||||
|
@ -796,7 +796,7 @@
|
||||
"message": "Test Musluğu"
|
||||
},
|
||||
"to": {
|
||||
"message": "Kime: "
|
||||
"message": "Kime"
|
||||
},
|
||||
"toETHviaShapeShift": {
|
||||
"message": "ShapeShift üstünden $1'dan ETH'e",
|
||||
|
@ -63,7 +63,6 @@
|
||||
"activeTab",
|
||||
"webRequest",
|
||||
"*://*.eth/",
|
||||
"*://*.test/",
|
||||
"notifications"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
|
@ -384,7 +384,7 @@ class PreferencesController {
|
||||
|
||||
/**
|
||||
* Returns an updated rpcList based on the passed url and the current list.
|
||||
* The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the
|
||||
* The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
|
||||
* end of the list. The current list is modified and returned as a promise.
|
||||
*
|
||||
* @param {string} _url The rpc url to add to the frequentRpcList.
|
||||
@ -400,7 +400,7 @@ class PreferencesController {
|
||||
if (_url !== 'http://localhost:8545') {
|
||||
rpcList.push(_url)
|
||||
}
|
||||
if (rpcList.length > 2) {
|
||||
if (rpcList.length > 3) {
|
||||
rpcList.shift()
|
||||
}
|
||||
return Promise.resolve(rpcList)
|
||||
|
@ -43,10 +43,24 @@ class AccountTracker {
|
||||
this._provider = opts.provider
|
||||
this._query = pify(new EthQuery(this._provider))
|
||||
this._blockTracker = opts.blockTracker
|
||||
// subscribe to latest block
|
||||
this._blockTracker.on('latest', this._updateForBlock.bind(this))
|
||||
// blockTracker.currentBlock may be null
|
||||
this._currentBlockNumber = this._blockTracker.getCurrentBlock()
|
||||
// bind function for easier listener syntax
|
||||
this._updateForBlock = this._updateForBlock.bind(this)
|
||||
}
|
||||
|
||||
start () {
|
||||
// remove first to avoid double add
|
||||
this._blockTracker.removeListener('latest', this._updateForBlock)
|
||||
// add listener
|
||||
this._blockTracker.addListener('latest', this._updateForBlock)
|
||||
// fetch account balances
|
||||
this._updateAccounts()
|
||||
}
|
||||
|
||||
stop () {
|
||||
// remove listener
|
||||
this._blockTracker.removeListener('latest', this._updateForBlock)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,7 +34,7 @@ module.exports = function (provider) {
|
||||
return { cancel: true }
|
||||
}
|
||||
|
||||
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
|
||||
extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']})
|
||||
|
||||
return {
|
||||
remove () {
|
||||
|
@ -67,6 +67,10 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
const initState = opts.initState || {}
|
||||
this.recordFirstTimeInfo(initState)
|
||||
|
||||
// this keeps track of how many "controllerStream" connections are open
|
||||
// the only thing that uses controller connections are open metamask UI instances
|
||||
this.activeControllerConnections = 0
|
||||
|
||||
// platform-specific api
|
||||
this.platform = opts.platform
|
||||
|
||||
@ -128,6 +132,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
provider: this.provider,
|
||||
blockTracker: this.blockTracker,
|
||||
})
|
||||
// start and stop polling for balances based on activeControllerConnections
|
||||
this.on('controllerConnectionChanged', (activeControllerConnections) => {
|
||||
if (activeControllerConnections > 0) {
|
||||
this.accountTracker.start()
|
||||
} else {
|
||||
this.accountTracker.stop()
|
||||
}
|
||||
})
|
||||
|
||||
// key mgmt
|
||||
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
|
||||
@ -138,19 +150,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
encryptor: opts.encryptor || undefined,
|
||||
})
|
||||
|
||||
// If only one account exists, make sure it is selected.
|
||||
this.keyringController.memStore.subscribe((state) => {
|
||||
const addresses = state.keyrings.reduce((res, keyring) => {
|
||||
return res.concat(keyring.accounts)
|
||||
}, [])
|
||||
if (addresses.length === 1) {
|
||||
const address = addresses[0]
|
||||
this.preferencesController.setSelectedAddress(address)
|
||||
}
|
||||
// ensure preferences + identities controller know about all addresses
|
||||
this.preferencesController.addAddresses(addresses)
|
||||
this.accountTracker.syncWithAddresses(addresses)
|
||||
})
|
||||
this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
|
||||
|
||||
// detect tokens controller
|
||||
this.detectTokensController = new DetectTokensController({
|
||||
@ -1211,11 +1211,19 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
setupControllerConnection (outStream) {
|
||||
const api = this.getApi()
|
||||
const dnode = Dnode(api)
|
||||
// report new active controller connection
|
||||
this.activeControllerConnections++
|
||||
this.emit('controllerConnectionChanged', this.activeControllerConnections)
|
||||
// connect dnode api to remote connection
|
||||
pump(
|
||||
outStream,
|
||||
dnode,
|
||||
outStream,
|
||||
(err) => {
|
||||
// report new active controller connection
|
||||
this.activeControllerConnections--
|
||||
this.emit('controllerConnectionChanged', this.activeControllerConnections)
|
||||
// report any error
|
||||
if (err) log.error(err)
|
||||
}
|
||||
)
|
||||
@ -1281,6 +1289,34 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a KeyringController update
|
||||
* @param {object} state the KC state
|
||||
* @return {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
async _onKeyringControllerUpdate (state) {
|
||||
const {isUnlocked, keyrings} = state
|
||||
const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), [])
|
||||
|
||||
if (!addresses.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure preferences + identities controller know about all addresses
|
||||
this.preferencesController.addAddresses(addresses)
|
||||
this.accountTracker.syncWithAddresses(addresses)
|
||||
|
||||
const wasLocked = !isUnlocked
|
||||
if (wasLocked) {
|
||||
const oldSelectedAddress = this.preferencesController.getSelectedAddress()
|
||||
if (!addresses.includes(oldSelectedAddress)) {
|
||||
const address = addresses[0]
|
||||
await this.preferencesController.setSelectedAddress(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A method for emitting the full MetaMask state to all registered listeners.
|
||||
* @private
|
||||
@ -1427,6 +1463,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Replace isClientOpen methods with `controllerConnectionChanged` events.
|
||||
/**
|
||||
* A method for recording whether the MetaMask user interface is open or not.
|
||||
* @private
|
||||
|
@ -350,11 +350,14 @@ module.exports = class AppBar extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
renderCommonRpc (rpcList, {rpcTarget}) {
|
||||
renderCommonRpc (rpcList, provider) {
|
||||
const {dispatch} = this.props
|
||||
const reversedRpcList = rpcList.slice().reverse()
|
||||
|
||||
return rpcList.map((rpc) => {
|
||||
if ((rpc === LOCALHOST_RPC_URL) || (rpc === rpcTarget)) {
|
||||
return reversedRpcList.map((rpc) => {
|
||||
const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
|
||||
|
||||
if ((rpc === LOCALHOST_RPC_URL) || currentRpcTarget) {
|
||||
return null
|
||||
} else {
|
||||
return h(DropdownMenuItem, {
|
||||
@ -364,7 +367,7 @@ module.exports = class AppBar extends Component {
|
||||
}, [
|
||||
h('i.fa.fa-question-circle.fa-lg.menu-icon'),
|
||||
rpc,
|
||||
rpcTarget === rpc
|
||||
currentRpcTarget
|
||||
? h('.check', '✓')
|
||||
: null,
|
||||
])
|
||||
|
@ -57,7 +57,11 @@
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"debug": true
|
||||
"browsers": [
|
||||
">0.25%",
|
||||
"not ie 11",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
],
|
||||
"stage-0"
|
||||
@ -185,6 +189,7 @@
|
||||
"react-dom": "^15.6.2",
|
||||
"react-hyperscript": "^3.0.0",
|
||||
"react-markdown": "^3.0.0",
|
||||
"react-media": "^1.8.0",
|
||||
"react-redux": "^5.0.5",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-select": "^1.0.0",
|
||||
|
@ -314,12 +314,12 @@ describe('Using MetaMask with an existing account', function () {
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
assert.equal(txValues.length, 1)
|
||||
assert.equal(await txValues[0].getText(), '1 ETH')
|
||||
assert.equal(await txValues[0].getText(), '-1 ETH')
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -225,19 +225,9 @@ describe('MetaMask', function () {
|
||||
await delay(regularDelayMs)
|
||||
}
|
||||
|
||||
await clickWordAndWait(words[0])
|
||||
await clickWordAndWait(words[1])
|
||||
await clickWordAndWait(words[2])
|
||||
await clickWordAndWait(words[3])
|
||||
await clickWordAndWait(words[4])
|
||||
await clickWordAndWait(words[5])
|
||||
await clickWordAndWait(words[6])
|
||||
await clickWordAndWait(words[7])
|
||||
await clickWordAndWait(words[8])
|
||||
await clickWordAndWait(words[9])
|
||||
await clickWordAndWait(words[10])
|
||||
await clickWordAndWait(words[11])
|
||||
|
||||
for (let i = 0; i < 12; i++) {
|
||||
await clickWordAndWait(words[i])
|
||||
}
|
||||
} catch (e) {
|
||||
if (count > 2) {
|
||||
throw e
|
||||
@ -414,12 +404,12 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /1\sETH/), 10000)
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-1\sETH/), 10000)
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -457,14 +447,11 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /3\sETH/), 10000)
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-3\sETH/), 10000)
|
||||
})
|
||||
})
|
||||
|
||||
@ -487,9 +474,9 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.xpath(`//span[contains(text(), 'Contract Deployment')]`))
|
||||
const txListItem = await findElement(driver, By.xpath(`//div[contains(text(), 'Contract Deployment')]`))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('displays the contract creation data', async () => {
|
||||
@ -511,13 +498,15 @@ describe('MetaMask', function () {
|
||||
it('confirms a deploy contract transaction', async () => {
|
||||
const confirmButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Confirm')]`))
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 3
|
||||
}, 10000)
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
assert.equal(await txAccounts[0].getText(), 'Contract Deployment')
|
||||
const txAction = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txAction[0], /Contract\sDeployment/), 10000)
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
@ -538,9 +527,9 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await findElements(driver, By.css('.tx-list-pending-item-container'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /4\sETH/), 10000)
|
||||
await findElements(driver, By.css('.transaction-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-4\sETH/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -568,15 +557,17 @@ describe('MetaMask', function () {
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 4
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /4\sETH/), 10000)
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-4\sETH/), 10000)
|
||||
|
||||
const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
const firstTxAddress = await txAccounts[0].getText()
|
||||
assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
|
||||
// const txAccounts = await findElements(driver, By.css('.tx-list-account'))
|
||||
// const firstTxAddress = await txAccounts[0].getText()
|
||||
// assert(firstTxAddress.match(/^0x\w{8}\.{3}\w{4}$/))
|
||||
})
|
||||
|
||||
it('calls and confirms a contract method where ETH is received', async () => {
|
||||
@ -590,7 +581,7 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txListItem = await findElement(driver, By.css('.tx-list-item'))
|
||||
const txListItem = await findElement(driver, By.css('.transaction-list-item'))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -598,18 +589,20 @@ describe('MetaMask', function () {
|
||||
await confirmButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 5
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElement(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /0\sETH/), 10000)
|
||||
const txValues = await findElement(driver, By.css('.transaction-list-item__amount--secondary'))
|
||||
await driver.wait(until.elementTextMatches(txValues, /-0\sETH/), 10000)
|
||||
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await driver.switchTo().window(extension)
|
||||
})
|
||||
|
||||
it('renders the correct ETH balance', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const balance = await findElement(driver, By.css('.transaction-view-balance__primary-balance'))
|
||||
await delay(regularDelayMs)
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
await driver.wait(until.elementTextMatches(balance, /^92.*ETH.*$/), 10000)
|
||||
@ -654,18 +647,17 @@ describe('MetaMask', function () {
|
||||
await closeAllWindowHandlesExcept(driver, [extension, dapp])
|
||||
await delay(regularDelayMs)
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
await delay(largeDelayMs)
|
||||
})
|
||||
|
||||
it('clicks on the Add Token button', async () => {
|
||||
const addToken = await findElement(driver, By.xpath(`//button[contains(text(), 'Add Token')]`))
|
||||
const addToken = await driver.findElement(By.css('.wallet-view__add-token-button'))
|
||||
await addToken.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
|
||||
it('picks the newly created Test token', async () => {
|
||||
const addCustomToken = await findElement(driver, By.xpath("//div[contains(text(), 'Custom Token')]"))
|
||||
const addCustomToken = await findElement(driver, By.xpath("//li[contains(text(), 'Custom Token')]"))
|
||||
await addCustomToken.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -683,7 +675,7 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('renders the balance for the new token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const balance = await findElement(driver, By.css('.transaction-view-balance .transaction-view-balance__token-balance'))
|
||||
await driver.wait(until.elementTextMatches(balance, /^100\s*TST\s*$/))
|
||||
const tokenAmount = await balance.getText()
|
||||
assert.ok(/^100\s*TST\s*$/.test(tokenAmount))
|
||||
@ -752,21 +744,25 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
const transactions = await findElements(driver, By.css('.transaction-list-item'))
|
||||
assert.equal(transactions.length, 1)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
assert.equal(txValues.length, 1)
|
||||
|
||||
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /50\sTST/), 10000)
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-50\sTST/), 10000)
|
||||
}
|
||||
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed|Failed/), 10000)
|
||||
assert.equal(await tx.getText(), 'Confirmed')
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 1
|
||||
}, 10000)
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
const tx = await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken|Failed/), 10000)
|
||||
assert.equal(await tx.getText(), 'Sent Tokens')
|
||||
})
|
||||
})
|
||||
|
||||
@ -789,9 +785,9 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(largeDelayMs)
|
||||
|
||||
await findElements(driver, By.css('.tx-list-pending-item-container'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /7\sTST/), 10000)
|
||||
await findElements(driver, By.css('.transaction-list__pending-transactions'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/), 10000)
|
||||
await txListValue.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
@ -838,25 +834,28 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const transactions = await findElements(driver, By.css('.tx-list-item'))
|
||||
assert.equal(transactions.length, 2)
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 2
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Sent\sToken/))
|
||||
|
||||
const walletBalance = await findElement(driver, By.css('.wallet-balance'))
|
||||
await walletBalance.click()
|
||||
|
||||
const tokenListItems = await findElements(driver, By.css('.token-list-item'))
|
||||
await tokenListItems[0].click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// test cancelled on firefox until https://github.com/mozilla/geckodriver/issues/906 is resolved,
|
||||
// or possibly until we use latest version of firefox in the tests
|
||||
if (process.env.SELENIUM_BROWSER !== 'firefox') {
|
||||
const tokenBalanceAmount = await findElement(driver, By.css('.token-balance__amount'))
|
||||
assert.equal(await tokenBalanceAmount.getText(), '43')
|
||||
const tokenBalanceAmount = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
|
||||
assert.equal(await tokenBalanceAmount.getText(), '43 TST')
|
||||
}
|
||||
})
|
||||
})
|
||||
@ -880,9 +879,14 @@ describe('MetaMask', function () {
|
||||
await driver.switchTo().window(extension)
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.tx-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /0\sETH/))
|
||||
driver.wait(async () => {
|
||||
const pendingTxes = await findElements(driver, By.css('.transaction-list__pending-transactions .transaction-list-item'))
|
||||
return pendingTxes.length === 1
|
||||
}, 10000)
|
||||
|
||||
const [txListItem] = await findElements(driver, By.css('.transaction-list-item'))
|
||||
const [txListValue] = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txListValue, /-7\sTST/))
|
||||
await txListItem.click()
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
@ -953,10 +957,15 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('finds the transaction in the transactions list', async function () {
|
||||
const txValues = await findElements(driver, By.css('.tx-list-value'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /0\sETH/))
|
||||
const txStatuses = await findElements(driver, By.css('.tx-list-status'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Confirmed/))
|
||||
driver.wait(async () => {
|
||||
const confirmedTxes = await findElements(driver, By.css('.transaction-list__completed-transactions .transaction-list-item'))
|
||||
return confirmedTxes.length === 3
|
||||
}, 10000)
|
||||
|
||||
const txValues = await findElements(driver, By.css('.transaction-list-item__amount--primary'))
|
||||
await driver.wait(until.elementTextMatches(txValues[0], /-7\sTST/))
|
||||
const txStatuses = await findElements(driver, By.css('.transaction-list-item__action'))
|
||||
await driver.wait(until.elementTextMatches(txStatuses[0], /Approve/))
|
||||
})
|
||||
})
|
||||
|
||||
@ -1006,9 +1015,69 @@ describe('MetaMask', function () {
|
||||
})
|
||||
|
||||
it('renders the balance for the chosen token', async () => {
|
||||
const balance = await findElement(driver, By.css('.tx-view .balance-display .token-amount'))
|
||||
const balance = await findElement(driver, By.css('.transaction-view-balance__token-balance'))
|
||||
await driver.wait(until.elementTextMatches(balance, /0\sBAT/))
|
||||
await delay(regularDelayMs)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Stores custom RPC history', () => {
|
||||
const customRpcUrls = [
|
||||
'https://mainnet.infura.io/1',
|
||||
'https://mainnet.infura.io/2',
|
||||
'https://mainnet.infura.io/3',
|
||||
'https://mainnet.infura.io/4',
|
||||
]
|
||||
|
||||
customRpcUrls.forEach(customRpcUrl => {
|
||||
it('creates custom RPC: ' + customRpcUrl, async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Custom RPC')]`))
|
||||
await customRpcButton.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcInput = await findElement(driver, By.css('input[placeholder="New RPC URL"]'))
|
||||
await customRpcInput.clear()
|
||||
await customRpcInput.sendKeys(customRpcUrl)
|
||||
|
||||
const customRpcSave = await findElement(driver, By.css('.settings__rpc-save-button'))
|
||||
await customRpcSave.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
})
|
||||
|
||||
it('selects another provider', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
const customRpcButton = await findElement(driver, By.xpath(`//span[contains(text(), 'Main Ethereum Network')]`))
|
||||
await customRpcButton.click()
|
||||
await delay(largeDelayMs * 2)
|
||||
})
|
||||
|
||||
it('finds 3 recent RPCs in history', async () => {
|
||||
const networkDropdown = await findElement(driver, By.css('.network-name'))
|
||||
await networkDropdown.click()
|
||||
await delay(regularDelayMs)
|
||||
|
||||
// oldest selected RPC is not found
|
||||
await assertElementNotPresent(webdriver, driver, By.xpath(`//span[contains(text(), '${customRpcUrls[0]}')]`))
|
||||
|
||||
// only recent 3 are found and in correct order (most recent at the top)
|
||||
const customRpcs = await findElements(driver, By.xpath(`//span[contains(text(), 'https://mainnet.infura.io/')]`))
|
||||
|
||||
assert.equal(customRpcs.length, 3)
|
||||
|
||||
for (let i = 0; i < customRpcs.length; i++) {
|
||||
const linkText = await customRpcs[i].getText()
|
||||
const rpcUrl = customRpcUrls[customRpcUrls.length - i - 1]
|
||||
|
||||
assert.notEqual(linkText.indexOf(rpcUrl), -1)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -86,7 +86,7 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
$('button.btn-primary.btn--large')[0].click()
|
||||
|
||||
// Verify added token image
|
||||
let heroBalance = await queryAsync($, '.hero-balance')
|
||||
let heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
|
||||
assert.ok(heroBalance, 'rendered hero balance')
|
||||
assert.ok(tokenImageUrl.indexOf(heroBalance.find('img').attr('src')) > -1, 'token added')
|
||||
|
||||
@ -134,7 +134,7 @@ async function runAddTokenFlowTest (assert, done) {
|
||||
// $('button.btn-primary--lg')[0].click()
|
||||
|
||||
// Verify added token image
|
||||
heroBalance = await queryAsync($, '.hero-balance')
|
||||
heroBalance = await queryAsync($, '.transaction-view-balance__balance-container')
|
||||
assert.ok(heroBalance, 'rendered hero balance')
|
||||
assert.ok(heroBalance.find('.identicon')[0], 'token added')
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ async function runConfirmSigRequestsTest (assert, done) {
|
||||
selectState.val('confirm sig requests')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const pendingRequestItem = $.find('.tx-list-item.tx-list-pending-item-container.tx-list-clickable')
|
||||
const pendingRequestItem = $.find('.transaction-list-item')
|
||||
|
||||
if (pendingRequestItem[0]) {
|
||||
pendingRequestItem[0].click()
|
||||
|
@ -22,8 +22,8 @@ async function runCurrencyLocalizationTest (assert, done) {
|
||||
await timeout(1000)
|
||||
reactTriggerChange(selectState[0])
|
||||
await timeout(1000)
|
||||
const txView = await queryAsync($, '.tx-view')
|
||||
const heroBalance = await findAsync($(txView), '.hero-balance')
|
||||
const fiatAmount = await findAsync($(heroBalance), '.fiat-amount')
|
||||
assert.equal(fiatAmount[0].textContent, '₱102,707.97')
|
||||
const txView = await queryAsync($, '.transaction-view')
|
||||
const heroBalance = await findAsync($(txView), '.transaction-view-balance__balance')
|
||||
const fiatAmount = await findAsync($(heroBalance), '.transaction-view-balance__secondary-balance')
|
||||
assert.equal(fiatAmount[0].textContent, '₱102,707.97 PHP')
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ async function runSendFlowTest (assert, done) {
|
||||
selectState.val('send new ui')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const sendScreenButton = await queryAsync($, 'button.btn-primary.hero-balance-button')
|
||||
const sendScreenButton = await queryAsync($, 'button.btn-primary.transaction-view-balance__button')
|
||||
assert.ok(sendScreenButton[1], 'send screen button present')
|
||||
sendScreenButton[1].click()
|
||||
|
||||
@ -124,10 +124,10 @@ async function runSendFlowTest (assert, done) {
|
||||
selectState.val('send edit')
|
||||
reactTriggerChange(selectState[0])
|
||||
|
||||
const confirmFromName = (await queryAsync($, '.sender-to-recipient__sender-name')).first()
|
||||
const confirmFromName = (await queryAsync($, '.sender-to-recipient__name')).first()
|
||||
assert.equal(confirmFromName[0].textContent, 'Send Account 4', 'confirm screen should show correct from name')
|
||||
|
||||
const confirmToName = (await queryAsync($, '.sender-to-recipient__recipient-name')).last()
|
||||
const confirmToName = (await queryAsync($, '.sender-to-recipient__name')).last()
|
||||
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
|
||||
|
||||
const confirmScreenRowFiats = await queryAsync($, '.confirm-detail-row__fiat')
|
||||
|
@ -29,26 +29,23 @@ async function runTxListItemsTest (assert, done) {
|
||||
assert.ok(metamaskLogo[0], 'metamask logo present')
|
||||
metamaskLogo[0].click()
|
||||
|
||||
const txListItems = await queryAsync($, '.tx-list-item')
|
||||
const txListItems = await queryAsync($, '.transaction-list-item')
|
||||
assert.equal(txListItems.length, 8, 'all tx list items are rendered')
|
||||
|
||||
const unapprovedTx = txListItems[0]
|
||||
assert.equal($(unapprovedTx).hasClass('tx-list-pending-item-container'), true, 'unapprovedTx has the correct class')
|
||||
|
||||
const retryTx = txListItems[1]
|
||||
const retryTxLink = await findAsync($(retryTx), '.tx-list-item-retry-container span')
|
||||
const retryTxLink = await findAsync($(retryTx), '.transaction-list-item__retry')
|
||||
assert.equal(retryTxLink[0].textContent, 'Taking too long? Increase the gas price on your transaction', 'retryTx has expected link')
|
||||
|
||||
const approvedTx = txListItems[2]
|
||||
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.tx-list-status')
|
||||
assert.equal(approvedTxRenderedStatus[0].textContent, 'Approved', 'approvedTx has correct label')
|
||||
const approvedTxRenderedStatus = await findAsync($(approvedTx), '.transaction-list-item__status')
|
||||
assert.equal(approvedTxRenderedStatus[0].textContent, 'pending', 'approvedTx has correct label')
|
||||
|
||||
const unapprovedMsg = txListItems[3]
|
||||
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.tx-list-account')
|
||||
const unapprovedMsgDescription = await findAsync($(unapprovedMsg), '.transaction-list-item__action')
|
||||
assert.equal(unapprovedMsgDescription[0].textContent, 'Signature Request', 'unapprovedMsg has correct description')
|
||||
|
||||
const failedTx = txListItems[4]
|
||||
const failedTxRenderedStatus = await findAsync($(failedTx), '.tx-list-status')
|
||||
const failedTxRenderedStatus = await findAsync($(failedTx), '.transaction-list-item__status')
|
||||
assert.equal(failedTxRenderedStatus[0].textContent, 'Failed', 'failedTx has correct label')
|
||||
|
||||
const shapeShiftTx = txListItems[5]
|
||||
@ -56,10 +53,10 @@ async function runTxListItemsTest (assert, done) {
|
||||
assert.equal(shapeShiftTxStatus[0].textContent, 'No deposits received', 'shapeShiftTx has correct status')
|
||||
|
||||
const confirmedTokenTx = txListItems[6]
|
||||
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.tx-list-account')
|
||||
assert.equal(confirmedTokenTxAddress[0].textContent, '0xE7884118...81a9', 'confirmedTokenTx has correct address')
|
||||
const confirmedTokenTxAddress = await findAsync($(confirmedTokenTx), '.transaction-list-item__status')
|
||||
assert.equal(confirmedTokenTxAddress[0].textContent, 'Confirmed', 'confirmedTokenTx has correct address')
|
||||
|
||||
const rejectedTx = txListItems[7]
|
||||
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.tx-list-status')
|
||||
const rejectedTxRenderedStatus = await findAsync($(rejectedTx), '.transaction-list-item__status')
|
||||
assert.equal(rejectedTxRenderedStatus[0].textContent, 'Rejected', 'rejectedTx has correct label')
|
||||
}
|
||||
|
@ -814,6 +814,77 @@ describe('MetaMaskController', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('#_onKeyringControllerUpdate', function () {
|
||||
it('should do nothing if there are no keyrings in state', async function () {
|
||||
const addAddresses = sinon.fake()
|
||||
const syncWithAddresses = sinon.fake()
|
||||
sandbox.replace(metamaskController, 'preferencesController', {
|
||||
addAddresses,
|
||||
})
|
||||
sandbox.replace(metamaskController, 'accountTracker', {
|
||||
syncWithAddresses,
|
||||
})
|
||||
|
||||
const oldState = metamaskController.getState()
|
||||
await metamaskController._onKeyringControllerUpdate({keyrings: []})
|
||||
|
||||
assert.ok(addAddresses.notCalled)
|
||||
assert.ok(syncWithAddresses.notCalled)
|
||||
assert.deepEqual(metamaskController.getState(), oldState)
|
||||
})
|
||||
|
||||
it('should update selected address if keyrings was locked', async function () {
|
||||
const addAddresses = sinon.fake()
|
||||
const getSelectedAddress = sinon.fake.returns('0x42')
|
||||
const setSelectedAddress = sinon.fake()
|
||||
const syncWithAddresses = sinon.fake()
|
||||
sandbox.replace(metamaskController, 'preferencesController', {
|
||||
addAddresses,
|
||||
getSelectedAddress,
|
||||
setSelectedAddress,
|
||||
})
|
||||
sandbox.replace(metamaskController, 'accountTracker', {
|
||||
syncWithAddresses,
|
||||
})
|
||||
|
||||
const oldState = metamaskController.getState()
|
||||
await metamaskController._onKeyringControllerUpdate({
|
||||
isUnlocked: false,
|
||||
keyrings: [{
|
||||
accounts: ['0x1', '0x2'],
|
||||
}],
|
||||
})
|
||||
|
||||
assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
|
||||
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
|
||||
assert.deepEqual(setSelectedAddress.args, [['0x1']])
|
||||
assert.deepEqual(metamaskController.getState(), oldState)
|
||||
})
|
||||
|
||||
it('should NOT update selected address if already unlocked', async function () {
|
||||
const addAddresses = sinon.fake()
|
||||
const syncWithAddresses = sinon.fake()
|
||||
sandbox.replace(metamaskController, 'preferencesController', {
|
||||
addAddresses,
|
||||
})
|
||||
sandbox.replace(metamaskController, 'accountTracker', {
|
||||
syncWithAddresses,
|
||||
})
|
||||
|
||||
const oldState = metamaskController.getState()
|
||||
await metamaskController._onKeyringControllerUpdate({
|
||||
isUnlocked: true,
|
||||
keyrings: [{
|
||||
accounts: ['0x1', '0x2'],
|
||||
}],
|
||||
})
|
||||
|
||||
assert.deepEqual(addAddresses.args, [[['0x1', '0x2']]])
|
||||
assert.deepEqual(syncWithAddresses.args, [[['0x1', '0x2']]])
|
||||
assert.deepEqual(metamaskController.getState(), oldState)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
function deferredPromise () {
|
||||
|
@ -1,33 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
// Main Views
|
||||
const TxView = require('./components/tx-view')
|
||||
const WalletView = require('./components/wallet-view')
|
||||
|
||||
module.exports = AccountAndTransactionDetails
|
||||
|
||||
inherits(AccountAndTransactionDetails, Component)
|
||||
function AccountAndTransactionDetails () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
AccountAndTransactionDetails.prototype.render = function () {
|
||||
return h('div.account-and-transaction-details', [
|
||||
// wallet
|
||||
h(WalletView, {
|
||||
style: {
|
||||
},
|
||||
responsiveDisplayClassname: '.lap-visible',
|
||||
}, [
|
||||
]),
|
||||
|
||||
// transaction
|
||||
h(TxView, {
|
||||
style: {
|
||||
},
|
||||
}, [
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ const ConfirmTransaction = require('./components/pages/confirm-transaction')
|
||||
const WalletView = require('./components/wallet-view')
|
||||
|
||||
// other views
|
||||
const Home = require('./components/pages/home')
|
||||
import Home from './components/pages/home'
|
||||
const Authenticated = require('./components/pages/authenticated')
|
||||
const Initialized = require('./components/pages/initialized')
|
||||
const Settings = require('./components/pages/settings')
|
||||
@ -185,7 +185,7 @@ class App extends Component {
|
||||
}, [
|
||||
// A second instance of Walletview is used for non-mobile viewports
|
||||
this.props.sidebarOpen ? h(WalletView, {
|
||||
responsiveDisplayClassname: '.sidebar',
|
||||
responsiveDisplayClassname: 'sidebar',
|
||||
style: {},
|
||||
}) : undefined,
|
||||
|
||||
|
@ -4,8 +4,7 @@ const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const TokenBalance = require('./token-balance')
|
||||
const Identicon = require('./identicon')
|
||||
const currencyFormatter = require('currency-formatter')
|
||||
const currencies = require('currency-formatter/currencies')
|
||||
import CurrencyDisplay from './currency-display'
|
||||
|
||||
const { formatBalance, generateBalanceObject } = require('../util')
|
||||
|
||||
@ -84,38 +83,12 @@ BalanceComponent.prototype.renderBalance = function () {
|
||||
style: {},
|
||||
}, this.getTokenBalance(formattedBalance, shorten)),
|
||||
|
||||
showFiat ? this.renderFiatValue(formattedBalance) : null,
|
||||
showFiat && h(CurrencyDisplay, {
|
||||
value: balanceValue,
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.renderFiatValue = function (formattedBalance) {
|
||||
|
||||
const { conversionRate, currentCurrency } = this.props
|
||||
|
||||
const fiatDisplayNumber = this.getFiatDisplayNumber(formattedBalance, conversionRate)
|
||||
|
||||
const fiatPrefix = currentCurrency === 'USD' ? '$' : ''
|
||||
|
||||
return this.renderFiatAmount(fiatDisplayNumber, currentCurrency, fiatPrefix)
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.renderFiatAmount = function (fiatDisplayNumber, fiatSuffix, fiatPrefix) {
|
||||
const shouldNotRenderFiat = fiatDisplayNumber === 'N/A' || Number(fiatDisplayNumber) === 0
|
||||
if (shouldNotRenderFiat) return null
|
||||
|
||||
const upperCaseFiatSuffix = fiatSuffix.toUpperCase()
|
||||
|
||||
const display = currencies.find(currency => currency.code === upperCaseFiatSuffix)
|
||||
? currencyFormatter.format(Number(fiatDisplayNumber), {
|
||||
code: upperCaseFiatSuffix,
|
||||
})
|
||||
: `${fiatPrefix}${fiatDisplayNumber} ${upperCaseFiatSuffix}`
|
||||
|
||||
return h('div.fiat-amount', {
|
||||
style: {},
|
||||
}, display)
|
||||
}
|
||||
|
||||
BalanceComponent.prototype.getTokenBalance = function (formattedBalance, shorten) {
|
||||
const balanceObj = generateBalanceObject(formattedBalance, shorten ? 1 : 3)
|
||||
|
||||
|
@ -1,267 +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 CoinbaseForm = require('./coinbase-form')
|
||||
const ShapeshiftForm = require('./shapeshift-form')
|
||||
const Loading = require('./loading-screen')
|
||||
const AccountPanel = require('./account-panel')
|
||||
const RadioList = require('./custom-radio-list')
|
||||
const { getNetworkDisplayName } = require('../../../app/scripts/controllers/network/util')
|
||||
|
||||
BuyButtonSubview.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(BuyButtonSubview)
|
||||
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
identity: state.appState.identity,
|
||||
account: state.metamask.accounts[state.appState.buyView.buyAddress],
|
||||
warning: state.appState.warning,
|
||||
buyView: state.appState.buyView,
|
||||
network: state.metamask.network,
|
||||
provider: state.metamask.provider,
|
||||
context: state.appState.currentView.context,
|
||||
isSubLoading: state.appState.isSubLoading,
|
||||
}
|
||||
}
|
||||
|
||||
inherits(BuyButtonSubview, Component)
|
||||
function BuyButtonSubview () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.render = function () {
|
||||
return (
|
||||
h('div', {
|
||||
style: {
|
||||
width: '100%',
|
||||
},
|
||||
}, [
|
||||
this.headerSubview(),
|
||||
this.primarySubview(),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.headerSubview = function () {
|
||||
const props = this.props
|
||||
const isLoading = props.isSubLoading
|
||||
return (
|
||||
|
||||
h('.flex-column', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
// header bar (back button, label)
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}, [
|
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
|
||||
onClick: this.backButtonContext.bind(this),
|
||||
style: {
|
||||
position: 'absolute',
|
||||
left: '10px',
|
||||
},
|
||||
}),
|
||||
h('h2.text-transform-uppercase.flex-center', {
|
||||
style: {
|
||||
width: '100vw',
|
||||
background: 'rgb(235, 235, 235)',
|
||||
color: 'rgb(174, 174, 174)',
|
||||
paddingTop: '4px',
|
||||
paddingBottom: '4px',
|
||||
},
|
||||
}, this.context.t('depositEth')),
|
||||
]),
|
||||
|
||||
// loading indication
|
||||
h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
top: '57vh',
|
||||
left: '49vw',
|
||||
},
|
||||
}, [
|
||||
isLoading && h(Loading),
|
||||
]),
|
||||
|
||||
// account panel
|
||||
h('div', {
|
||||
style: {
|
||||
width: '80%',
|
||||
},
|
||||
}, [
|
||||
h(AccountPanel, {
|
||||
showFullAddress: true,
|
||||
identity: props.identity,
|
||||
account: props.account,
|
||||
}),
|
||||
]),
|
||||
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}, [
|
||||
h('h3.text-transform-uppercase.flex-center', {
|
||||
style: {
|
||||
paddingLeft: '15px',
|
||||
width: '100vw',
|
||||
background: 'rgb(235, 235, 235)',
|
||||
color: 'rgb(174, 174, 174)',
|
||||
paddingTop: '4px',
|
||||
paddingBottom: '4px',
|
||||
},
|
||||
}, this.context.t('selectService')),
|
||||
]),
|
||||
|
||||
])
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
BuyButtonSubview.prototype.primarySubview = function () {
|
||||
const props = this.props
|
||||
const network = props.network
|
||||
|
||||
switch (network) {
|
||||
case 'loading':
|
||||
return
|
||||
|
||||
case '1':
|
||||
return this.mainnetSubview()
|
||||
|
||||
// Ropsten, Rinkeby, Kovan
|
||||
case '3':
|
||||
case '4':
|
||||
case '42':
|
||||
const networkName = getNetworkDisplayName(network)
|
||||
const label = `${networkName} ${this.context.t('testFaucet')}`
|
||||
return (
|
||||
h('div.flex-column', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
margin: '20px 50px',
|
||||
},
|
||||
}, [
|
||||
h('button.text-transform-uppercase', {
|
||||
onClick: () => this.props.dispatch(actions.buyEth({ network })),
|
||||
style: {
|
||||
marginTop: '15px',
|
||||
},
|
||||
}, label),
|
||||
// Kovan only: Dharma loans beta
|
||||
network === '42' ? (
|
||||
h('button.text-transform-uppercase', {
|
||||
onClick: () => this.navigateTo('https://borrow.dharma.io/'),
|
||||
style: {
|
||||
marginTop: '15px',
|
||||
},
|
||||
}, this.context.t('borrowDharma'))
|
||||
) : null,
|
||||
])
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
h('h2.error', this.context.t('unknownNetworkId'))
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.mainnetSubview = function () {
|
||||
const props = this.props
|
||||
|
||||
return (
|
||||
|
||||
h('.flex-column', {
|
||||
style: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
}, [
|
||||
|
||||
h('.flex-row.selected-exchange', {
|
||||
style: {
|
||||
position: 'relative',
|
||||
right: '35px',
|
||||
marginTop: '20px',
|
||||
marginBottom: '20px',
|
||||
},
|
||||
}, [
|
||||
h(RadioList, {
|
||||
defaultFocus: props.buyView.subview,
|
||||
labels: [
|
||||
'Coinbase',
|
||||
'ShapeShift',
|
||||
],
|
||||
subtext: {
|
||||
'Coinbase': `${this.context.t('crypto')}/${this.context.t('fiat')} (${this.context.t('usaOnly')})`,
|
||||
'ShapeShift': this.context.t('crypto'),
|
||||
},
|
||||
onClick: this.radioHandler.bind(this),
|
||||
}),
|
||||
]),
|
||||
|
||||
h('h3.text-transform-uppercase', {
|
||||
style: {
|
||||
paddingLeft: '15px',
|
||||
fontFamily: 'Montserrat Light',
|
||||
width: '100vw',
|
||||
background: 'rgb(235, 235, 235)',
|
||||
color: 'rgb(174, 174, 174)',
|
||||
paddingTop: '4px',
|
||||
paddingBottom: '4px',
|
||||
},
|
||||
}, props.buyView.subview),
|
||||
|
||||
this.formVersionSubview(),
|
||||
])
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.formVersionSubview = function () {
|
||||
const network = this.props.network
|
||||
if (network === '1') {
|
||||
if (this.props.buyView.formView.coinbase) {
|
||||
return h(CoinbaseForm, this.props)
|
||||
} else if (this.props.buyView.formView.shapeshift) {
|
||||
return h(ShapeshiftForm, this.props)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.navigateTo = function (url) {
|
||||
global.platform.openWindow({ url })
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.backButtonContext = function () {
|
||||
if (this.props.context === 'confTx') {
|
||||
this.props.dispatch(actions.showConfTxPage({transForward: false}))
|
||||
} else {
|
||||
this.props.dispatch(actions.goHome())
|
||||
}
|
||||
}
|
||||
|
||||
BuyButtonSubview.prototype.radioHandler = function (event) {
|
||||
switch (event.target.title) {
|
||||
case 'Coinbase':
|
||||
return this.props.dispatch(actions.coinBaseSubview())
|
||||
case 'ShapeShift':
|
||||
return this.props.dispatch(actions.shapeShiftSubview(this.props.provider.type))
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ETH } from '../../constants/common'
|
||||
|
||||
export default class CurrencyDisplay extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
displayValue: PropTypes.string,
|
||||
prefix: PropTypes.string,
|
||||
currency: PropTypes.oneOf([ETH]),
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, displayValue, prefix } = this.props
|
||||
const text = `${prefix || ''}${displayValue}`
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
title={text}
|
||||
>
|
||||
{ text }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux'
|
||||
import CurrencyDisplay from './currency-display.component'
|
||||
import { getValueFromWeiHex, formatCurrency } from '../../helpers/confirm-transaction/util'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { value, numberOfDecimals = 2, currency } = ownProps
|
||||
const { metamask: { currentCurrency, conversionRate } } = state
|
||||
|
||||
const toCurrency = currency || currentCurrency
|
||||
const convertedValue = getValueFromWeiHex({ value, toCurrency, conversionRate, numberOfDecimals })
|
||||
const formattedValue = formatCurrency(convertedValue, toCurrency)
|
||||
const displayValue = `${formattedValue} ${toCurrency.toUpperCase()}`
|
||||
|
||||
return {
|
||||
displayValue,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(CurrencyDisplay)
|
1
ui/app/components/currency-display/index.js
Normal file
1
ui/app/components/currency-display/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './currency-display.container'
|
@ -0,0 +1,27 @@
|
||||
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')
|
||||
})
|
||||
})
|
@ -0,0 +1,61 @@
|
||||
import assert from 'assert'
|
||||
import proxyquire from 'proxyquire'
|
||||
|
||||
let mapStateToProps
|
||||
|
||||
proxyquire('../currency-display.container.js', {
|
||||
'react-redux': {
|
||||
connect: ms => {
|
||||
mapStateToProps = ms
|
||||
return () => ({})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
describe('CurrencyDisplay container', () => {
|
||||
describe('mapStateToProps()', () => {
|
||||
it('should return the correct props', () => {
|
||||
const mockState = {
|
||||
metamask: {
|
||||
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',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
tests.forEach(({ props, result }) => {
|
||||
assert.deepEqual(mapStateToProps(mockState, props), result)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
@ -1,60 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
|
||||
module.exports = RadioList
|
||||
|
||||
inherits(RadioList, Component)
|
||||
function RadioList () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
RadioList.prototype.render = function () {
|
||||
const props = this.props
|
||||
const activeClass = '.custom-radio-selected'
|
||||
const inactiveClass = '.custom-radio-inactive'
|
||||
const {
|
||||
labels,
|
||||
defaultFocus,
|
||||
} = props
|
||||
|
||||
|
||||
return (
|
||||
h('.flex-row', {
|
||||
style: {
|
||||
fontSize: '12px',
|
||||
},
|
||||
}, [
|
||||
h('.flex-column.custom-radios', {
|
||||
style: {
|
||||
marginRight: '5px',
|
||||
},
|
||||
},
|
||||
labels.map((lable, i) => {
|
||||
let isSelcted = (this.state !== null)
|
||||
isSelcted = isSelcted ? (this.state.selected === lable) : (defaultFocus === lable)
|
||||
return h(isSelcted ? activeClass : inactiveClass, {
|
||||
title: lable,
|
||||
onClick: (event) => {
|
||||
this.setState({selected: event.target.title})
|
||||
props.onClick(event)
|
||||
},
|
||||
})
|
||||
})
|
||||
),
|
||||
h('.text', {},
|
||||
labels.map((lable) => {
|
||||
if (props.subtext) {
|
||||
return h('.flex-row', {}, [
|
||||
h('.radio-titles', lable),
|
||||
h('.radio-titles-subtext', `- ${props.subtext[lable]}`),
|
||||
])
|
||||
} else {
|
||||
return h('.radio-titles', lable)
|
||||
}
|
||||
})
|
||||
),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
@ -272,10 +272,12 @@ NetworkDropdown.prototype.getNetworkName = function () {
|
||||
|
||||
NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
|
||||
const props = this.props
|
||||
const rpcTarget = provider.rpcTarget
|
||||
const reversedRpcList = rpcList.slice().reverse()
|
||||
|
||||
return rpcList.map((rpc) => {
|
||||
if ((rpc === 'http://localhost:8545') || (rpc === rpcTarget)) {
|
||||
return reversedRpcList.map((rpc) => {
|
||||
const currentRpcTarget = provider.type === 'rpc' && rpc === provider.rpcTarget
|
||||
|
||||
if ((rpc === 'http://localhost:8545') || currentRpcTarget) {
|
||||
return null
|
||||
} else {
|
||||
return h(
|
||||
@ -291,11 +293,11 @@ NetworkDropdown.prototype.renderCommonRpc = function (rpcList, provider) {
|
||||
},
|
||||
},
|
||||
[
|
||||
rpcTarget === rpc ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
|
||||
currentRpcTarget ? h('i.fa.fa-check') : h('.network-check__transparent', '✓'),
|
||||
h('i.fa.fa-question-circle.fa-med.menu-icon-circle'),
|
||||
h('span.network-name-item', {
|
||||
style: {
|
||||
color: rpcTarget === rpc ? '#ffffff' : '#9b9b9b',
|
||||
color: currentRpcTarget ? '#ffffff' : '#9b9b9b',
|
||||
},
|
||||
}, rpc),
|
||||
]
|
||||
|
@ -1,23 +1,35 @@
|
||||
@import './app-header/index';
|
||||
|
||||
@import './button-group/index';
|
||||
|
||||
@import './confirm-page-container/index';
|
||||
|
||||
@import './export-text-container/index';
|
||||
|
||||
@import './selected-account/index';
|
||||
|
||||
@import './info-box/index';
|
||||
|
||||
@import './network-display/index';
|
||||
@import './menu-bar/index';
|
||||
|
||||
@import './confirm-page-container/index';
|
||||
@import './modals/index';
|
||||
|
||||
@import './network-display/index';
|
||||
|
||||
@import './page-container/index';
|
||||
|
||||
@import './pages/index';
|
||||
|
||||
@import './modals/index';
|
||||
@import './selected-account/index';
|
||||
|
||||
@import './sender-to-recipient/index';
|
||||
|
||||
@import './tabs/index';
|
||||
|
||||
@import './app-header/index';
|
||||
@import './transaction-view/index';
|
||||
|
||||
@import './transaction-view-balance/index';
|
||||
|
||||
@import './transaction-list/index';
|
||||
|
||||
@import './transaction-list-item/index';
|
||||
|
||||
@import './transaction-status/index';
|
||||
|
1
ui/app/components/menu-bar/index.js
Normal file
1
ui/app/components/menu-bar/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './menu-bar.container'
|
23
ui/app/components/menu-bar/index.scss
Normal file
23
ui/app/components/menu-bar/index.scss
Normal file
@ -0,0 +1,23 @@
|
||||
.menu-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 16px;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
&__sidebar-button {
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
&__open-in-browser {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
52
ui/app/components/menu-bar/menu-bar.component.js
Normal file
52
ui/app/components/menu-bar/menu-bar.component.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Tooltip from '../tooltip'
|
||||
import SelectedAccount from '../selected-account'
|
||||
|
||||
export default class MenuBar extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
hideSidebar: PropTypes.func,
|
||||
isMascara: PropTypes.bool,
|
||||
sidebarOpen: PropTypes.bool,
|
||||
showSidebar: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { isMascara, sidebarOpen, hideSidebar, showSidebar } = this.props
|
||||
|
||||
return (
|
||||
<div className="menu-bar">
|
||||
<Tooltip
|
||||
title={t('menu')}
|
||||
position="bottom"
|
||||
>
|
||||
<div
|
||||
className="fa fa-bars menu-bar__sidebar-button"
|
||||
onClick={() => sidebarOpen ? hideSidebar() : showSidebar()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<SelectedAccount />
|
||||
{
|
||||
!isMascara && (
|
||||
<Tooltip
|
||||
title={t('openInTab')}
|
||||
position="bottom"
|
||||
>
|
||||
<div
|
||||
className="menu-bar__open-in-browser"
|
||||
onClick={() => global.platform.openExtensionInBrowser()}
|
||||
>
|
||||
<img src="images/popout.svg" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
21
ui/app/components/menu-bar/menu-bar.container.js
Normal file
21
ui/app/components/menu-bar/menu-bar.container.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { connect } from 'react-redux'
|
||||
import MenuBar from './menu-bar.component'
|
||||
import { showSidebar, hideSidebar } from '../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { appState: { sidebarOpen, isMascara } } = state
|
||||
|
||||
return {
|
||||
sidebarOpen,
|
||||
isMascara,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
showSidebar: () => dispatch(showSidebar()),
|
||||
hideSidebar: () => dispatch(hideSidebar()),
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(MenuBar)
|
@ -61,7 +61,7 @@ AccountDetailsModal.prototype.render = function () {
|
||||
|
||||
let exportPrivateKeyFeatureEnabled = true
|
||||
// This feature is disabled for hardware wallets
|
||||
if (keyring.type.search('Hardware') !== -1) {
|
||||
if (keyring && keyring.type.search('Hardware') !== -1) {
|
||||
exportPrivateKeyFeatureEnabled = false
|
||||
}
|
||||
|
||||
|
@ -7,9 +7,9 @@ const actions = require('../../actions')
|
||||
const { getSelectedIdentity } = require('../../selectors')
|
||||
const Identicon = require('../identicon')
|
||||
|
||||
function mapStateToProps (state) {
|
||||
function mapStateToProps (state, ownProps) {
|
||||
return {
|
||||
selectedIdentity: getSelectedIdentity(state),
|
||||
selectedIdentity: ownProps.selectedIdentity || getSelectedIdentity(state),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
const log = require('loglevel')
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
@ -11,19 +12,33 @@ const ReadOnlyInput = require('../readonly-input')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const { checksumAddress } = require('../../util')
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
warning: state.appState.warning,
|
||||
privateKey: state.appState.accountDetail.privateKey,
|
||||
network: state.metamask.network,
|
||||
selectedIdentity: getSelectedIdentity(state),
|
||||
previousModalState: state.appState.modal.previousModalState.name,
|
||||
function mapStateToPropsFactory () {
|
||||
let selectedIdentity = null
|
||||
return function mapStateToProps (state) {
|
||||
// We should **not** change the identity displayed here even if it changes from underneath us.
|
||||
// If we do, we will be showing the user one private key and a **different** address and name.
|
||||
// Note that the selected identity **will** change from underneath us when we unlock the keyring
|
||||
// which is the expected behavior that we are side-stepping.
|
||||
selectedIdentity = selectedIdentity || getSelectedIdentity(state)
|
||||
return {
|
||||
warning: state.appState.warning,
|
||||
privateKey: state.appState.accountDetail.privateKey,
|
||||
network: state.metamask.network,
|
||||
selectedIdentity,
|
||||
previousModalState: state.appState.modal.previousModalState.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
exportAccount: (password, address) => dispatch(actions.exportAccount(password, address)),
|
||||
exportAccount: (password, address) => {
|
||||
return dispatch(actions.exportAccount(password, address))
|
||||
.then((res) => {
|
||||
dispatch(actions.hideWarning())
|
||||
return res
|
||||
})
|
||||
},
|
||||
showAccountDetailModal: () => dispatch(actions.showModal({ name: 'ACCOUNT_DETAILS' })),
|
||||
hideModal: () => dispatch(actions.hideModal()),
|
||||
}
|
||||
@ -36,6 +51,7 @@ function ExportPrivateKeyModal () {
|
||||
this.state = {
|
||||
password: '',
|
||||
privateKey: null,
|
||||
showWarning: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,14 +59,18 @@ ExportPrivateKeyModal.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(ExportPrivateKeyModal)
|
||||
module.exports = connect(mapStateToPropsFactory, mapDispatchToProps)(ExportPrivateKeyModal)
|
||||
|
||||
|
||||
ExportPrivateKeyModal.prototype.exportAccountAndGetPrivateKey = function (password, address) {
|
||||
const { exportAccount } = this.props
|
||||
|
||||
exportAccount(password, address)
|
||||
.then(privateKey => this.setState({ privateKey }))
|
||||
.then(privateKey => this.setState({
|
||||
privateKey,
|
||||
showWarning: false,
|
||||
}))
|
||||
.catch((e) => log.error(e))
|
||||
}
|
||||
|
||||
ExportPrivateKeyModal.prototype.renderPasswordLabel = function (privateKey) {
|
||||
@ -110,9 +130,13 @@ ExportPrivateKeyModal.prototype.render = function () {
|
||||
} = this.props
|
||||
const { name, address } = selectedIdentity
|
||||
|
||||
const { privateKey } = this.state
|
||||
const {
|
||||
privateKey,
|
||||
showWarning,
|
||||
} = this.state
|
||||
|
||||
return h(AccountModalContainer, {
|
||||
selectedIdentity,
|
||||
showBackButton: previousModalState === 'ACCOUNT_DETAILS',
|
||||
backButtonAction: () => showAccountDetailModal(),
|
||||
}, [
|
||||
@ -134,7 +158,7 @@ ExportPrivateKeyModal.prototype.render = function () {
|
||||
|
||||
this.renderPasswordInput(privateKey),
|
||||
|
||||
!warning ? null : h('span.private-key-password-error', warning),
|
||||
showWarning && warning ? h('span.private-key-password-error', warning) : null,
|
||||
]),
|
||||
|
||||
h('div.private-key-password-warning', this.context.t('privateKeyWarning')),
|
||||
|
@ -109,7 +109,7 @@
|
||||
|
||||
&--selected {
|
||||
color: $curious-blue;
|
||||
border-bottom: 3px solid $curious-blue;
|
||||
border-bottom: 2px solid $curious-blue;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class PageContainerHeader extends Component {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
@ -11,8 +11,18 @@ export default class PageContainerHeader extends Component {
|
||||
onBackButtonClick: PropTypes.func,
|
||||
backButtonStyles: PropTypes.object,
|
||||
backButtonString: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
tabs: PropTypes.node,
|
||||
}
|
||||
|
||||
renderTabs () {
|
||||
const { tabs } = this.props
|
||||
|
||||
return tabs && (
|
||||
<ul className="page-container__tabs">
|
||||
{ tabs }
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
renderHeaderRow () {
|
||||
const { showBackButton, onBackButtonClick, backButtonStyles, backButtonString } = this.props
|
||||
@ -31,15 +41,18 @@ export default class PageContainerHeader extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { title, subtitle, onClose, children } = this.props
|
||||
const { title, subtitle, onClose, tabs } = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container__header">
|
||||
<div className={
|
||||
classnames(
|
||||
'page-container__header',
|
||||
{ 'page-container__header--no-padding-bottom': Boolean(tabs) }
|
||||
)
|
||||
}>
|
||||
|
||||
{ this.renderHeaderRow() }
|
||||
|
||||
{ children }
|
||||
|
||||
{
|
||||
title && <div className="page-container__title">
|
||||
{ title }
|
||||
@ -59,6 +72,7 @@ export default class PageContainerHeader extends Component {
|
||||
/>
|
||||
}
|
||||
|
||||
{ this.renderTabs() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,30 +1,82 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import PageContainerHeader from './page-container-header'
|
||||
import PageContainerFooter from './page-container-footer'
|
||||
|
||||
export default class PageContainer extends Component {
|
||||
|
||||
export default class PageContainer extends PureComponent {
|
||||
static propTypes = {
|
||||
// PageContainerHeader props
|
||||
title: PropTypes.string.isRequired,
|
||||
subtitle: PropTypes.string,
|
||||
backButtonString: PropTypes.string,
|
||||
backButtonStyles: PropTypes.object,
|
||||
onBackButtonClick: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
showBackButton: PropTypes.bool,
|
||||
onBackButtonClick: PropTypes.func,
|
||||
backButtonStyles: PropTypes.object,
|
||||
backButtonString: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
// Tabs-related props
|
||||
defaultActiveTabIndex: PropTypes.number,
|
||||
tabsComponent: PropTypes.node,
|
||||
// Content props
|
||||
ContentComponent: PropTypes.func,
|
||||
contentComponentProps: PropTypes.object,
|
||||
contentComponent: PropTypes.node,
|
||||
// PageContainerFooter props
|
||||
onCancel: PropTypes.func,
|
||||
cancelText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
submitText: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
}
|
||||
|
||||
state = {
|
||||
activeTabIndex: this.props.defaultActiveTabIndex || 0,
|
||||
}
|
||||
|
||||
handleTabClick (activeTabIndex) {
|
||||
this.setState({ activeTabIndex })
|
||||
}
|
||||
|
||||
renderTabs () {
|
||||
const { tabsComponent } = this.props
|
||||
|
||||
if (!tabsComponent) {
|
||||
return
|
||||
}
|
||||
|
||||
const numberOfTabs = React.Children.count(tabsComponent.props.children)
|
||||
|
||||
return React.Children.map(tabsComponent.props.children, (child, tabIndex) => {
|
||||
return child && React.cloneElement(child, {
|
||||
onClick: index => this.handleTabClick(index),
|
||||
tabIndex,
|
||||
isActive: numberOfTabs > 1 && tabIndex === this.state.activeTabIndex,
|
||||
key: tabIndex,
|
||||
className: 'page-container__tab',
|
||||
activeClassName: 'page-container__tab--selected',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
renderActiveTabContent () {
|
||||
const { tabsComponent } = this.props
|
||||
const { children } = tabsComponent.props
|
||||
const { activeTabIndex } = this.state
|
||||
|
||||
return children[activeTabIndex]
|
||||
? children[activeTabIndex].props.children
|
||||
: children.props.children
|
||||
}
|
||||
|
||||
renderContent () {
|
||||
const { contentComponent, tabsComponent } = this.props
|
||||
|
||||
if (contentComponent) {
|
||||
return contentComponent
|
||||
} else if (tabsComponent) {
|
||||
return this.renderActiveTabContent()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
@ -35,8 +87,6 @@ export default class PageContainer extends Component {
|
||||
onBackButtonClick,
|
||||
backButtonStyles,
|
||||
backButtonString,
|
||||
ContentComponent,
|
||||
contentComponentProps,
|
||||
onCancel,
|
||||
cancelText,
|
||||
onSubmit,
|
||||
@ -54,9 +104,10 @@ export default class PageContainer extends Component {
|
||||
onBackButtonClick={onBackButtonClick}
|
||||
backButtonStyles={backButtonStyles}
|
||||
backButtonString={backButtonString}
|
||||
tabs={this.renderTabs()}
|
||||
/>
|
||||
<div className="page-container__content">
|
||||
<ContentComponent { ...contentComponentProps } />
|
||||
{ this.renderContent() }
|
||||
</div>
|
||||
<PageContainerFooter
|
||||
onCancel={onCancel}
|
||||
@ -68,5 +119,4 @@ export default class PageContainer extends Component {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { Component } from 'react'
|
||||
import classnames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
import ethUtil from 'ethereumjs-util'
|
||||
import { checkExistingAddresses } from './util'
|
||||
import { tokenInfoGetter } from '../../../token-util'
|
||||
import { DEFAULT_ROUTE, CONFIRM_ADD_TOKEN_ROUTE } from '../../../routes'
|
||||
import Button from '../../button'
|
||||
import TextField from '../../text-field'
|
||||
import TokenList from './token-list'
|
||||
import TokenSearch from './token-search'
|
||||
import PageContainer from '../../page-container'
|
||||
import { Tabs, Tab } from '../../tabs'
|
||||
|
||||
const emptyAddr = '0x0000000000000000000000000000000000000000'
|
||||
const SEARCH_TAB = 'SEARCH'
|
||||
@ -285,65 +285,33 @@ class AddToken extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
renderTabs () {
|
||||
return (
|
||||
<Tabs>
|
||||
<Tab name={this.context.t('search')}>
|
||||
{ this.renderSearchToken() }
|
||||
</Tab>
|
||||
<Tab name={this.context.t('customToken')}>
|
||||
{ this.renderCustomTokenForm() }
|
||||
</Tab>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { displayedTab } = this.state
|
||||
const { history, clearPendingTokens } = this.props
|
||||
|
||||
return (
|
||||
<div className="page-container">
|
||||
<div className="page-container__header page-container__header--no-padding-bottom">
|
||||
<div className="page-container__title">
|
||||
{ this.context.t('addTokens') }
|
||||
</div>
|
||||
<div className="page-container__tabs">
|
||||
<div
|
||||
className={classnames('page-container__tab', {
|
||||
'page-container__tab--selected': displayedTab === SEARCH_TAB,
|
||||
})}
|
||||
onClick={() => this.setState({ displayedTab: SEARCH_TAB })}
|
||||
>
|
||||
{ this.context.t('search') }
|
||||
</div>
|
||||
<div
|
||||
className={classnames('page-container__tab', {
|
||||
'page-container__tab--selected': displayedTab === CUSTOM_TOKEN_TAB,
|
||||
})}
|
||||
onClick={() => this.setState({ displayedTab: CUSTOM_TOKEN_TAB })}
|
||||
>
|
||||
{ this.context.t('customToken') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page-container__content">
|
||||
{
|
||||
displayedTab === CUSTOM_TOKEN_TAB
|
||||
? this.renderCustomTokenForm()
|
||||
: this.renderSearchToken()
|
||||
}
|
||||
</div>
|
||||
<div className="page-container__footer">
|
||||
<Button
|
||||
type="default"
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
onClick={() => {
|
||||
clearPendingTokens()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}}
|
||||
>
|
||||
{ this.context.t('cancel') }
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
large
|
||||
className="page-container__footer-button"
|
||||
onClick={() => this.handleNext()}
|
||||
disabled={this.hasError() || !this.hasSelected()}
|
||||
>
|
||||
{ this.context.t('next') }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<PageContainer
|
||||
title={this.context.t('addTokens')}
|
||||
tabsComponent={this.renderTabs()}
|
||||
onSubmit={() => this.handleNext()}
|
||||
disabled={this.hasError() || !this.hasSelected()}
|
||||
onCancel={() => {
|
||||
clearPendingTokens()
|
||||
history.push(DEFAULT_ROUTE)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import { DEFAULT_ROUTE } from '../../../routes'
|
||||
import Button from '../../button'
|
||||
import Identicon from '../../../components/identicon'
|
||||
import TokenBalance from '../confirm-add-token/token-balance'
|
||||
import TokenBalance from '../../token-balance'
|
||||
|
||||
export default class ConfirmAddSuggestedToken extends Component {
|
||||
static contextTypes = {
|
||||
|
@ -2,8 +2,8 @@ import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DEFAULT_ROUTE, ADD_TOKEN_ROUTE } from '../../../routes'
|
||||
import Button from '../../button'
|
||||
import Identicon from '../../../components/identicon'
|
||||
import TokenBalance from './token-balance'
|
||||
import Identicon from '../../identicon'
|
||||
import TokenBalance from '../../token-balance'
|
||||
|
||||
export default class ConfirmAddToken extends Component {
|
||||
static contextTypes = {
|
||||
|
@ -1,2 +0,0 @@
|
||||
import TokenBalance from './token-balance.container'
|
||||
module.exports = TokenBalance
|
@ -1,16 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default class TokenBalance extends Component {
|
||||
static propTypes = {
|
||||
string: PropTypes.string,
|
||||
symbol: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="hide-text-overflow">{ this.props.string }</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -12,25 +12,27 @@ import {
|
||||
CONFIRM_TOKEN_METHOD_PATH,
|
||||
SIGNATURE_REQUEST_PATH,
|
||||
} from '../../../routes'
|
||||
import { isConfirmDeployContract } from './confirm-transaction-switch.util'
|
||||
import { isConfirmDeployContract } from '../../../helpers/transactions.util'
|
||||
import {
|
||||
TOKEN_METHOD_TRANSFER,
|
||||
TOKEN_METHOD_APPROVE,
|
||||
TOKEN_METHOD_TRANSFER_FROM,
|
||||
} from './confirm-transaction-switch.constants'
|
||||
} from '../../../constants/transactions'
|
||||
|
||||
export default class ConfirmTransactionSwitch extends Component {
|
||||
static propTypes = {
|
||||
txData: PropTypes.object,
|
||||
methodData: PropTypes.object,
|
||||
fetchingMethodData: PropTypes.bool,
|
||||
fetchingData: PropTypes.bool,
|
||||
isEtherTransaction: PropTypes.bool,
|
||||
}
|
||||
|
||||
redirectToTransaction () {
|
||||
const {
|
||||
txData,
|
||||
methodData: { name },
|
||||
fetchingMethodData,
|
||||
fetchingData,
|
||||
isEtherTransaction,
|
||||
} = this.props
|
||||
const { id, txParams: { data } = {} } = txData
|
||||
|
||||
@ -39,10 +41,15 @@ export default class ConfirmTransactionSwitch extends Component {
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
|
||||
if (fetchingMethodData) {
|
||||
if (fetchingData) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
if (isEtherTransaction) {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}`
|
||||
return <Redirect to={{ pathname }} />
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const methodName = name && name.toLowerCase()
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
export const TOKEN_METHOD_TRANSFER = 'transfer'
|
||||
export const TOKEN_METHOD_APPROVE = 'approve'
|
||||
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
|
@ -6,14 +6,16 @@ const mapStateToProps = state => {
|
||||
confirmTransaction: {
|
||||
txData,
|
||||
methodData,
|
||||
fetchingMethodData,
|
||||
fetchingData,
|
||||
toSmartContract,
|
||||
},
|
||||
} = state
|
||||
|
||||
return {
|
||||
txData,
|
||||
methodData,
|
||||
fetchingMethodData,
|
||||
fetchingData,
|
||||
isEtherTransaction: !toSmartContract,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,247 +0,0 @@
|
||||
const { Component } = require('react')
|
||||
const { connect } = require('react-redux')
|
||||
const PropTypes = require('prop-types')
|
||||
const { Redirect, withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../../actions')
|
||||
const log = require('loglevel')
|
||||
|
||||
// init
|
||||
const NewKeyChainScreen = require('../../new-keychain')
|
||||
// mascara
|
||||
const MascaraBuyEtherScreen = require('../../../../mascara/src/app/first-time/buy-ether-screen').default
|
||||
|
||||
// accounts
|
||||
const MainContainer = require('../../main-container')
|
||||
|
||||
// other views
|
||||
const BuyView = require('../../components/buy-button-subview')
|
||||
const QrView = require('../../components/qr-code')
|
||||
|
||||
// Routes
|
||||
const {
|
||||
INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||
RESTORE_VAULT_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
NOTICE_ROUTE,
|
||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||
} = require('../../routes')
|
||||
|
||||
const { unconfirmedTransactionsCountSelector } = require('../../selectors/confirm-transaction')
|
||||
|
||||
class Home extends Component {
|
||||
componentDidMount () {
|
||||
const {
|
||||
history,
|
||||
suggestedTokens = {},
|
||||
unconfirmedTransactionsCount = 0,
|
||||
} = this.props
|
||||
|
||||
// suggested new tokens
|
||||
if (Object.keys(suggestedTokens).length > 0) {
|
||||
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
|
||||
}
|
||||
|
||||
// unapprovedTxs and unapproved messages
|
||||
if (unconfirmedTransactionsCount > 0) {
|
||||
history.push(CONFIRM_TRANSACTION_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
log.debug('rendering primary')
|
||||
const {
|
||||
noActiveNotices,
|
||||
lostAccounts,
|
||||
forgottenPassword,
|
||||
currentView,
|
||||
activeAddress,
|
||||
seedWords,
|
||||
} = this.props
|
||||
|
||||
// notices
|
||||
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
|
||||
return h(Redirect, {
|
||||
to: {
|
||||
pathname: NOTICE_ROUTE,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// seed words
|
||||
if (seedWords) {
|
||||
log.debug('rendering seed words')
|
||||
return h(Redirect, {
|
||||
to: {
|
||||
pathname: INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (forgottenPassword) {
|
||||
log.debug('rendering restore vault screen')
|
||||
return h(Redirect, {
|
||||
to: {
|
||||
pathname: RESTORE_VAULT_ROUTE,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// show current view
|
||||
switch (currentView.name) {
|
||||
|
||||
case 'accountDetail':
|
||||
log.debug('rendering main container')
|
||||
return h(MainContainer, {key: 'account-detail'})
|
||||
|
||||
case 'newKeychain':
|
||||
log.debug('rendering new keychain screen')
|
||||
return h(NewKeyChainScreen, {key: 'new-keychain'})
|
||||
|
||||
case 'buyEth':
|
||||
log.debug('rendering buy ether screen')
|
||||
return h(BuyView, {key: 'buyEthView'})
|
||||
|
||||
case 'onboardingBuyEth':
|
||||
log.debug('rendering onboarding buy ether screen')
|
||||
return h(MascaraBuyEtherScreen, {key: 'buyEthView'})
|
||||
|
||||
case 'qr':
|
||||
log.debug('rendering show qr screen')
|
||||
return h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
},
|
||||
}, [
|
||||
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer.color-orange', {
|
||||
onClick: () => this.props.dispatch(actions.backToAccountDetail(activeAddress)),
|
||||
style: {
|
||||
marginLeft: '10px',
|
||||
marginTop: '50px',
|
||||
},
|
||||
}),
|
||||
h('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
left: '44px',
|
||||
width: '285px',
|
||||
},
|
||||
}, [
|
||||
h(QrView, {key: 'qr'}),
|
||||
]),
|
||||
])
|
||||
|
||||
default:
|
||||
log.debug('rendering default, account detail screen')
|
||||
return h(MainContainer, {key: 'account-detail'})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Home.propTypes = {
|
||||
currentCurrency: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
loadingMessage: PropTypes.string,
|
||||
network: PropTypes.string,
|
||||
provider: PropTypes.object,
|
||||
frequentRpcList: PropTypes.array,
|
||||
currentView: PropTypes.object,
|
||||
sidebarOpen: PropTypes.bool,
|
||||
isMascara: PropTypes.bool,
|
||||
isOnboarding: PropTypes.bool,
|
||||
isUnlocked: PropTypes.bool,
|
||||
networkDropdownOpen: PropTypes.bool,
|
||||
history: PropTypes.object,
|
||||
dispatch: 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,
|
||||
isMouseUser: PropTypes.bool,
|
||||
t: PropTypes.func,
|
||||
suggestedTokens: PropTypes.object,
|
||||
unconfirmedTransactionsCount: PropTypes.number,
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const { appState, metamask } = state
|
||||
const {
|
||||
networkDropdownOpen,
|
||||
sidebarOpen,
|
||||
isLoading,
|
||||
loadingMessage,
|
||||
} = appState
|
||||
|
||||
const {
|
||||
accounts,
|
||||
address,
|
||||
isInitialized,
|
||||
noActiveNotices,
|
||||
seedWords,
|
||||
unapprovedTxs,
|
||||
nextUnreadNotice,
|
||||
lostAccounts,
|
||||
unapprovedMsgCount,
|
||||
unapprovedPersonalMsgCount,
|
||||
unapprovedTypedMessagesCount,
|
||||
} = metamask
|
||||
const selected = address || Object.keys(accounts)[0]
|
||||
|
||||
return {
|
||||
// state from plugin
|
||||
networkDropdownOpen,
|
||||
sidebarOpen,
|
||||
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,
|
||||
isRevealingSeedWords: state.metamask.isRevealingSeedWords,
|
||||
Qr: state.appState.Qr,
|
||||
welcomeScreenSeen: state.metamask.welcomeScreenSeen,
|
||||
suggestedTokens: state.metamask.suggestedTokens,
|
||||
// state needed to get account dropdown temporarily rendering from app bar
|
||||
selected,
|
||||
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps)
|
||||
)(Home)
|
77
ui/app/components/pages/home/home.component.js
Normal file
77
ui/app/components/pages/home/home.component.js
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Media from 'react-media'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import WalletView from '../../wallet-view'
|
||||
import TransactionView from '../../transaction-view'
|
||||
import {
|
||||
INITIALIZE_BACKUP_PHRASE_ROUTE,
|
||||
RESTORE_VAULT_ROUTE,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
NOTICE_ROUTE,
|
||||
CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE,
|
||||
} from '../../../routes'
|
||||
|
||||
export default class Home extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
noActiveNotices: PropTypes.bool,
|
||||
lostAccounts: PropTypes.array,
|
||||
forgottenPassword: PropTypes.bool,
|
||||
seedWords: PropTypes.string,
|
||||
suggestedTokens: PropTypes.object,
|
||||
unconfirmedTransactionsCount: PropTypes.number,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const {
|
||||
history,
|
||||
suggestedTokens = {},
|
||||
unconfirmedTransactionsCount = 0,
|
||||
} = this.props
|
||||
|
||||
// suggested new tokens
|
||||
if (Object.keys(suggestedTokens).length > 0) {
|
||||
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE)
|
||||
}
|
||||
|
||||
if (unconfirmedTransactionsCount > 0) {
|
||||
history.push(CONFIRM_TRANSACTION_ROUTE)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
noActiveNotices,
|
||||
lostAccounts,
|
||||
forgottenPassword,
|
||||
seedWords,
|
||||
} = this.props
|
||||
|
||||
// notices
|
||||
if (!noActiveNotices || (lostAccounts && lostAccounts.length > 0)) {
|
||||
return <Redirect to={{ pathname: NOTICE_ROUTE }} />
|
||||
}
|
||||
|
||||
// seed words
|
||||
if (seedWords) {
|
||||
return <Redirect to={{ pathname: INITIALIZE_BACKUP_PHRASE_ROUTE }}/>
|
||||
}
|
||||
|
||||
if (forgottenPassword) {
|
||||
return <Redirect to={{ pathname: RESTORE_VAULT_ROUTE }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-container">
|
||||
<div className="account-and-transaction-details">
|
||||
<Media
|
||||
query="(min-width: 576px)"
|
||||
render={() => <WalletView />}
|
||||
/>
|
||||
<TransactionView />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
30
ui/app/components/pages/home/home.container.js
Normal file
30
ui/app/components/pages/home/home.container.js
Normal file
@ -0,0 +1,30 @@
|
||||
import Home from './home.component'
|
||||
import { compose } from 'recompose'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { unconfirmedTransactionsCountSelector } from '../../../selectors/confirm-transaction'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const { metamask, appState } = state
|
||||
const {
|
||||
noActiveNotices,
|
||||
lostAccounts,
|
||||
seedWords,
|
||||
suggestedTokens,
|
||||
} = metamask
|
||||
const { forgottenPassword } = appState
|
||||
|
||||
return {
|
||||
noActiveNotices,
|
||||
lostAccounts,
|
||||
forgottenPassword,
|
||||
seedWords,
|
||||
suggestedTokens,
|
||||
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps)
|
||||
)(Home)
|
1
ui/app/components/pages/home/index.js
Normal file
1
ui/app/components/pages/home/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './home.container'
|
@ -1,56 +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 AccountPanel = require('./account-panel')
|
||||
|
||||
PendingMsgDetails.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(PendingMsgDetails)
|
||||
|
||||
|
||||
inherits(PendingMsgDetails, Component)
|
||||
function PendingMsgDetails () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
PendingMsgDetails.prototype.render = function () {
|
||||
var state = this.props
|
||||
var msgData = state.txData
|
||||
|
||||
var msgParams = msgData.msgParams || {}
|
||||
var address = msgParams.from || state.selectedAddress
|
||||
var identity = state.identities[address] || { address: address }
|
||||
var account = state.accounts[address] || { address: address }
|
||||
|
||||
return (
|
||||
h('div', {
|
||||
key: msgData.id,
|
||||
style: {
|
||||
margin: '10px 20px',
|
||||
},
|
||||
}, [
|
||||
|
||||
// account that will sign
|
||||
h(AccountPanel, {
|
||||
showFullAddress: true,
|
||||
identity: identity,
|
||||
account: account,
|
||||
imageifyIdenticons: state.imageifyIdenticons,
|
||||
}),
|
||||
|
||||
// message data
|
||||
h('.tx-data.flex-column.flex-justify-center.flex-grow.select-none', [
|
||||
h('.flex-column.flex-space-between', [
|
||||
h('label.font-small.allcaps', this.context.t('message')),
|
||||
h('span.font-small', msgParams.data),
|
||||
]),
|
||||
]),
|
||||
|
||||
])
|
||||
)
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const PendingTxDetails = require('./pending-msg-details')
|
||||
const connect = require('react-redux').connect
|
||||
|
||||
PendingMsg.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = connect()(PendingMsg)
|
||||
|
||||
|
||||
inherits(PendingMsg, Component)
|
||||
function PendingMsg () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
PendingMsg.prototype.render = function () {
|
||||
var state = this.props
|
||||
var msgData = state.txData
|
||||
|
||||
return (
|
||||
|
||||
h('div', {
|
||||
key: msgData.id,
|
||||
style: {
|
||||
maxWidth: '350px',
|
||||
},
|
||||
}, [
|
||||
|
||||
// header
|
||||
h('h3', {
|
||||
style: {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}, this.context.t('signMessage')),
|
||||
|
||||
h('.error', {
|
||||
style: {
|
||||
margin: '10px',
|
||||
},
|
||||
}, [
|
||||
this.context.t('signNotice'),
|
||||
h('a', {
|
||||
href: 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527',
|
||||
style: { color: 'rgb(247, 134, 28)' },
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
const url = 'https://medium.com/metamask/the-new-secure-way-to-sign-data-in-your-browser-6af9dd2a1527'
|
||||
global.platform.openWindow({ url })
|
||||
},
|
||||
}, this.context.t('readMore')),
|
||||
]),
|
||||
|
||||
// message details
|
||||
h(PendingTxDetails, state),
|
||||
|
||||
// sign + cancel
|
||||
h('.flex-row.flex-space-around', [
|
||||
h('button', {
|
||||
onClick: state.cancelMessage,
|
||||
}, this.context.t('cancel')),
|
||||
h('button', {
|
||||
onClick: state.signMessage,
|
||||
}, this.context.t('sign')),
|
||||
]),
|
||||
])
|
||||
|
||||
)
|
||||
}
|
@ -48,7 +48,7 @@ export default class SendToRow extends Component {
|
||||
return (
|
||||
<SendRowWrapper
|
||||
errorType={'to'}
|
||||
label={`${this.context.t('to')}`}
|
||||
label={`${this.context.t('to')}: `}
|
||||
showError={inError}
|
||||
>
|
||||
<EnsInput
|
||||
|
@ -102,7 +102,7 @@ describe('SendToRow Component', function () {
|
||||
|
||||
assert.equal(errorType, 'to')
|
||||
|
||||
assert.equal(label, 'to_t')
|
||||
assert.equal(label, 'to_t: ')
|
||||
|
||||
assert.equal(showError, false)
|
||||
})
|
||||
|
@ -1,5 +1,5 @@
|
||||
.sender-to-recipient {
|
||||
&__container {
|
||||
&--default {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -8,67 +8,114 @@
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
&__tooltip-wrapper {
|
||||
min-width: 0;
|
||||
}
|
||||
.sender-to-recipient {
|
||||
&__tooltip-wrapper {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__tooltip-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
&__tooltip-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&__sender,
|
||||
&__recipient {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
&__party {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: 0 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&__sender {
|
||||
padding-right: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
&--sender {
|
||||
padding-right: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__recipient {
|
||||
padding-left: 30px;
|
||||
border-left: 1px solid $geyser;
|
||||
&--recipient {
|
||||
padding-left: 30px;
|
||||
border-left: 1px solid $geyser;
|
||||
|
||||
&--with-address {
|
||||
cursor: pointer;
|
||||
&-with-address {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__arrow-container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__arrow-circle {
|
||||
background: $white;
|
||||
padding: 5px;
|
||||
border: 1px solid $geyser;
|
||||
border-radius: 20px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__name {
|
||||
padding-left: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: .875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__arrow-container {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
&--cards {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
padding: 8px;
|
||||
|
||||
&__arrow-circle {
|
||||
background: $white;
|
||||
padding: 5px;
|
||||
border: 1px solid $geyser;
|
||||
border-radius: 20px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.sender-to-recipient {
|
||||
&__party {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08);
|
||||
padding: 6px;
|
||||
background: $white;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
color: $dusty-gray;
|
||||
}
|
||||
|
||||
&__name {
|
||||
padding-left: 14px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: .875rem;
|
||||
&__tooltip-wrapper {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: .5rem;
|
||||
}
|
||||
|
||||
&__arrow-container {
|
||||
padding: 0 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,29 @@
|
||||
import React, { Component } from 'react'
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import Identicon from '../identicon'
|
||||
import Tooltip from '../tooltip-v2'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
import { DEFAULT_VARIANT, CARDS_VARIANT } from './sender-to-recipient.constants'
|
||||
|
||||
export default class SenderToRecipient extends Component {
|
||||
const variantHash = {
|
||||
[DEFAULT_VARIANT]: 'sender-to-recipient--default',
|
||||
[CARDS_VARIANT]: 'sender-to-recipient--cards',
|
||||
}
|
||||
|
||||
export default class SenderToRecipient extends PureComponent {
|
||||
static propTypes = {
|
||||
senderName: PropTypes.string,
|
||||
senderAddress: PropTypes.string,
|
||||
recipientName: PropTypes.string,
|
||||
recipientAddress: PropTypes.string,
|
||||
t: PropTypes.func,
|
||||
variant: PropTypes.oneOf([DEFAULT_VARIANT, CARDS_VARIANT]),
|
||||
addressOnly: PropTypes.bool,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
variant: DEFAULT_VARIANT,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
@ -22,24 +35,62 @@ export default class SenderToRecipient extends Component {
|
||||
recipientAddressCopied: false,
|
||||
}
|
||||
|
||||
renderSenderIdenticon () {
|
||||
return !this.props.addressOnly && (
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={this.props.senderAddress}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderSenderAddress () {
|
||||
const { t } = this.context
|
||||
const { senderName, senderAddress, addressOnly } = this.props
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
|
||||
wrapperClassName="sender-to-recipient__tooltip-wrapper"
|
||||
containerClassName="sender-to-recipient__tooltip-container"
|
||||
onHidden={() => this.setState({ senderAddressCopied: false })}
|
||||
>
|
||||
<div className="sender-to-recipient__name">
|
||||
{ addressOnly ? `${t('from')}: ${senderAddress}` : senderName }
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
renderRecipientIdenticon () {
|
||||
const { recipientAddress } = this.props
|
||||
|
||||
return !this.props.addressOnly && (
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={recipientAddress}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderRecipientWithAddress () {
|
||||
const { t } = this.context
|
||||
const { recipientName, recipientAddress } = this.props
|
||||
const { recipientName, recipientAddress, addressOnly } = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sender-to-recipient__recipient sender-to-recipient__recipient--with-address"
|
||||
className="sender-to-recipient__party sender-to-recipient__party--recipient sender-to-recipient__party--recipient-with-address"
|
||||
onClick={() => {
|
||||
this.setState({ recipientAddressCopied: true })
|
||||
copyToClipboard(recipientAddress)
|
||||
}}
|
||||
>
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={recipientAddress}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
{ this.renderRecipientIdenticon() }
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
|
||||
@ -47,8 +98,12 @@ export default class SenderToRecipient extends Component {
|
||||
containerClassName="sender-to-recipient__tooltip-container"
|
||||
onHidden={() => this.setState({ recipientAddressCopied: false })}
|
||||
>
|
||||
<div className="sender-to-recipient__name sender-to-recipient__recipient-name">
|
||||
{ recipientName || this.context.t('newContract') }
|
||||
<div className="sender-to-recipient__name">
|
||||
{
|
||||
addressOnly
|
||||
? `${t('to')}: ${recipientAddress}`
|
||||
: (recipientName || this.context.t('newContract'))
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -57,46 +112,25 @@ export default class SenderToRecipient extends Component {
|
||||
|
||||
renderRecipientWithoutAddress () {
|
||||
return (
|
||||
<div className="sender-to-recipient__recipient">
|
||||
<div className="sender-to-recipient__party sender-to-recipient__party--recipient">
|
||||
<i className="fa fa-file-text-o" />
|
||||
<div className="sender-to-recipient__name sender-to-recipient__recipient-name">
|
||||
<div className="sender-to-recipient__name">
|
||||
{ this.context.t('newContract') }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.context
|
||||
const { senderName, senderAddress, recipientAddress } = this.props
|
||||
|
||||
return (
|
||||
<div className="sender-to-recipient__container">
|
||||
<div
|
||||
className="sender-to-recipient__sender"
|
||||
onClick={() => {
|
||||
this.setState({ senderAddressCopied: true })
|
||||
copyToClipboard(senderAddress)
|
||||
}}
|
||||
>
|
||||
<div className="sender-to-recipient__sender-icon">
|
||||
<Identicon
|
||||
address={senderAddress}
|
||||
diameter={24}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
|
||||
wrapperClassName="sender-to-recipient__tooltip-wrapper"
|
||||
containerClassName="sender-to-recipient__tooltip-container"
|
||||
onHidden={() => this.setState({ senderAddressCopied: false })}
|
||||
>
|
||||
<div className="sender-to-recipient__name sender-to-recipient__sender-name">
|
||||
{ senderName }
|
||||
</div>
|
||||
</Tooltip>
|
||||
renderArrow () {
|
||||
return this.props.variant === CARDS_VARIANT
|
||||
? (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<img
|
||||
height={20}
|
||||
src="./images/caret-right.svg"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="sender-to-recipient__arrow-container">
|
||||
<div className="sender-to-recipient__arrow-circle">
|
||||
<img
|
||||
@ -106,6 +140,25 @@ export default class SenderToRecipient extends Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { senderAddress, recipientAddress, variant } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames(variantHash[variant])}>
|
||||
<div
|
||||
className={classnames('sender-to-recipient__party sender-to-recipient__party--sender')}
|
||||
onClick={() => {
|
||||
this.setState({ senderAddressCopied: true })
|
||||
copyToClipboard(senderAddress)
|
||||
}}
|
||||
>
|
||||
{ this.renderSenderIdenticon() }
|
||||
{ this.renderSenderAddress() }
|
||||
</div>
|
||||
{ this.renderArrow() }
|
||||
{
|
||||
recipientAddress
|
||||
? this.renderRecipientWithAddress()
|
||||
|
@ -0,0 +1,3 @@
|
||||
// Component design variants
|
||||
export const DEFAULT_VARIANT = 'DEFAULT_VARIANT'
|
||||
export const CARDS_VARIANT = 'CARDS_VARIANT'
|
@ -35,12 +35,13 @@ function ShiftListItem () {
|
||||
}
|
||||
|
||||
ShiftListItem.prototype.render = function () {
|
||||
return h('div.tx-list-item.tx-list-clickable', {
|
||||
return h('div.transaction-list-item.tx-list-clickable', {
|
||||
style: {
|
||||
paddingTop: '20px',
|
||||
paddingBottom: '20px',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
}, [
|
||||
h('div', {
|
||||
|
@ -3,13 +3,13 @@ import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
const Tab = props => {
|
||||
const { name, onClick, isActive, tabIndex } = props
|
||||
const { name, onClick, isActive, tabIndex, className, activeClassName } = props
|
||||
|
||||
return (
|
||||
<li
|
||||
className={classnames(
|
||||
'tab',
|
||||
isActive && 'tab--active',
|
||||
className,
|
||||
{ [activeClassName]: isActive },
|
||||
)}
|
||||
onClick={event => {
|
||||
event.preventDefault()
|
||||
@ -26,6 +26,13 @@ Tab.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
isActive: PropTypes.bool,
|
||||
tabIndex: PropTypes.number,
|
||||
className: PropTypes.string,
|
||||
activeClassName: PropTypes.string,
|
||||
}
|
||||
|
||||
Tab.defaultProps = {
|
||||
className: 'tab',
|
||||
activeClassName: 'tab--active',
|
||||
}
|
||||
|
||||
export default Tab
|
||||
|
@ -1,120 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const TokenTracker = require('eth-token-tracker')
|
||||
const connect = require('react-redux').connect
|
||||
const selectors = require('../selectors')
|
||||
const log = require('loglevel')
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
userAddress: selectors.getSelectedAddress(state),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(TokenBalance)
|
||||
|
||||
|
||||
inherits(TokenBalance, Component)
|
||||
function TokenBalance () {
|
||||
this.state = {
|
||||
string: '',
|
||||
symbol: '',
|
||||
isLoading: true,
|
||||
error: null,
|
||||
}
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
TokenBalance.prototype.render = function () {
|
||||
const state = this.state
|
||||
const { symbol, string, isLoading } = state
|
||||
const { balanceOnly } = this.props
|
||||
|
||||
return isLoading
|
||||
? h('span', '')
|
||||
: h('span.token-balance', [
|
||||
h('span.hide-text-overflow.token-balance__amount', string),
|
||||
!balanceOnly && h('span.token-balance__symbol', symbol),
|
||||
])
|
||||
}
|
||||
|
||||
TokenBalance.prototype.componentDidMount = function () {
|
||||
this.createFreshTokenTracker()
|
||||
}
|
||||
|
||||
TokenBalance.prototype.createFreshTokenTracker = function () {
|
||||
if (this.tracker) {
|
||||
// Clean up old trackers when refreshing:
|
||||
this.tracker.stop()
|
||||
this.tracker.removeListener('update', this.balanceUpdater)
|
||||
this.tracker.removeListener('error', this.showError)
|
||||
}
|
||||
|
||||
if (!global.ethereumProvider) return
|
||||
const { userAddress, token } = this.props
|
||||
|
||||
this.tracker = new TokenTracker({
|
||||
userAddress,
|
||||
provider: global.ethereumProvider,
|
||||
tokens: [token],
|
||||
pollingInterval: 8000,
|
||||
})
|
||||
|
||||
|
||||
// Set up listener instances for cleaning up
|
||||
this.balanceUpdater = this.updateBalance.bind(this)
|
||||
this.showError = error => {
|
||||
this.setState({ error, isLoading: false })
|
||||
}
|
||||
this.tracker.on('update', this.balanceUpdater)
|
||||
this.tracker.on('error', this.showError)
|
||||
|
||||
this.tracker.updateBalances()
|
||||
.then(() => {
|
||||
this.updateBalance(this.tracker.serialize())
|
||||
})
|
||||
.catch((reason) => {
|
||||
log.error(`Problem updating balances`, reason)
|
||||
this.setState({ isLoading: false })
|
||||
})
|
||||
}
|
||||
|
||||
TokenBalance.prototype.componentDidUpdate = function (nextProps) {
|
||||
const {
|
||||
userAddress: oldAddress,
|
||||
token: { address: oldTokenAddress },
|
||||
} = this.props
|
||||
const {
|
||||
userAddress: newAddress,
|
||||
token: { address: newTokenAddress },
|
||||
} = nextProps
|
||||
|
||||
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return
|
||||
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return
|
||||
|
||||
this.setState({ isLoading: true })
|
||||
this.createFreshTokenTracker()
|
||||
}
|
||||
|
||||
TokenBalance.prototype.updateBalance = function (tokens = []) {
|
||||
if (!this.tracker.running) {
|
||||
return
|
||||
}
|
||||
|
||||
const [{ string, symbol }] = tokens
|
||||
|
||||
this.setState({
|
||||
string,
|
||||
symbol,
|
||||
isLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
TokenBalance.prototype.componentWillUnmount = function () {
|
||||
if (!this.tracker) return
|
||||
this.tracker.stop()
|
||||
this.tracker.removeListener('update', this.balanceUpdater)
|
||||
this.tracker.removeListener('error', this.showError)
|
||||
}
|
||||
|
1
ui/app/components/token-balance/index.js
Normal file
1
ui/app/components/token-balance/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './token-balance.container'
|
23
ui/app/components/token-balance/token-balance.component.js
Normal file
23
ui/app/components/token-balance/token-balance.component.js
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
|
||||
export default class TokenBalance extends PureComponent {
|
||||
static propTypes = {
|
||||
string: PropTypes.string,
|
||||
symbol: PropTypes.string,
|
||||
error: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
withSymbol: PropTypes.bool,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, string, withSymbol, symbol } = this.props
|
||||
|
||||
return (
|
||||
<div className={classnames('hide-text-overflow', className)}>
|
||||
{ string + (withSymbol ? ` ${symbol}` : '') }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { compose } from 'recompose'
|
||||
import withTokenTracker from '../../../../helpers/with-token-tracker'
|
||||
import withTokenTracker from '../../higher-order-components/with-token-tracker'
|
||||
import TokenBalance from './token-balance.component'
|
||||
import selectors from '../../../../selectors'
|
||||
import selectors from '../../selectors'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
1
ui/app/components/token-currency-display/index.js
Normal file
1
ui/app/components/token-currency-display/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './token-currency-display.component'
|
@ -0,0 +1,54 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import CurrencyDisplay from '../currency-display/currency-display.component'
|
||||
import { getTokenData } from '../../helpers/transactions.util'
|
||||
import { calcTokenAmount } from '../../token-util'
|
||||
|
||||
export default class TokenCurrencyDisplay extends PureComponent {
|
||||
static propTypes = {
|
||||
transactionData: PropTypes.string,
|
||||
token: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
displayValue: '',
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.setDisplayValue()
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { transactionData } = this.props
|
||||
const { transactionData: prevTransactionData } = prevProps
|
||||
|
||||
if (transactionData !== prevTransactionData) {
|
||||
this.setDisplayValue()
|
||||
}
|
||||
}
|
||||
|
||||
setDisplayValue () {
|
||||
const { transactionData: data, token } = this.props
|
||||
const { decimals = '', symbol = '' } = token
|
||||
const tokenData = getTokenData(data)
|
||||
|
||||
let displayValue
|
||||
|
||||
if (tokenData.params && tokenData.params.length === 2) {
|
||||
const tokenValue = tokenData.params[1].value
|
||||
const tokenAmount = calcTokenAmount(tokenValue, decimals)
|
||||
displayValue = `${tokenAmount} ${symbol}`
|
||||
}
|
||||
|
||||
this.setState({ displayValue })
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<CurrencyDisplay
|
||||
{...this.props}
|
||||
displayValue={this.state.displayValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/transaction-action/index.js
Normal file
1
ui/app/components/transaction-action/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-action.component'
|
@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import TransactionAction from '../transaction-action.component'
|
||||
|
||||
describe('TransactionAction Component', () => {
|
||||
const tOrDefault = key => key
|
||||
global.eth = {
|
||||
getCode: sinon.stub().callsFake(address => {
|
||||
console.log('CALLED')
|
||||
const code = address === 'approveAddress' ? 'contract' : '0x'
|
||||
return Promise.resolve(code)
|
||||
}),
|
||||
}
|
||||
|
||||
describe('Outgoing transaction', () => {
|
||||
it('should render -- when methodData is still fetching', () => {
|
||||
const methodData = { data: {}, done: false, error: null }
|
||||
const transaction = {
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
submittedTime: 1534045442919,
|
||||
time: 1534045440641,
|
||||
txParams: {
|
||||
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0x96',
|
||||
to: '0x50a9d56c2b8ba9a5c7f2c08c3d26e0499f23a706',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(<TransactionAction
|
||||
methodData={methodData}
|
||||
transaction={transaction}
|
||||
className="transaction-action"
|
||||
/>, { context: { tOrDefault }})
|
||||
|
||||
assert.equal(wrapper.find('.transaction-action').length, 1)
|
||||
assert.equal(wrapper.text(), '--')
|
||||
})
|
||||
|
||||
it('should render Sent Ether', () => {
|
||||
const methodData = { data: {}, done: true, error: null }
|
||||
const transaction = {
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
submittedTime: 1534045442919,
|
||||
time: 1534045440641,
|
||||
txParams: {
|
||||
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0x96',
|
||||
to: 'sentEtherAddress',
|
||||
value: '0x2386f26fc10000',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(<TransactionAction
|
||||
methodData={methodData}
|
||||
transaction={transaction}
|
||||
className="transaction-action"
|
||||
/>, { context: { tOrDefault }})
|
||||
|
||||
assert.equal(wrapper.find('.transaction-action').length, 1)
|
||||
wrapper.setState({ transactionAction: 'sentEther' })
|
||||
assert.equal(wrapper.text(), 'sentEther')
|
||||
})
|
||||
|
||||
it('should render Approved', () => {
|
||||
const methodData = {
|
||||
data: {
|
||||
name: 'Approve',
|
||||
params: [
|
||||
{ type: 'address' },
|
||||
{ type: 'uint256' },
|
||||
],
|
||||
},
|
||||
done: true,
|
||||
error: null,
|
||||
}
|
||||
const transaction = {
|
||||
id: 1,
|
||||
status: 'confirmed',
|
||||
submittedTime: 1534045442919,
|
||||
time: 1534045440641,
|
||||
txParams: {
|
||||
from: '0xc5ae6383e126f901dcb06131d97a88745bfa88d6',
|
||||
gas: '0x5208',
|
||||
gasPrice: '0x3b9aca00',
|
||||
nonce: '0x96',
|
||||
to: 'approveAddress',
|
||||
value: '0x2386f26fc10000',
|
||||
data: '0x095ea7b300000000000000000000000050a9d56c2b8ba9a5c7f2c08c3d26e0499f23a7060000000000000000000000000000000000000000000000000000000000000003',
|
||||
},
|
||||
}
|
||||
|
||||
const wrapper = shallow(<TransactionAction
|
||||
methodData={methodData}
|
||||
transaction={transaction}
|
||||
className="transaction-action"
|
||||
/>, { context: { tOrDefault }})
|
||||
|
||||
assert.equal(wrapper.find('.transaction-action').length, 1)
|
||||
wrapper.setState({ transactionAction: 'approve' })
|
||||
assert.equal(wrapper.text(), 'approve')
|
||||
})
|
||||
})
|
||||
})
|
@ -0,0 +1,52 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { getTransactionActionKey } from '../../helpers/transactions.util'
|
||||
|
||||
export default class TransactionAction extends PureComponent {
|
||||
static contextTypes = {
|
||||
tOrDefault: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
transaction: PropTypes.object,
|
||||
methodData: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
transactionAction: '',
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.getTransactionAction()
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.getTransactionAction()
|
||||
}
|
||||
|
||||
async getTransactionAction () {
|
||||
const { transactionAction } = this.state
|
||||
const { transaction, methodData } = this.props
|
||||
const { data, done } = methodData
|
||||
|
||||
if (!done || transactionAction) {
|
||||
return
|
||||
}
|
||||
|
||||
const actionKey = await getTransactionActionKey(transaction, data)
|
||||
const action = actionKey && this.context.tOrDefault(actionKey)
|
||||
this.setState({ transactionAction: action })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, methodData: { done } } = this.props
|
||||
const { transactionAction } = this.state
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{ (done && transactionAction) || '--' }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/transaction-list-item/index.js
Normal file
1
ui/app/components/transaction-list-item/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-list-item.container'
|
117
ui/app/components/transaction-list-item/index.scss
Normal file
117
ui/app/components/transaction-list-item/index.scss
Normal file
@ -0,0 +1,117 @@
|
||||
.transaction-list-item {
|
||||
box-sizing: border-box;
|
||||
min-height: 74px;
|
||||
padding: 8px 20px;
|
||||
border-bottom: 1px solid $geyser;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 8px 20px 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba($alto, .2);
|
||||
}
|
||||
|
||||
&__grid {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 45px 1fr 1fr 1fr;
|
||||
grid-template-areas:
|
||||
"identicon action status primary-amount"
|
||||
"identicon nonce status secondary-amount";
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
grid-template-columns: 45px 5fr 3fr;
|
||||
grid-template-areas:
|
||||
"nonce nonce nonce"
|
||||
"identicon action primary-amount"
|
||||
"identicon status secondary-amount";
|
||||
}
|
||||
}
|
||||
|
||||
&__identicon {
|
||||
grid-area: identicon;
|
||||
grid-row: 1 / span 2;
|
||||
align-self: center;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
grid-row: 2 / span 2;
|
||||
}
|
||||
}
|
||||
|
||||
&__action {
|
||||
text-transform: capitalize;
|
||||
padding: 0 8px 2px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
grid-area: action;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
&__status {
|
||||
grid-area: status;
|
||||
grid-row: 1 / span 2;
|
||||
align-self: center;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
grid-row: 3;
|
||||
}
|
||||
}
|
||||
|
||||
&__nonce {
|
||||
font-size: .75rem;
|
||||
color: #5e6064;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
grid-area: nonce;
|
||||
align-self: start;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__amount {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&--primary {
|
||||
text-align: end;
|
||||
grid-area: primary-amount;
|
||||
align-self: end;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
text-align: end;
|
||||
font-size: .75rem;
|
||||
color: #5e6064;
|
||||
grid-area: secondary-amount;
|
||||
align-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
&__retry {
|
||||
background: #d1edff;
|
||||
border-radius: 12px;
|
||||
font-size: .75rem;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
font-size: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Identicon from '../identicon'
|
||||
import TransactionStatus from '../transaction-status'
|
||||
import TransactionAction from '../transaction-action'
|
||||
import CurrencyDisplay from '../currency-display'
|
||||
import TokenCurrencyDisplay from '../token-currency-display'
|
||||
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
|
||||
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
|
||||
import { UNAPPROVED_STATUS, TOKEN_METHOD_TRANSFER } from '../../constants/transactions'
|
||||
import { ETH } from '../../constants/common'
|
||||
|
||||
export default class TransactionListItem extends PureComponent {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
transaction: PropTypes.object,
|
||||
value: PropTypes.string,
|
||||
methodData: PropTypes.object,
|
||||
showRetry: PropTypes.bool,
|
||||
retryTransaction: PropTypes.func,
|
||||
setSelectedToken: PropTypes.func,
|
||||
nonceAndDate: PropTypes.string,
|
||||
token: PropTypes.object,
|
||||
}
|
||||
|
||||
handleClick = () => {
|
||||
const { transaction, history } = this.props
|
||||
const { id, status, hash, metamaskNetworkId } = transaction
|
||||
|
||||
if (status === UNAPPROVED_STATUS) {
|
||||
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`)
|
||||
} else if (hash) {
|
||||
const prefix = prefixForNetwork(metamaskNetworkId)
|
||||
const etherscanUrl = `https://${prefix}etherscan.io/tx/${hash}`
|
||||
global.platform.openWindow({ url: etherscanUrl })
|
||||
}
|
||||
}
|
||||
|
||||
handleRetryClick = event => {
|
||||
event.stopPropagation()
|
||||
|
||||
const {
|
||||
transaction: { txParams: { to } = {} },
|
||||
methodData: { name } = {},
|
||||
setSelectedToken,
|
||||
} = this.props
|
||||
|
||||
if (name === TOKEN_METHOD_TRANSFER) {
|
||||
setSelectedToken(to)
|
||||
}
|
||||
|
||||
this.resubmit()
|
||||
}
|
||||
|
||||
resubmit () {
|
||||
const { transaction: { id }, retryTransaction, history } = this.props
|
||||
retryTransaction(id)
|
||||
.then(id => history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
|
||||
}
|
||||
|
||||
renderPrimaryCurrency () {
|
||||
const { token, transaction: { txParams: { data } = {} } = {}, value } = this.props
|
||||
|
||||
return token
|
||||
? (
|
||||
<TokenCurrencyDisplay
|
||||
className="transaction-list-item__amount transaction-list-item__amount--primary"
|
||||
token={token}
|
||||
transactionData={data}
|
||||
prefix="-"
|
||||
/>
|
||||
) : (
|
||||
<CurrencyDisplay
|
||||
className="transaction-list-item__amount transaction-list-item__amount--primary"
|
||||
value={value}
|
||||
prefix="-"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderSecondaryCurrency () {
|
||||
const { token, value } = this.props
|
||||
|
||||
return token
|
||||
? null
|
||||
: (
|
||||
<CurrencyDisplay
|
||||
className="transaction-list-item__amount transaction-list-item__amount--secondary"
|
||||
prefix="-"
|
||||
value={value}
|
||||
numberOfDecimals={2}
|
||||
currency={ETH}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
transaction,
|
||||
methodData,
|
||||
showRetry,
|
||||
nonceAndDate,
|
||||
} = this.props
|
||||
const { txParams = {} } = transaction
|
||||
|
||||
return (
|
||||
<div
|
||||
className="transaction-list-item"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<div className="transaction-list-item__grid">
|
||||
<Identicon
|
||||
className="transaction-list-item__identicon"
|
||||
address={txParams.to}
|
||||
diameter={34}
|
||||
/>
|
||||
<TransactionAction
|
||||
transaction={transaction}
|
||||
methodData={methodData}
|
||||
className="transaction-list-item__action"
|
||||
/>
|
||||
<div
|
||||
className="transaction-list-item__nonce"
|
||||
title={nonceAndDate}
|
||||
>
|
||||
{ nonceAndDate }
|
||||
</div>
|
||||
<TransactionStatus
|
||||
className="transaction-list-item__status"
|
||||
statusKey={transaction.status}
|
||||
/>
|
||||
{ this.renderPrimaryCurrency() }
|
||||
{ this.renderSecondaryCurrency() }
|
||||
</div>
|
||||
{
|
||||
showRetry && methodData.done && (
|
||||
<div
|
||||
className="transaction-list-item__retry"
|
||||
onClick={this.handleRetryClick}
|
||||
>
|
||||
<span>Taking too long? Increase the gas price on your transaction</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import withMethodData from '../../higher-order-components/with-method-data'
|
||||
import TransactionListItem from './transaction-list-item.component'
|
||||
import { setSelectedToken, retryTransaction } from '../../actions'
|
||||
import { hexToDecimal } from '../../helpers/conversions.util'
|
||||
import { formatDate } from '../../util'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { transaction: { txParams: { value, nonce } = {}, time } = {} } = ownProps
|
||||
|
||||
const nonceAndDate = nonce ? `#${hexToDecimal(nonce)} - ${formatDate(time)}` : formatDate(time)
|
||||
|
||||
return {
|
||||
value,
|
||||
nonceAndDate,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
setSelectedToken: tokenAddress => dispatch(setSelectedToken(tokenAddress)),
|
||||
retryTransaction: transactionId => dispatch(retryTransaction(transactionId)),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withMethodData,
|
||||
)(TransactionListItem)
|
1
ui/app/components/transaction-list/index.js
Normal file
1
ui/app/components/transaction-list/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-list.container'
|
46
ui/app/components/transaction-list/index.scss
Normal file
46
ui/app/components/transaction-list/index.scss
Normal file
@ -0,0 +1,46 @@
|
||||
.transaction-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
|
||||
&__completed-transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__header {
|
||||
flex: 0 0 auto;
|
||||
font-size: .875rem;
|
||||
color: $dusty-gray;
|
||||
border-bottom: 1px solid $geyser;
|
||||
padding: 16px 0 8px 20px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 8px 0 8px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__transactions {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__pending-transactions {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-rows: 35% 1fr;
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
grid-row-start: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: $silver;
|
||||
}
|
||||
}
|
117
ui/app/components/transaction-list/transaction-list.component.js
Normal file
117
ui/app/components/transaction-list/transaction-list.component.js
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import TransactionListItem from '../transaction-list-item'
|
||||
import ShapeShiftTransactionListItem from '../shift-list-item'
|
||||
import { TRANSACTION_TYPE_SHAPESHIFT } from '../../constants/transactions'
|
||||
|
||||
export default class TransactionList extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
pendingTransactions: [],
|
||||
completedTransactions: [],
|
||||
transactionToRetry: {},
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
pendingTransactions: PropTypes.array,
|
||||
completedTransactions: PropTypes.array,
|
||||
transactionToRetry: PropTypes.object,
|
||||
selectedToken: PropTypes.object,
|
||||
updateNetworkNonce: PropTypes.func,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.updateNetworkNonce()
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { pendingTransactions: prevPendingTransactions = [] } = prevProps
|
||||
const { pendingTransactions = [], updateNetworkNonce } = this.props
|
||||
|
||||
if (pendingTransactions.length > prevPendingTransactions.length) {
|
||||
updateNetworkNonce()
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowRetry = transaction => {
|
||||
const { transactionToRetry } = this.props
|
||||
const { id, submittedTime } = transaction
|
||||
return id === transactionToRetry.id && Date.now() - submittedTime > 30000
|
||||
}
|
||||
|
||||
renderTransactions () {
|
||||
const { t } = this.context
|
||||
const { pendingTransactions = [], completedTransactions = [] } = this.props
|
||||
|
||||
return (
|
||||
<div className="transaction-list__transactions">
|
||||
{
|
||||
pendingTransactions.length > 0 && (
|
||||
<div className="transaction-list__pending-transactions">
|
||||
<div className="transaction-list__header">
|
||||
{ `${t('queue')} (${pendingTransactions.length})` }
|
||||
</div>
|
||||
{
|
||||
pendingTransactions.map((transaction, index) => (
|
||||
this.renderTransaction(transaction, index)
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="transaction-list__completed-transactions">
|
||||
<div className="transaction-list__header">
|
||||
{ t('history') }
|
||||
</div>
|
||||
{
|
||||
completedTransactions.length > 0
|
||||
? completedTransactions.map((transaction, index) => (
|
||||
this.renderTransaction(transaction, index)
|
||||
))
|
||||
: this.renderEmpty()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderTransaction (transaction, index) {
|
||||
const { selectedToken } = this.props
|
||||
|
||||
return transaction.key === TRANSACTION_TYPE_SHAPESHIFT
|
||||
? (
|
||||
<ShapeShiftTransactionListItem
|
||||
{ ...transaction }
|
||||
key={`shapeshift${index}`}
|
||||
/>
|
||||
) : (
|
||||
<TransactionListItem
|
||||
transaction={transaction}
|
||||
key={transaction.id}
|
||||
showRetry={this.shouldShowRetry(transaction)}
|
||||
token={selectedToken}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
renderEmpty () {
|
||||
return (
|
||||
<div className="transaction-list__empty">
|
||||
<div className="transaction-list__empty-text">
|
||||
{ this.context.t('noTransactions') }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="transaction-list">
|
||||
{ this.renderTransactions() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import TransactionList from './transaction-list.component'
|
||||
import {
|
||||
pendingTransactionsSelector,
|
||||
submittedPendingTransactionsSelector,
|
||||
completedTransactionsSelector,
|
||||
} from '../../selectors/transactions'
|
||||
import { getSelectedAddress } from '../../selectors'
|
||||
import { selectedTokenSelector } from '../../selectors/tokens'
|
||||
import { getLatestSubmittedTxWithNonce } from '../../helpers/transactions.util'
|
||||
import { updateNetworkNonce } from '../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const pendingTransactions = pendingTransactionsSelector(state)
|
||||
const submittedPendingTransactions = submittedPendingTransactionsSelector(state)
|
||||
const networkNonce = state.appState.networkNonce
|
||||
|
||||
return {
|
||||
completedTransactions: completedTransactionsSelector(state),
|
||||
pendingTransactions,
|
||||
transactionToRetry: getLatestSubmittedTxWithNonce(submittedPendingTransactions, networkNonce),
|
||||
selectedToken: selectedTokenSelector(state),
|
||||
selectedAddress: getSelectedAddress(state),
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
updateNetworkNonce: address => dispatch(updateNetworkNonce(address)),
|
||||
}
|
||||
}
|
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => {
|
||||
const { selectedAddress, ...restStateProps } = stateProps
|
||||
const { updateNetworkNonce, ...restDispatchProps } = dispatchProps
|
||||
|
||||
return {
|
||||
...restStateProps,
|
||||
...restDispatchProps,
|
||||
...ownProps,
|
||||
updateNetworkNonce: () => updateNetworkNonce(selectedAddress),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps)
|
||||
)(TransactionList)
|
1
ui/app/components/transaction-status/index.js
Normal file
1
ui/app/components/transaction-status/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-status.component'
|
28
ui/app/components/transaction-status/index.scss
Normal file
28
ui/app/components/transaction-status/index.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.transaction-status {
|
||||
height: 26px;
|
||||
width: 81px;
|
||||
border-radius: 4px;
|
||||
background-color: #f0f0f0;
|
||||
color: #5e6064;
|
||||
font-size: .625rem;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
height: 16px;
|
||||
width: 70px;
|
||||
font-size: .5rem;
|
||||
}
|
||||
|
||||
&--confirmed {
|
||||
background-color: #eafad7;
|
||||
color: #609a1c;
|
||||
}
|
||||
|
||||
&--approved, &--submitted {
|
||||
background-color: #FFF2DB;
|
||||
color: #CA810A;
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import classnames from 'classnames'
|
||||
import {
|
||||
UNAPPROVED_STATUS,
|
||||
REJECTED_STATUS,
|
||||
APPROVED_STATUS,
|
||||
SIGNED_STATUS,
|
||||
SUBMITTED_STATUS,
|
||||
CONFIRMED_STATUS,
|
||||
FAILED_STATUS,
|
||||
DROPPED_STATUS,
|
||||
} from '../../constants/transactions'
|
||||
|
||||
const statusToClassNameHash = {
|
||||
[UNAPPROVED_STATUS]: 'transaction-status--unapproved',
|
||||
[REJECTED_STATUS]: 'transaction-status--rejected',
|
||||
[APPROVED_STATUS]: 'transaction-status--approved',
|
||||
[SIGNED_STATUS]: 'transaction-status--signed',
|
||||
[SUBMITTED_STATUS]: 'transaction-status--submitted',
|
||||
[CONFIRMED_STATUS]: 'transaction-status--confirmed',
|
||||
[FAILED_STATUS]: 'transaction-status--failed',
|
||||
[DROPPED_STATUS]: 'transaction-status--dropped',
|
||||
}
|
||||
|
||||
const statusToTextHash = {
|
||||
[APPROVED_STATUS]: 'pending',
|
||||
[SUBMITTED_STATUS]: 'pending',
|
||||
}
|
||||
|
||||
export default class TransactionStatus extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
statusKey: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, statusKey } = this.props
|
||||
const statusText = this.context.t(statusToTextHash[statusKey] || statusKey)
|
||||
|
||||
return (
|
||||
<div className={classnames('transaction-status', className, statusToClassNameHash[statusKey])}>
|
||||
{ statusText }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
1
ui/app/components/transaction-view-balance/index.js
Normal file
1
ui/app/components/transaction-view-balance/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-view-balance.container'
|
76
ui/app/components/transaction-view-balance/index.scss
Normal file
76
ui/app/components/transaction-view-balance/index.scss
Normal file
@ -0,0 +1,76 @@
|
||||
.transaction-view-balance {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
height: 54px;
|
||||
|
||||
&__balance {
|
||||
margin-left: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__token-balance {
|
||||
margin-left: 12px;
|
||||
font-size: 1.5rem;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__primary-balance {
|
||||
font-size: 1.5rem;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__secondary-balance {
|
||||
font-size: 1.15rem;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
&__balance-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__button {
|
||||
min-width: initial;
|
||||
width: 100px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
flex-direction: column;
|
||||
height: initial
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import { shallow } from 'enzyme'
|
||||
import sinon from 'sinon'
|
||||
import TokenBalance from '../../token-balance'
|
||||
import CurrencyDisplay from '../../currency-display'
|
||||
import { SEND_ROUTE } from '../../../routes'
|
||||
import TransactionViewBalance from '../transaction-view-balance.component'
|
||||
|
||||
const propsMethodSpies = {
|
||||
showDepositModal: sinon.spy(),
|
||||
}
|
||||
|
||||
const historySpies = {
|
||||
push: sinon.spy(),
|
||||
}
|
||||
|
||||
const t = (str1, str2) => str2 ? str1 + str2 : str1
|
||||
|
||||
describe('TransactionViewBalance Component', () => {
|
||||
afterEach(() => {
|
||||
propsMethodSpies.showDepositModal.resetHistory()
|
||||
historySpies.push.resetHistory()
|
||||
})
|
||||
|
||||
it('should render ETH balance properly', () => {
|
||||
const wrapper = shallow(<TransactionViewBalance
|
||||
showDepositModal={propsMethodSpies.showDepositModal}
|
||||
history={historySpies}
|
||||
network="3"
|
||||
ethBalance={123}
|
||||
fiatBalance={456}
|
||||
currentCurrency="usd"
|
||||
/>, { context: { t } })
|
||||
|
||||
assert.equal(wrapper.find('.transaction-view-balance').length, 1)
|
||||
assert.equal(wrapper.find('.transaction-view-balance__button').length, 2)
|
||||
assert.equal(wrapper.find(CurrencyDisplay).length, 2)
|
||||
|
||||
const buttons = wrapper.find('.transaction-view-balance__buttons')
|
||||
assert.equal(propsMethodSpies.showDepositModal.callCount, 0)
|
||||
buttons.childAt(0).simulate('click')
|
||||
assert.equal(propsMethodSpies.showDepositModal.callCount, 1)
|
||||
assert.equal(historySpies.push.callCount, 0)
|
||||
buttons.childAt(1).simulate('click')
|
||||
assert.equal(historySpies.push.callCount, 1)
|
||||
assert.equal(historySpies.push.getCall(0).args[0], SEND_ROUTE)
|
||||
})
|
||||
|
||||
it('should render token balance properly', () => {
|
||||
const token = {
|
||||
address: '0x35865238f0bec9d5ce6abff0fdaebe7b853dfcc5',
|
||||
decimals: '2',
|
||||
symbol: 'ABC',
|
||||
}
|
||||
|
||||
const wrapper = shallow(<TransactionViewBalance
|
||||
showDepositModal={propsMethodSpies.showDepositModal}
|
||||
history={historySpies}
|
||||
network="3"
|
||||
ethBalance={123}
|
||||
fiatBalance={456}
|
||||
currentCurrency="usd"
|
||||
selectedToken={token}
|
||||
/>, { context: { t } })
|
||||
|
||||
assert.equal(wrapper.find('.transaction-view-balance').length, 1)
|
||||
assert.equal(wrapper.find('.transaction-view-balance__button').length, 1)
|
||||
assert.equal(wrapper.find(TokenBalance).length, 1)
|
||||
})
|
||||
})
|
@ -0,0 +1,94 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Button from '../button'
|
||||
import Identicon from '../identicon'
|
||||
import TokenBalance from '../token-balance'
|
||||
import CurrencyDisplay from '../currency-display'
|
||||
import { SEND_ROUTE } from '../../routes'
|
||||
import { ETH } from '../../constants/common'
|
||||
|
||||
export default class TransactionViewBalance extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
showDepositModal: PropTypes.func,
|
||||
selectedToken: PropTypes.object,
|
||||
history: PropTypes.object,
|
||||
network: PropTypes.string,
|
||||
balance: PropTypes.string,
|
||||
}
|
||||
|
||||
renderBalance () {
|
||||
const { selectedToken, balance } = this.props
|
||||
|
||||
return selectedToken
|
||||
? (
|
||||
<TokenBalance
|
||||
token={selectedToken}
|
||||
withSymbol
|
||||
className="transaction-view-balance__token-balance"
|
||||
/>
|
||||
) : (
|
||||
<div className="transaction-view-balance__balance">
|
||||
<CurrencyDisplay
|
||||
className="transaction-view-balance__primary-balance"
|
||||
value={balance}
|
||||
currency={ETH}
|
||||
numberOfDecimals={3}
|
||||
/>
|
||||
<CurrencyDisplay
|
||||
className="transaction-view-balance__secondary-balance"
|
||||
value={balance}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderButtons () {
|
||||
const { t } = this.context
|
||||
const { selectedToken, showDepositModal, history } = this.props
|
||||
|
||||
return (
|
||||
<div className="transaction-view-balance__buttons">
|
||||
{
|
||||
!selectedToken && (
|
||||
<Button
|
||||
type="primary"
|
||||
className="transaction-view-balance__button"
|
||||
onClick={() => showDepositModal()}
|
||||
>
|
||||
{ t('deposit') }
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
type="primary"
|
||||
className="transaction-view-balance__button"
|
||||
onClick={() => history.push(SEND_ROUTE)}
|
||||
>
|
||||
{ t('send') }
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { network, selectedToken } = this.props
|
||||
|
||||
return (
|
||||
<div className="transaction-view-balance">
|
||||
<div className="transaction-view-balance__balance-container">
|
||||
<Identicon
|
||||
diameter={50}
|
||||
address={selectedToken && selectedToken.address}
|
||||
network={network}
|
||||
/>
|
||||
{ this.renderBalance() }
|
||||
</div>
|
||||
{ this.renderButtons() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { compose } from 'recompose'
|
||||
import TransactionViewBalance from './transaction-view-balance.component'
|
||||
import { getSelectedToken, getSelectedAddress } from '../../selectors'
|
||||
import { showModal } from '../../actions'
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const selectedAddress = getSelectedAddress(state)
|
||||
const { metamask: { network, accounts } } = state
|
||||
const account = accounts[selectedAddress]
|
||||
const { balance } = account
|
||||
|
||||
return {
|
||||
selectedToken: getSelectedToken(state),
|
||||
network,
|
||||
balance,
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
|
||||
}
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(TransactionViewBalance)
|
1
ui/app/components/transaction-view/index.js
Normal file
1
ui/app/components/transaction-view/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './transaction-view.component'
|
27
ui/app/components/transaction-view/index.scss
Normal file
27
ui/app/components/transaction-view/index.scss
Normal file
@ -0,0 +1,27 @@
|
||||
.transaction-view {
|
||||
flex: 1 1 66.5%;
|
||||
background: $white;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&__balance-wrapper {
|
||||
@media screen and (max-width: $break-small) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 0 0 auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $break-large) {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin: 2.3em 2.37em .8em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Media from 'react-media'
|
||||
import MenuBar from '../menu-bar'
|
||||
import TransactionViewBalance from '../transaction-view-balance'
|
||||
import TransactionList from '../transaction-list'
|
||||
|
||||
export default class TransactionView extends PureComponent {
|
||||
static contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="transaction-view">
|
||||
<Media
|
||||
query="(max-width: 575px)"
|
||||
render={() => <MenuBar />}
|
||||
/>
|
||||
<div className="transaction-view__balance-wrapper">
|
||||
<TransactionViewBalance />
|
||||
</div>
|
||||
<TransactionList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,356 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const { compose } = require('recompose')
|
||||
const { withRouter } = require('react-router-dom')
|
||||
const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const inherits = require('util').inherits
|
||||
const classnames = require('classnames')
|
||||
const abi = require('human-standard-token-abi')
|
||||
const abiDecoder = require('abi-decoder')
|
||||
abiDecoder.addABI(abi)
|
||||
const Identicon = require('./identicon')
|
||||
const contractMap = require('eth-contract-metadata')
|
||||
const { checksumAddress } = require('../util')
|
||||
|
||||
const actions = require('../actions')
|
||||
const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
|
||||
const { calcTokenAmount } = require('../token-util')
|
||||
|
||||
const { getCurrentCurrency } = require('../selectors')
|
||||
const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
|
||||
|
||||
TxListItem.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(TxListItem)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
tokens: state.metamask.tokens,
|
||||
currentCurrency: getCurrentCurrency(state),
|
||||
contractExchangeRates: state.metamask.contractExchangeRates,
|
||||
selectedAddressTxList: state.metamask.selectedAddressTxList,
|
||||
networkNonce: state.appState.networkNonce,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
setSelectedToken: tokenAddress => dispatch(actions.setSelectedToken(tokenAddress)),
|
||||
retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)),
|
||||
}
|
||||
}
|
||||
|
||||
inherits(TxListItem, Component)
|
||||
function TxListItem () {
|
||||
Component.call(this)
|
||||
|
||||
this.state = {
|
||||
total: null,
|
||||
fiatTotal: null,
|
||||
isTokenTx: null,
|
||||
}
|
||||
|
||||
this.unmounted = false
|
||||
}
|
||||
|
||||
TxListItem.prototype.componentDidMount = async function () {
|
||||
const { txParams = {} } = this.props
|
||||
|
||||
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
|
||||
const { name: txDataName } = decodedData || {}
|
||||
const isTokenTx = txDataName === 'transfer'
|
||||
|
||||
const { total, fiatTotal } = isTokenTx
|
||||
? await this.getSendTokenTotal()
|
||||
: this.getSendEtherTotal()
|
||||
|
||||
if (this.unmounted) {
|
||||
return
|
||||
}
|
||||
this.setState({ total, fiatTotal, isTokenTx })
|
||||
}
|
||||
|
||||
TxListItem.prototype.componentWillUnmount = function () {
|
||||
this.unmounted = true
|
||||
}
|
||||
|
||||
TxListItem.prototype.getAddressText = function () {
|
||||
const {
|
||||
address,
|
||||
txParams = {},
|
||||
isMsg,
|
||||
} = this.props
|
||||
|
||||
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
|
||||
const { name: txDataName, params = [] } = decodedData || {}
|
||||
const { value } = params[0] || {}
|
||||
const checksummedAddress = checksumAddress(address)
|
||||
const checksummedValue = checksumAddress(value)
|
||||
|
||||
let addressText
|
||||
if (txDataName === 'transfer' || address) {
|
||||
const addressToRender = txDataName === 'transfer' ? checksummedValue : checksummedAddress
|
||||
addressText = `${addressToRender.slice(0, 10)}...${addressToRender.slice(-4)}`
|
||||
} else if (isMsg) {
|
||||
addressText = this.context.t('sigRequest')
|
||||
} else {
|
||||
addressText = this.context.t('contractDeployment')
|
||||
}
|
||||
|
||||
return addressText
|
||||
}
|
||||
|
||||
TxListItem.prototype.getSendEtherTotal = function () {
|
||||
const {
|
||||
transactionAmount,
|
||||
conversionRate,
|
||||
address,
|
||||
currentCurrency,
|
||||
} = this.props
|
||||
|
||||
if (!address) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const totalInFiat = conversionUtil(transactionAmount, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromCurrency: 'ETH',
|
||||
toCurrency: currentCurrency,
|
||||
fromDenomination: 'WEI',
|
||||
numberOfDecimals: 2,
|
||||
conversionRate,
|
||||
})
|
||||
const totalInETH = conversionUtil(transactionAmount, {
|
||||
fromNumericBase: 'hex',
|
||||
toNumericBase: 'dec',
|
||||
fromCurrency: 'ETH',
|
||||
toCurrency: 'ETH',
|
||||
fromDenomination: 'WEI',
|
||||
conversionRate,
|
||||
numberOfDecimals: 6,
|
||||
})
|
||||
|
||||
return {
|
||||
total: `${totalInETH} ETH`,
|
||||
fiatTotal: `${totalInFiat} ${currentCurrency.toUpperCase()}`,
|
||||
}
|
||||
}
|
||||
|
||||
TxListItem.prototype.getTokenInfo = async function () {
|
||||
const { txParams = {}, tokenInfoGetter, tokens } = this.props
|
||||
const toAddress = txParams.to
|
||||
|
||||
let decimals
|
||||
let symbol
|
||||
|
||||
({ decimals, symbol } = tokens.filter(({ address }) => address === toAddress)[0] || {})
|
||||
|
||||
if (!decimals && !symbol) {
|
||||
({ decimals, symbol } = contractMap[toAddress] || {})
|
||||
}
|
||||
|
||||
if (!decimals && !symbol) {
|
||||
({ decimals, symbol } = await tokenInfoGetter(toAddress))
|
||||
}
|
||||
|
||||
return { decimals, symbol, address: toAddress }
|
||||
}
|
||||
|
||||
TxListItem.prototype.getSendTokenTotal = async function () {
|
||||
const {
|
||||
txParams = {},
|
||||
conversionRate,
|
||||
contractExchangeRates,
|
||||
currentCurrency,
|
||||
} = this.props
|
||||
|
||||
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
|
||||
const { params = [] } = decodedData || {}
|
||||
const { value } = params[1] || {}
|
||||
const { decimals, symbol, address } = await this.getTokenInfo()
|
||||
const total = calcTokenAmount(value, decimals)
|
||||
|
||||
let tokenToFiatRate
|
||||
let totalInFiat
|
||||
|
||||
if (contractExchangeRates[address]) {
|
||||
tokenToFiatRate = multiplyCurrencies(
|
||||
contractExchangeRates[address],
|
||||
conversionRate
|
||||
)
|
||||
|
||||
totalInFiat = conversionUtil(total, {
|
||||
fromNumericBase: 'dec',
|
||||
toNumericBase: 'dec',
|
||||
fromCurrency: symbol,
|
||||
toCurrency: currentCurrency,
|
||||
numberOfDecimals: 2,
|
||||
conversionRate: tokenToFiatRate,
|
||||
})
|
||||
}
|
||||
|
||||
const showFiat = Boolean(totalInFiat) && currentCurrency.toUpperCase() !== symbol
|
||||
|
||||
return {
|
||||
total: `${total} ${symbol}`,
|
||||
fiatTotal: showFiat && `${totalInFiat} ${currentCurrency.toUpperCase()}`,
|
||||
}
|
||||
}
|
||||
|
||||
TxListItem.prototype.showRetryButton = function () {
|
||||
const {
|
||||
transactionSubmittedTime,
|
||||
selectedAddressTxList,
|
||||
transactionId,
|
||||
txParams,
|
||||
networkNonce,
|
||||
} = this.props
|
||||
if (!txParams) {
|
||||
return false
|
||||
}
|
||||
let currentTxSharesEarliestNonce = false
|
||||
const currentNonce = txParams.nonce
|
||||
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
|
||||
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
|
||||
const currentSubmittedTxs = selectedAddressTxList.filter(tx => tx.status === 'submitted')
|
||||
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
|
||||
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
|
||||
lastSubmittedTxWithCurrentNonce.id === transactionId
|
||||
if (currentSubmittedTxs.length > 0) {
|
||||
currentTxSharesEarliestNonce = currentNonce === networkNonce
|
||||
}
|
||||
|
||||
return currentTxSharesEarliestNonce && currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
|
||||
}
|
||||
|
||||
TxListItem.prototype.setSelectedToken = function (tokenAddress) {
|
||||
this.props.setSelectedToken(tokenAddress)
|
||||
}
|
||||
|
||||
TxListItem.prototype.resubmit = function () {
|
||||
const { transactionId } = this.props
|
||||
this.props.retryTransaction(transactionId)
|
||||
.then(id => this.props.history.push(`${CONFIRM_TRANSACTION_ROUTE}/${id}`))
|
||||
}
|
||||
|
||||
TxListItem.prototype.render = function () {
|
||||
const {
|
||||
transactionStatus,
|
||||
onClick,
|
||||
transactionId,
|
||||
dateString,
|
||||
address,
|
||||
className,
|
||||
txParams,
|
||||
} = this.props
|
||||
const { total, fiatTotal, isTokenTx } = this.state
|
||||
|
||||
return h(`div${className || ''}`, {
|
||||
key: transactionId,
|
||||
onClick: () => onClick && onClick(transactionId),
|
||||
}, [
|
||||
h(`div.flex-column.tx-list-item-wrapper`, {}, [
|
||||
|
||||
h('div.tx-list-date-wrapper', {
|
||||
style: {},
|
||||
}, [
|
||||
h('span.tx-list-date', {}, [
|
||||
dateString,
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div.flex-row.tx-list-content-wrapper', {
|
||||
style: {},
|
||||
}, [
|
||||
|
||||
h('div.tx-list-identicon-wrapper', {
|
||||
style: {},
|
||||
}, [
|
||||
h(Identicon, {
|
||||
address,
|
||||
diameter: 28,
|
||||
}),
|
||||
]),
|
||||
|
||||
h('div.tx-list-account-and-status-wrapper', {}, [
|
||||
h('div.tx-list-account-wrapper', {
|
||||
style: {},
|
||||
}, [
|
||||
h('span.tx-list-account', {}, [
|
||||
this.getAddressText(address),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div.tx-list-status-wrapper', {
|
||||
style: {},
|
||||
}, [
|
||||
h('span', {
|
||||
className: classnames('tx-list-status', {
|
||||
'tx-list-status--rejected': transactionStatus === 'rejected',
|
||||
'tx-list-status--failed': transactionStatus === 'failed',
|
||||
'tx-list-status--dropped': transactionStatus === 'dropped',
|
||||
}),
|
||||
},
|
||||
this.txStatusIndicator(),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
|
||||
h('div.flex-column.tx-list-details-wrapper', {
|
||||
style: {},
|
||||
}, [
|
||||
|
||||
h('span.tx-list-value', total),
|
||||
|
||||
fiatTotal && h('span.tx-list-fiat-value', fiatTotal),
|
||||
|
||||
]),
|
||||
]),
|
||||
|
||||
this.showRetryButton() && h('.tx-list-item-retry-container', {
|
||||
onClick: (event) => {
|
||||
event.stopPropagation()
|
||||
if (isTokenTx) {
|
||||
this.setSelectedToken(txParams.to)
|
||||
}
|
||||
this.resubmit()
|
||||
},
|
||||
}, [
|
||||
h('span', 'Taking too long? Increase the gas price on your transaction'),
|
||||
]),
|
||||
|
||||
]), // holding on icon from design
|
||||
])
|
||||
}
|
||||
|
||||
TxListItem.prototype.txStatusIndicator = function () {
|
||||
const { transactionStatus } = this.props
|
||||
|
||||
let name
|
||||
|
||||
if (transactionStatus === 'unapproved') {
|
||||
name = this.context.t('unapproved')
|
||||
} else if (transactionStatus === 'rejected') {
|
||||
name = this.context.t('rejected')
|
||||
} else if (transactionStatus === 'approved') {
|
||||
name = this.context.t('approved')
|
||||
} else if (transactionStatus === 'signed') {
|
||||
name = this.context.t('signed')
|
||||
} else if (transactionStatus === 'submitted') {
|
||||
name = this.context.t('submitted')
|
||||
} else if (transactionStatus === 'confirmed') {
|
||||
name = this.context.t('confirmed')
|
||||
} else if (transactionStatus === 'failed') {
|
||||
name = this.context.t('failed')
|
||||
} else if (transactionStatus === 'dropped') {
|
||||
name = this.context.t('dropped')
|
||||
}
|
||||
return name
|
||||
}
|
@ -1,171 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const connect = require('react-redux').connect
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const prefixForNetwork = require('../../lib/etherscan-prefix-for-network')
|
||||
const selectors = require('../selectors')
|
||||
const TxListItem = require('./tx-list-item')
|
||||
const ShiftListItem = require('./shift-list-item')
|
||||
const { formatDate } = require('../util')
|
||||
const { showConfTxPage, updateNetworkNonce } = require('../actions')
|
||||
const classnames = require('classnames')
|
||||
const { tokenInfoGetter } = require('../token-util')
|
||||
const { withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
const { CONFIRM_TRANSACTION_ROUTE } = require('../routes')
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(TxList)
|
||||
|
||||
TxList.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
txsToRender: selectors.transactionsSelector(state),
|
||||
conversionRate: selectors.conversionRateSelector(state),
|
||||
selectedAddress: selectors.getSelectedAddress(state),
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showConfTxPage: ({ id }) => dispatch(showConfTxPage({ id })),
|
||||
updateNetworkNonce: (address) => dispatch(updateNetworkNonce(address)),
|
||||
}
|
||||
}
|
||||
|
||||
inherits(TxList, Component)
|
||||
function TxList () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
TxList.prototype.componentWillMount = function () {
|
||||
this.tokenInfoGetter = tokenInfoGetter()
|
||||
this.props.updateNetworkNonce(this.props.selectedAddress)
|
||||
}
|
||||
|
||||
TxList.prototype.componentDidUpdate = function (prevProps) {
|
||||
const oldTxsToRender = prevProps.txsToRender
|
||||
const {
|
||||
txsToRender: newTxsToRender,
|
||||
selectedAddress,
|
||||
updateNetworkNonce,
|
||||
} = this.props
|
||||
|
||||
if (newTxsToRender.length > oldTxsToRender.length) {
|
||||
updateNetworkNonce(selectedAddress)
|
||||
}
|
||||
}
|
||||
|
||||
TxList.prototype.render = function () {
|
||||
return h('div.flex-column', [
|
||||
h('div.flex-row.tx-list-header-wrapper', [
|
||||
h('div.flex-row.tx-list-header', [
|
||||
h('div', this.context.t('transactions')),
|
||||
]),
|
||||
]),
|
||||
h('div.flex-column.tx-list-container', {}, [
|
||||
this.renderTransaction(),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
TxList.prototype.renderTransaction = function () {
|
||||
const { txsToRender, conversionRate } = this.props
|
||||
|
||||
return txsToRender.length
|
||||
? txsToRender.map((transaction, i) => this.renderTransactionListItem(transaction, conversionRate, i))
|
||||
: [h(
|
||||
'div.tx-list-item.tx-list-item--empty',
|
||||
{ key: 'tx-list-none' },
|
||||
[ this.context.t('noTransactions') ],
|
||||
)]
|
||||
}
|
||||
|
||||
// TODO: Consider moving TxListItem into a separate component
|
||||
TxList.prototype.renderTransactionListItem = function (transaction, conversionRate, index) {
|
||||
// console.log({transaction})
|
||||
// refer to transaction-list.js:line 58
|
||||
|
||||
if (transaction.key === 'shapeshift') {
|
||||
return h(ShiftListItem, { ...transaction, key: `shapeshift${index}` })
|
||||
}
|
||||
|
||||
const props = {
|
||||
dateString: formatDate(transaction.time),
|
||||
address: transaction.txParams && transaction.txParams.to,
|
||||
transactionStatus: transaction.status,
|
||||
transactionAmount: transaction.txParams && transaction.txParams.value,
|
||||
transactionId: transaction.id,
|
||||
transactionHash: transaction.hash,
|
||||
transactionNetworkId: transaction.metamaskNetworkId,
|
||||
transactionSubmittedTime: transaction.submittedTime,
|
||||
}
|
||||
|
||||
const {
|
||||
address,
|
||||
transactionStatus,
|
||||
transactionAmount,
|
||||
dateString,
|
||||
transactionId,
|
||||
transactionHash,
|
||||
transactionNetworkId,
|
||||
transactionSubmittedTime,
|
||||
} = props
|
||||
const { history } = this.props
|
||||
|
||||
const opts = {
|
||||
key: transactionId || transactionHash,
|
||||
txParams: transaction.txParams,
|
||||
isMsg: Boolean(transaction.msgParams),
|
||||
transactionStatus,
|
||||
transactionId,
|
||||
dateString,
|
||||
address,
|
||||
transactionAmount,
|
||||
transactionHash,
|
||||
conversionRate,
|
||||
tokenInfoGetter: this.tokenInfoGetter,
|
||||
transactionSubmittedTime,
|
||||
}
|
||||
|
||||
const isUnapproved = transactionStatus === 'unapproved'
|
||||
|
||||
if (isUnapproved) {
|
||||
opts.onClick = () => {
|
||||
this.props.showConfTxPage({ id: transactionId })
|
||||
history.push(CONFIRM_TRANSACTION_ROUTE)
|
||||
}
|
||||
opts.transactionStatus = this.context.t('notStarted')
|
||||
} else if (transactionHash) {
|
||||
opts.onClick = () => this.view(transactionHash, transactionNetworkId)
|
||||
}
|
||||
|
||||
opts.className = classnames('.tx-list-item', {
|
||||
'.tx-list-pending-item-container': isUnapproved,
|
||||
'.tx-list-clickable': Boolean(transactionHash) || isUnapproved,
|
||||
})
|
||||
|
||||
return h(TxListItem, opts)
|
||||
}
|
||||
|
||||
TxList.prototype.view = function (txHash, network) {
|
||||
const url = etherscanLinkFor(txHash, network)
|
||||
if (url) {
|
||||
navigateTo(url)
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo (url) {
|
||||
global.platform.openWindow({ url })
|
||||
}
|
||||
|
||||
function etherscanLinkFor (txHash, network) {
|
||||
const prefix = prefixForNetwork(network)
|
||||
return `https://${prefix}etherscan.io/tx/${txHash}`
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
const Component = require('react').Component
|
||||
const PropTypes = require('prop-types')
|
||||
const connect = require('react-redux').connect
|
||||
const h = require('react-hyperscript')
|
||||
const inherits = require('util').inherits
|
||||
const { withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
const actions = require('../actions')
|
||||
const selectors = require('../selectors')
|
||||
const { SEND_ROUTE } = require('../routes')
|
||||
const { checksumAddress: toChecksumAddress } = require('../util')
|
||||
|
||||
const BalanceComponent = require('./balance-component')
|
||||
const Tooltip = require('./tooltip')
|
||||
const TxList = require('./tx-list')
|
||||
const SelectedAccount = require('./selected-account')
|
||||
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps, mapDispatchToProps)
|
||||
)(TxView)
|
||||
|
||||
TxView.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const sidebarOpen = state.appState.sidebarOpen
|
||||
const isMascara = state.appState.isMascara
|
||||
|
||||
const identities = state.metamask.identities
|
||||
const accounts = state.metamask.accounts
|
||||
const network = state.metamask.network
|
||||
const selectedTokenAddress = state.metamask.selectedTokenAddress
|
||||
const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
|
||||
const checksumAddress = toChecksumAddress(selectedAddress)
|
||||
const identity = identities[selectedAddress]
|
||||
|
||||
return {
|
||||
sidebarOpen,
|
||||
selectedAddress,
|
||||
checksumAddress,
|
||||
selectedTokenAddress,
|
||||
selectedToken: selectors.getSelectedToken(state),
|
||||
identity,
|
||||
network,
|
||||
isMascara,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showSidebar: () => { dispatch(actions.showSidebar()) },
|
||||
hideSidebar: () => { dispatch(actions.hideSidebar()) },
|
||||
showModal: (payload) => { dispatch(actions.showModal(payload)) },
|
||||
showSendPage: () => { dispatch(actions.showSendPage()) },
|
||||
showSendTokenPage: () => { dispatch(actions.showSendTokenPage()) },
|
||||
}
|
||||
}
|
||||
|
||||
inherits(TxView, Component)
|
||||
function TxView () {
|
||||
Component.call(this)
|
||||
}
|
||||
|
||||
TxView.prototype.renderHeroBalance = function () {
|
||||
const { selectedToken } = this.props
|
||||
|
||||
return h('div.hero-balance', {}, [
|
||||
|
||||
h(BalanceComponent, { token: selectedToken }),
|
||||
|
||||
this.renderButtons(),
|
||||
])
|
||||
}
|
||||
|
||||
TxView.prototype.renderButtons = function () {
|
||||
const {selectedToken, showModal, history } = this.props
|
||||
|
||||
return !selectedToken
|
||||
? (
|
||||
h('div.flex-row.flex-center.hero-balance-buttons', [
|
||||
h('button.btn-primary.hero-balance-button', {
|
||||
onClick: () => showModal({
|
||||
name: 'DEPOSIT_ETHER',
|
||||
}),
|
||||
}, this.context.t('deposit')),
|
||||
|
||||
h('button.btn-primary.hero-balance-button', {
|
||||
style: {
|
||||
marginLeft: '0.8em',
|
||||
},
|
||||
onClick: () => history.push(SEND_ROUTE),
|
||||
}, this.context.t('send')),
|
||||
])
|
||||
)
|
||||
: (
|
||||
h('div.flex-row.flex-center.hero-balance-buttons', [
|
||||
h('button.btn-primary.hero-balance-button', {
|
||||
onClick: () => history.push(SEND_ROUTE),
|
||||
}, this.context.t('send')),
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
TxView.prototype.render = function () {
|
||||
const { hideSidebar, isMascara, showSidebar, sidebarOpen } = this.props
|
||||
const { t } = this.context
|
||||
|
||||
return h('div.tx-view.flex-column', {
|
||||
style: {},
|
||||
}, [
|
||||
|
||||
h('div.flex-row.phone-visible', {
|
||||
style: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
flex: '0 0 auto',
|
||||
marginBottom: '16px',
|
||||
padding: '5px',
|
||||
borderBottom: '1px solid #e5e5e5',
|
||||
},
|
||||
}, [
|
||||
|
||||
h(Tooltip, {
|
||||
title: t('menu'),
|
||||
position: 'bottom',
|
||||
}, [
|
||||
h('div.fa.fa-bars', {
|
||||
style: {
|
||||
fontSize: '1.3em',
|
||||
cursor: 'pointer',
|
||||
padding: '10px',
|
||||
},
|
||||
onClick: () => sidebarOpen ? hideSidebar() : showSidebar(),
|
||||
}),
|
||||
]),
|
||||
|
||||
h(SelectedAccount),
|
||||
|
||||
!isMascara && h(Tooltip, {
|
||||
title: t('openInTab'),
|
||||
position: 'bottom',
|
||||
}, [
|
||||
h('div.open-in-browser', {
|
||||
onClick: () => global.platform.openExtensionInBrowser(),
|
||||
}, [h('img', { src: 'images/popout.svg' })]),
|
||||
]),
|
||||
]),
|
||||
|
||||
this.renderHeroBalance(),
|
||||
|
||||
h(TxList),
|
||||
|
||||
])
|
||||
}
|
@ -26,6 +26,10 @@ WalletView.contextTypes = {
|
||||
t: PropTypes.func,
|
||||
}
|
||||
|
||||
WalletView.defaultProps = {
|
||||
responsiveDisplayClassname: '',
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
|
||||
return {
|
||||
@ -131,8 +135,9 @@ WalletView.prototype.render = function () {
|
||||
}
|
||||
}
|
||||
|
||||
return h('div.wallet-view.flex-column' + (responsiveDisplayClassname || ''), {
|
||||
return h('div.wallet-view.flex-column', {
|
||||
style: {},
|
||||
className: responsiveDisplayClassname,
|
||||
}, [
|
||||
|
||||
// TODO: Separate component: wallet account details
|
||||
|
1
ui/app/constants/common.js
Normal file
1
ui/app/constants/common.js
Normal file
@ -0,0 +1 @@
|
||||
export const ETH = 'ETH'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user