aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEsteban Miño <efmino@uc.cl>2018-07-28 04:07:08 +0800
committerGitHub <noreply@github.com>2018-07-28 04:07:08 +0800
commit228f48c6e5423cbf8cfd5d48a3f6f106448cc22e (patch)
tree19a1aa5a5d8ebe349c4807d247f6e317d2ea0852
parent5b9725d1f154e8ed0994c3aab844f696937b0e6e (diff)
parent1540b5e256634dbbbc01627ca45afe82329d39a9 (diff)
downloadtangerine-wallet-browser-228f48c6e5423cbf8cfd5d48a3f6f106448cc22e.tar.gz
tangerine-wallet-browser-228f48c6e5423cbf8cfd5d48a3f6f106448cc22e.tar.zst
tangerine-wallet-browser-228f48c6e5423cbf8cfd5d48a3f6f106448cc22e.zip
Merge branch 'develop' into TokensPerAccountBasis
-rw-r--r--.github/CODEOWNERS6
-rw-r--r--app/_locales/index.json4
-rw-r--r--app/_locales/ja/messages.json33
-rw-r--r--app/images/ethereum-metamask-chrome.pngbin0 -> 60022 bytes
-rw-r--r--app/manifest.json3
-rw-r--r--app/phishing.html60
-rw-r--r--app/scripts/contentscript.js6
-rw-r--r--app/scripts/controllers/transactions/tx-gas-utils.js10
-rw-r--r--app/scripts/migrations/index.js1
-rw-r--r--old-ui/app/components/transaction-list-item.js11
-rw-r--r--ui/app/components/button-group/button-group.component.js61
-rw-r--r--ui/app/components/button-group/button-group.stories.js49
-rw-r--r--ui/app/components/button-group/index.js1
-rw-r--r--ui/app/components/button-group/index.scss38
-rw-r--r--ui/app/components/button-group/tests/button-group-component.test.js97
-rw-r--r--ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js64
-rw-r--r--ui/app/components/index.scss2
-rw-r--r--ui/app/components/tx-list-item.js11
18 files changed, 439 insertions, 18 deletions
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 000000000..1cdadda65
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1,6 @@
+# Lines starting with '#' are comments.
+# Each line is a file pattern followed by one or more owners.
+
+ui/ @danjm @alextsg @whymarrh
+app/scripts/controllers/transactions @frankiebee
+
diff --git a/app/_locales/index.json b/app/_locales/index.json
index 7717502b7..f50c09f88 100644
--- a/app/_locales/index.json
+++ b/app/_locales/index.json
@@ -17,6 +17,6 @@
{ "code": "tml", "name": "Tamil" },
{ "code": "tr", "name": "Turkish" },
{ "code": "vi", "name": "Vietnamese" },
- { "code": "zh_CN", "name": "Mandarin" },
- { "code": "zh_TW", "name": "Taiwanese" }
+ { "code": "zh_CN", "name": "Chinese (Simplified)" },
+ { "code": "zh_TW", "name": "Chinese (Traditional)" }
]
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 75deeaddf..c9d192139 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -122,6 +122,9 @@
"copy": {
"message": "コピー"
},
+ "copyContractAddress": {
+ "message": "コントラクトアドレスをコピー"
+ },
"copyToClipboard": {
"message": "クリップボードへコピー"
},
@@ -395,6 +398,9 @@
"mainnet": {
"message": "Ethereumメインネットワーク"
},
+ "menu": {
+ "message": "メニュー"
+ },
"message": {
"message": "メッセージ"
},
@@ -464,6 +470,9 @@
"oldUIMessage": {
"message": "旧UIを表示しています。右上のドロップダウンメニューのオプションより、新UIへ切り替えが可能です。"
},
+ "openInTab": {
+ "message": "タブを開く"
+ },
"or": {
"message": "または",
"description": "choice between creating or importing a new account"
@@ -573,6 +582,15 @@
"searchResults": {
"message": "検索結果"
},
+ "newPassword8Chars": {
+ "message": "新しいパスワード (8桁以上)"
+ },
+ "select": {
+ "message": "選択"
+ },
+ "selectCurrency": {
+ "message": "通貨を選択"
+ },
"selectService": {
"message": "サービスを選択"
},
@@ -586,10 +604,14 @@
"message": "ETHの送信"
},
"sendTokens": {
- "message": "トークンを送る"
+ "message": "トークンを送信"
},
"onlySendToEtherAddress": {
- "message": "ETHはイーサリウムアカウントのみに送信できます。"
+ "message": "ETH はイーサリウムアカウントのみに送信できます。"
+ },
+ "onlySendTokensToAccountAddress": {
+ "message": "$1 はイーサリアムアカウントのみに送信できます。",
+ "description": "displays token symbol"
},
"searchTokens": {
"message": "トークンの検索"
@@ -690,10 +712,10 @@
"message": "パスワードの入力"
},
"uiWelcome": {
- "message": "新UIへようこそ!(ベータ版)"
+ "message": "新UIへようこそ! (ベータ版)"
},
"uiWelcomeMessage": {
- "message": "現在Metamaskの新しいUIをお使いになっています。トークン送信など、新たな機能を試してみましょう!何か問題があればご報告ください。"
+ "message": "現在、MetaMask の新しいUIをお使いになっています。トークン送信など、新たな機能を試してみましょう! 何か問題があればご報告ください。"
},
"unavailable": {
"message": "有効ではありません。"
@@ -720,6 +742,9 @@
"viewAccount": {
"message": "アカウントを見る"
},
+ "viewOnEtherscan": {
+ "message": "Etherscan で見る"
+ },
"warning": {
"message": "警告"
},
diff --git a/app/images/ethereum-metamask-chrome.png b/app/images/ethereum-metamask-chrome.png
new file mode 100644
index 000000000..0b886babb
--- /dev/null
+++ b/app/images/ethereum-metamask-chrome.png
Binary files differ
diff --git a/app/manifest.json b/app/manifest.json
index 52256c5b7..ed328f19f 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -67,7 +67,8 @@
"notifications"
],
"web_accessible_resources": [
- "inpage.js"
+ "inpage.js",
+ "phishing.html"
],
"externally_connectable": {
"matches": [
diff --git a/app/phishing.html b/app/phishing.html
new file mode 100644
index 000000000..86f2985cc
--- /dev/null
+++ b/app/phishing.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+
+<html>
+
+ <head>
+ <title>Phishing Warning</title>
+
+ <style>
+body {
+ background: #c50000;
+ padding: 50px;
+ display: flex;
+ justify-content: center;
+ font-family: sans-serif;
+}
+.centered {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ color: white;
+ max-width: 600px;
+}
+a {
+ color: white;
+}
+ </style>
+
+ <script>
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+ ga('create', 'UA-37075177-6', 'auto');
+ ga('send', 'pageview');
+ //Send referral data to EAL
+ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+ })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
+ ga('create', 'UA-68598031-1', 'auto' {'allowLinker':true});
+ ga('send', 'pageview');
+ ga('require', 'linker');
+ ga('linker:autoLink', ['harrydenley.com', 'metamask.io'], false, true);
+ </script>
+
+ </head>
+
+ <body>
+ <div class="centered">
+
+ <img src="/images/ethereum-metamask-chrome.png" style="width:100%">
+ <h3>ATTENTION</h3>
+ <p>MetaMask believes this domain to have malicious intent and has prevented you from interacting with it.</p>
+ <p>This is because the site tested positive on the <a href="https://github.com/metamask/eth-phishing-detect">Ethereum Phishing Detector</a>.</p>
+ <p>You can turn MetaMask off to interact with this site, but it's advised not to.</p>
+ <p>If you think this domain is incorrectly flagged, <a href="https://github.com/metamask/eth-phishing-detect/issues/new">please file an issue</a>.</p>
+
+ </div>
+ </body>
+</html> \ No newline at end of file
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index 7c775fb04..b7496f318 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -178,6 +178,7 @@ function blacklistedDomainCheck () {
'adyen.com',
'gravityforms.com',
'harbourair.com',
+ 'ani.gamer.com.tw',
'blueskybooking.com',
]
var currentUrl = window.location.href
@@ -196,6 +197,7 @@ function blacklistedDomainCheck () {
* Redirects the current page to a phishing information page
*/
function redirectToPhishingWarning () {
- console.log('MetaMask - redirecting to phishing warning')
- window.location.href = 'https://metamask.io/phishing.html'
+ console.log('MetaMask - routing to Phishing Warning component')
+ let extensionURL = extension.runtime.getURL('phishing.html')
+ window.location.href = extensionURL
}
diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js
index ab4031faa..5cd0f5407 100644
--- a/app/scripts/controllers/transactions/tx-gas-utils.js
+++ b/app/scripts/controllers/transactions/tx-gas-utils.js
@@ -30,14 +30,10 @@ class TxGasUtil {
try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
} catch (err) {
- const simulationFailed = (
- err.message.includes('Transaction execution error.') ||
- err.message.includes('gas required exceeds allowance or always failing transaction')
- )
- if (simulationFailed) {
- txMeta.simulationFails = true
- return txMeta
+ txMeta.simulationFails = {
+ reason: err.message,
}
+ return txMeta
}
this.setTxGas(txMeta, block.gasLimit, estimatedGasHex)
return txMeta
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index 2499b7fd1..3b512715e 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -37,5 +37,6 @@ module.exports = [
require('./024'),
require('./025'),
require('./026'),
+ require('./027'),
require('./028'),
]
diff --git a/old-ui/app/components/transaction-list-item.js b/old-ui/app/components/transaction-list-item.js
index e9280419a..f479ce666 100644
--- a/old-ui/app/components/transaction-list-item.js
+++ b/old-ui/app/components/transaction-list-item.js
@@ -36,14 +36,23 @@ TransactionListItem.prototype.showRetryButton = function () {
return false
}
+ let currentTxIsLatest = false
const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
+ const currentSubmittedTxs = transactions.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[0]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce &&
lastSubmittedTxWithCurrentNonce.id === transaction.id
+ if (currentSubmittedTxs.length > 0) {
+ const lastTx = currentSubmittedTxs.reduce((tx1, tx2) => {
+ if (tx1.submittedTime < tx2.submittedTime) return tx1
+ return tx2
+ })
+ currentTxIsLatest = lastTx.id === transaction.id
+ }
- return currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000
+ return currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000 && currentTxIsLatest
}
TransactionListItem.prototype.render = function () {
diff --git a/ui/app/components/button-group/button-group.component.js b/ui/app/components/button-group/button-group.component.js
new file mode 100644
index 000000000..f99f710ce
--- /dev/null
+++ b/ui/app/components/button-group/button-group.component.js
@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react'
+import PropTypes from 'prop-types'
+import classnames from 'classnames'
+
+export default class ButtonGroup extends PureComponent {
+ static propTypes = {
+ defaultActiveButtonIndex: PropTypes.number,
+ disabled: PropTypes.bool,
+ children: PropTypes.array,
+ className: PropTypes.string,
+ style: PropTypes.object,
+ }
+
+ static defaultProps = {
+ className: 'button-group',
+ }
+
+ state = {
+ activeButtonIndex: this.props.defaultActiveButtonIndex || 0,
+ }
+
+ handleButtonClick (activeButtonIndex) {
+ this.setState({ activeButtonIndex })
+ }
+
+ renderButtons () {
+ const { children, disabled } = this.props
+
+ return React.Children.map(children, (child, index) => {
+ return child && (
+ <button
+ className={classnames(
+ 'button-group__button',
+ { 'button-group__button--active': index === this.state.activeButtonIndex },
+ )}
+ onClick={() => {
+ this.handleButtonClick(index)
+ child.props.onClick && child.props.onClick()
+ }}
+ disabled={disabled || child.props.disabled}
+ key={index}
+ >
+ { child.props.children }
+ </button>
+ )
+ })
+ }
+
+ render () {
+ const { className, style } = this.props
+
+ return (
+ <div
+ className={className}
+ style={style}
+ >
+ { this.renderButtons() }
+ </div>
+ )
+ }
+}
diff --git a/ui/app/components/button-group/button-group.stories.js b/ui/app/components/button-group/button-group.stories.js
new file mode 100644
index 000000000..14e1a7e49
--- /dev/null
+++ b/ui/app/components/button-group/button-group.stories.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import ButtonGroup from './'
+import Button from '../button'
+import { text, boolean } from '@storybook/addon-knobs/react'
+
+storiesOf('ButtonGroup', module)
+ .add('with Buttons', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ defaultActiveButtonIndex={1}
+ >
+ <Button
+ onClick={action('cheap')}
+ >
+ {text('Button1', 'Cheap')}
+ </Button>
+ <Button
+ onClick={action('average')}
+ >
+ {text('Button2', 'Average')}
+ </Button>
+ <Button
+ onClick={action('fast')}
+ >
+ {text('Button3', 'Fast')}
+ </Button>
+ </ButtonGroup>
+ )
+ .add('with a disabled Button', () =>
+ <ButtonGroup
+ style={{ width: '300px' }}
+ disabled={boolean('Disabled', false)}
+ >
+ <Button
+ onClick={action('enabled')}
+ >
+ {text('Button1', 'Enabled')}
+ </Button>
+ <Button
+ onClick={action('disabled')}
+ disabled
+ >
+ {text('Button2', 'Disabled')}
+ </Button>
+ </ButtonGroup>
+ )
diff --git a/ui/app/components/button-group/index.js b/ui/app/components/button-group/index.js
new file mode 100644
index 000000000..df470bd57
--- /dev/null
+++ b/ui/app/components/button-group/index.js
@@ -0,0 +1 @@
+export { default } from './button-group.component'
diff --git a/ui/app/components/button-group/index.scss b/ui/app/components/button-group/index.scss
new file mode 100644
index 000000000..29713c75b
--- /dev/null
+++ b/ui/app/components/button-group/index.scss
@@ -0,0 +1,38 @@
+.button-group {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &__button {
+ font-family: Roboto;
+ font-size: 1rem;
+ color: $tundora;
+ border-style: solid;
+ border-color: $alto;
+ border-width: 1px 1px 1px;
+ border-left: 0;
+ flex: 1;
+ padding: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:first-child {
+ border-left: 1px solid $alto;
+ border-radius: 4px 0 0 4px;
+ }
+
+ &:last-child {
+ border-radius: 0 4px 4px 0;
+ }
+
+ &--active {
+ background-color: $dodger-blue;
+ color: $white;
+ }
+
+ &:disabled {
+ opacity: .5;
+ }
+ }
+} \ No newline at end of file
diff --git a/ui/app/components/button-group/tests/button-group-component.test.js b/ui/app/components/button-group/tests/button-group-component.test.js
new file mode 100644
index 000000000..f07bb97c8
--- /dev/null
+++ b/ui/app/components/button-group/tests/button-group-component.test.js
@@ -0,0 +1,97 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import sinon from 'sinon'
+import ButtonGroup from '../button-group.component.js'
+
+const childButtonSpies = {
+ onClick: sinon.spy(),
+}
+
+sinon.spy(ButtonGroup.prototype, 'handleButtonClick')
+sinon.spy(ButtonGroup.prototype, 'renderButtons')
+
+const mockButtons = [
+ <button onClick={childButtonSpies.onClick} key={'a'}><div className="mockClass" /></button>,
+ <button onClick={childButtonSpies.onClick} key={'b'}></button>,
+ <button onClick={childButtonSpies.onClick} key={'c'}></button>,
+]
+
+describe('ButtonGroup Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<ButtonGroup
+ defaultActiveButtonIndex={1}
+ disabled={false}
+ className="someClassName"
+ style={ { color: 'red' } }
+ >{mockButtons}</ButtonGroup>)
+ })
+
+ afterEach(() => {
+ childButtonSpies.onClick.resetHistory()
+ ButtonGroup.prototype.handleButtonClick.resetHistory()
+ ButtonGroup.prototype.renderButtons.resetHistory()
+ })
+
+ describe('handleButtonClick', () => {
+ it('should set the activeButtonIndex', () => {
+ assert.equal(wrapper.state('activeButtonIndex'), 1)
+ wrapper.instance().handleButtonClick(2)
+ assert.equal(wrapper.state('activeButtonIndex'), 2)
+ })
+ })
+
+ describe('renderButtons', () => {
+ it('should render a button for each child', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ assert.equal(childButtons.length, 3)
+ })
+
+ it('should render the correct button with an active state', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ const activeChildButton = wrapper.find('.button-group__button--active')
+ assert.deepEqual(childButtons.get(1), activeChildButton.get(0))
+ })
+
+ it('should call handleButtonClick and the respective button\'s onClick method when a button is clicked', () => {
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 0)
+ assert.equal(childButtonSpies.onClick.callCount, 0)
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.at(0).props().onClick()
+ childButtons.at(1).props().onClick()
+ childButtons.at(2).props().onClick()
+ assert.equal(ButtonGroup.prototype.handleButtonClick.callCount, 3)
+ assert.equal(childButtonSpies.onClick.callCount, 3)
+ })
+
+ it('should render all child buttons as disabled if props.disabled is true', () => {
+ const childButtons = wrapper.find('.button-group__button')
+ childButtons.forEach(button => {
+ assert.equal(button.props().disabled, undefined)
+ })
+ wrapper.setProps({ disabled: true })
+ const disabledChildButtons = wrapper.find('[disabled=true]')
+ assert.equal(disabledChildButtons.length, 3)
+ })
+
+ it('should render the children of the button', () => {
+ const mockClass = wrapper.find('.mockClass')
+ assert.equal(mockClass.length, 1)
+ })
+ })
+
+ describe('render', () => {
+ it('should render a div with the expected class and style', () => {
+ assert.equal(wrapper.find('div').at(0).props().className, 'someClassName')
+ assert.deepEqual(wrapper.find('div').at(0).props().style, { color: 'red' })
+ })
+
+ it('should call renderButtons when rendering', () => {
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 1)
+ wrapper.instance().render()
+ assert.equal(ButtonGroup.prototype.renderButtons.callCount, 2)
+ })
+ })
+})
diff --git a/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js
new file mode 100644
index 000000000..6f2489071
--- /dev/null
+++ b/ui/app/components/confirm-page-container/confirm-detail-row/tests/confirm-detail-row.component.test.js
@@ -0,0 +1,64 @@
+import React from 'react'
+import assert from 'assert'
+import { shallow } from 'enzyme'
+import ConfirmDetailRow from '../confirm-detail-row.component.js'
+import sinon from 'sinon'
+
+const propsMethodSpies = {
+ onHeaderClick: sinon.spy(),
+}
+
+describe('Confirm Detail Row Component', function () {
+ let wrapper
+
+ beforeEach(() => {
+ wrapper = shallow(<ConfirmDetailRow
+ errorType={'mockErrorType'}
+ label={'mockLabel'}
+ showError={false}
+ fiatText = {'mockFiatText'}
+ ethText = {'mockEthText'}
+ fiatTextColor= {'mockColor'}
+ onHeaderClick= {propsMethodSpies.onHeaderClick}
+ headerText = {'mockHeaderText'}
+ headerTextClassName = {'mockHeaderClass'}
+ />)
+ })
+
+ describe('render', () => {
+ it('should render a div with a confirm-detail-row class', () => {
+ assert.equal(wrapper.find('div.confirm-detail-row').length, 1)
+ })
+
+ it('should render the label as a child of the confirm-detail-row__label', () => {
+ assert.equal(wrapper.find('.confirm-detail-row > .confirm-detail-row__label').childAt(0).text(), 'mockLabel')
+ })
+
+ it('should render the headerText as a child of the confirm-detail-row__header-text', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__header-text').childAt(0).text(), 'mockHeaderText')
+ })
+
+ it('should render the fiatText as a child of the confirm-detail-row__fiat', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__fiat').childAt(0).text(), 'mockFiatText')
+ })
+
+ it('should render the ethText as a child of the confirm-detail-row__eth', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__details > .confirm-detail-row__eth').childAt(0).text(), 'mockEthText')
+ })
+
+ it('should set the fiatTextColor on confirm-detail-row__fiat', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__fiat').props().style.color, 'mockColor')
+ })
+
+ it('should assure the confirm-detail-row__header-text classname is correct', () => {
+ assert.equal(wrapper.find('.confirm-detail-row__header-text').props().className, 'confirm-detail-row__header-text mockHeaderClass')
+ })
+
+ it('should call onHeaderClick when headerText div gets clicked', () => {
+ wrapper.find('.confirm-detail-row__header-text').props().onClick()
+ assert.equal(assert.equal(propsMethodSpies.onHeaderClick.callCount, 1))
+ })
+
+
+ })
+})
diff --git a/ui/app/components/index.scss b/ui/app/components/index.scss
index 32f0e90e4..b3e14ce23 100644
--- a/ui/app/components/index.scss
+++ b/ui/app/components/index.scss
@@ -1,3 +1,5 @@
+@import './button-group/index';
+
@import './export-text-container/index';
@import './selected-account/index';
diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js
index 0d693b805..1a639d0b9 100644
--- a/ui/app/components/tx-list-item.js
+++ b/ui/app/components/tx-list-item.js
@@ -213,14 +213,23 @@ TxListItem.prototype.showRetryButton = function () {
if (!txParams) {
return false
}
+ let currentTxIsLatest = 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) {
+ const lastTx = currentSubmittedTxs.reduce((tx1, tx2) => {
+ if (tx1.submittedTime < tx2.submittedTime) return tx1
+ return tx2
+ })
+ currentTxIsLatest = lastTx.id === transactionId
+ }
- return currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
+ return currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000 && currentTxIsLatest
}
TxListItem.prototype.setSelectedToken = function (tokenAddress) {