Refactor transactions list views. Add redesign components

feature/default_network_editable
Alexander Tseung 6 years ago
parent d733bd34fb
commit 5ee40675b9
  1. 11
      app/_locales/en/messages.json
  2. 24
      ui/app/components/index.scss
  3. 4
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js
  4. 3
      ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js
  5. 21
      ui/app/components/pages/home/home.component.js
  6. 10
      ui/app/components/pages/home/home.container.js
  7. 120
      ui/app/components/token-balance.js
  8. 1
      ui/app/components/token-view-balance/index.js
  9. 66
      ui/app/components/token-view-balance/index.scss
  10. 92
      ui/app/components/token-view-balance/token-view-balance.component.js
  11. 42
      ui/app/components/token-view-balance/token-view-balance.container.js
  12. 1
      ui/app/components/token-view/index.js
  13. 27
      ui/app/components/token-view/index.scss
  14. 28
      ui/app/components/token-view/token-view.component.js
  15. 1
      ui/app/components/transaction-action/index.js
  16. 52
      ui/app/components/transaction-action/transaction-action.component.js
  17. 4
      ui/app/components/transaction-action/transaction-action.container.js
  18. 1
      ui/app/components/transaction-list-item/index.js
  19. 71
      ui/app/components/transaction-list-item/index.scss
  20. 82
      ui/app/components/transaction-list-item/transaction-list-item.component.js
  21. 28
      ui/app/components/transaction-list-item/transaction-list-item.container.js
  22. 1
      ui/app/components/transaction-list/index.js
  23. 40
      ui/app/components/transaction-list/index.scss
  24. 90
      ui/app/components/transaction-list/transaction-list.component.js
  25. 20
      ui/app/components/transaction-list/transaction-list.container.js
  26. 6
      ui/app/components/transaction-status/index.scss
  27. 51
      ui/app/components/tx-view.js
  28. 18
      ui/app/constants/transactions.js
  29. 130
      ui/app/css/itcss/components/hero-balance.scss
  30. 2
      ui/app/css/itcss/components/index.scss
  31. 7
      ui/app/css/itcss/components/newui-sections.scss
  32. 8
      ui/app/ducks/confirm-transaction.duck.js
  33. 17
      ui/app/helpers/confirm-transaction/util.js
  34. 6
      ui/app/helpers/confirm-transaction/util.test.js
  35. 37
      ui/app/helpers/conversions.util.js
  36. 57
      ui/app/helpers/transactions.util.js
  37. 22
      ui/app/higher-order-components/with-method-data/with-method-data.component.js
  38. 4
      ui/app/i18n-provider.js
  39. 62
      ui/app/selectors.js
  40. 50
      ui/app/selectors/transactions.js

@ -451,6 +451,9 @@
"hideTokenPrompt": {
"message": "Hide Token?"
},
"history": {
"message": "History"
},
"howToDeposit": {
"message": "How would you like to deposit Ether?"
},
@ -651,7 +654,7 @@
"message": "No transaction history."
},
"noTransactions": {
"message": "No Transactions"
"message": "You have no transactions"
},
"notFound": {
"message": "Not Found"
@ -702,6 +705,9 @@
"pasteSeed": {
"message": "Paste your seed phrase here!"
},
"pending": {
"message": "Pending"
},
"personalAddressDetected": {
"message": "Personal address detected. Input the token contract address."
},
@ -894,6 +900,9 @@
"sendETH": {
"message": "Send ETH"
},
"sendEther": {
"message": "Send Ether"
},
"sendTokens": {
"message": "Send Tokens"
},

@ -1,23 +1,35 @@
@import './app-header/index';
@import './button-group/index';
@import './export-text-container/index';
@import './confirm-page-container/index';
@import './selected-account/index';
@import './export-text-container/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 './token-view/index';
@import './token-view-balance/index';
@import './transaction-list/index';
@import './transaction-list-item/index';
@import './transaction-status/index';

@ -12,12 +12,12 @@ 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 = {

@ -1,3 +0,0 @@
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'

@ -4,6 +4,7 @@ import Media from 'react-media'
import { Redirect } from 'react-router-dom'
import WalletView from '../../wallet-view'
import TxView from '../../tx-view'
import TokenView from '../../token-view'
import {
INITIALIZE_BACKUP_PHRASE_ROUTE,
RESTORE_VAULT_ROUTE,
@ -14,28 +15,17 @@ import {
export default class Home extends PureComponent {
static propTypes = {
history: PropTypes.object,
unapprovedTxs: PropTypes.object,
unapprovedMsgCount: PropTypes.number,
unapprovedPersonalMsgCount: PropTypes.number,
unapprovedTypedMessagesCount: PropTypes.number,
noActiveNotices: PropTypes.bool,
lostAccounts: PropTypes.array,
forgottenPassword: PropTypes.bool,
seedWords: PropTypes.string,
unconfirmedTransactionsCount: PropTypes.number,
}
componentDidMount () {
const {
history,
unapprovedTxs = {},
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedTypedMessagesCount = 0,
} = this.props
const { history, unconfirmedTransactionsCount = 0 } = this.props
// unapprovedTxs and unapproved messages
if (Object.keys(unapprovedTxs).length ||
unapprovedTypedMessagesCount + unapprovedMsgCount + unapprovedPersonalMsgCount > 0) {
if (unconfirmedTransactionsCount > 0) {
history.push(CONFIRM_TRANSACTION_ROUTE)
}
}
@ -69,7 +59,8 @@ export default class Home extends PureComponent {
query="(min-width: 576px)"
render={() => <WalletView />}
/>
<TxView />
<TokenView />
{/* <TxView /> */}
</div>
</div>
)

@ -2,14 +2,11 @@ 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 {
unapprovedTxs = {},
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedTypedMessagesCount = 0,
noActiveNotices,
lostAccounts,
seedWords,
@ -17,14 +14,11 @@ const mapStateToProps = state => {
const { forgottenPassword } = appState
return {
unapprovedTxs,
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
noActiveNotices,
lostAccounts,
forgottenPassword,
seedWords,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state),
}
}

@ -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)
}

@ -0,0 +1 @@
export { default } from './token-view-balance.container'

@ -0,0 +1,66 @@
.token-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;
}
}
&__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,92 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Button from '../button'
import Identicon from '../identicon'
import TokenBalance from '../token-balance'
import { SEND_ROUTE } from '../../routes'
import { formatCurrency } from '../../helpers/confirm-transaction/util'
export default class TokenViewBalance extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
showDepositModal: PropTypes.func,
selectedToken: PropTypes.object,
history: PropTypes.object,
network: PropTypes.string,
ethBalance: PropTypes.string,
fiatBalance: PropTypes.string,
currentCurrency: PropTypes.string,
}
renderBalance () {
const { selectedToken, ethBalance, fiatBalance, currentCurrency } = this.props
const formattedFiatBalance = formatCurrency(fiatBalance, currentCurrency)
return selectedToken
? (
<TokenBalance
token={selectedToken}
withSymbol
className="token-view-balance__primary-balance"
/>
) : (
<div className="token-view-balance__balance">
<div className="token-view-balance__primary-balance">
{ `${ethBalance} ETH` }
</div>
<div className="token-view-balance__secondary-balance">
{ formattedFiatBalance }
</div>
</div>
)
}
renderButtons () {
const { t } = this.context
const { selectedToken, showDepositModal, history } = this.props
return (
<div className="token-view-balance__buttons">
{
!selectedToken && (
<Button
type="primary"
className="token-view-balance__button"
onClick={() => showDepositModal()}
>
{ t('deposit') }
</Button>
)
}
<Button
type="primary"
className="token-view-balance__button"
onClick={() => history.push(SEND_ROUTE)}
>
{ t('send') }
</Button>
</div>
)
}
render () {
const { network, selectedToken } = this.props
return (
<div className="token-view-balance">
<div className="token-view-balance__balance-container">
<Identicon
diameter={50}
address={selectedToken && selectedToken.address}
network={network}
/>
{ this.renderBalance() }
</div>
{ this.renderButtons() }
</div>
)
}
}

@ -0,0 +1,42 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TokenViewBalance from './token-view-balance.component'
import { getSelectedToken, getSelectedAddress } from '../../selectors'
import { showModal } from '../../actions'
import { getValueFromWeiHex } from '../../helpers/confirm-transaction/util'
const mapStateToProps = state => {
const selectedAddress = getSelectedAddress(state)
const { metamask } = state
const { network, accounts, currentCurrency, conversionRate } = metamask
const account = accounts[selectedAddress]
const { balance: value } = account
const ethBalance = getValueFromWeiHex({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 3,
})
const fiatBalance = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
return {
selectedToken: getSelectedToken(state),
network,
ethBalance,
fiatBalance,
currentCurrency,
}
}
const mapDispatchToProps = dispatch => {
return {
showDepositModal: () => dispatch(showModal({ name: 'DEPOSIT_ETHER' })),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(TokenViewBalance)

@ -0,0 +1 @@
export { default } from './token-view.component'

@ -0,0 +1,27 @@
.token-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,28 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Media from 'react-media'
import MenuBar from '../menu-bar'
import TokenViewBalance from '../token-view-balance'
// import TransactionList from '../tx-list'
import TransactionList from '../transaction-list'
export default class TokenView extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
render () {
return (
<div className="token-view">
<Media
query="(max-width: 575px)"
render={() => <MenuBar />}
/>
<div className="token-view__balance-wrapper">
<TokenViewBalance />
</div>
<TransactionList />
</div>
)
}
}

@ -0,0 +1 @@
export { default } from './transaction-action.container'

@ -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()
}
getTransactionAction () {
const { transactionAction } = this.state
const { transaction, methodData } = this.props
const { data, isFetching } = methodData
if (isFetching || transactionAction) {
return
}
const actionKey = getTransactionActionKey(transaction, data)
const action = actionKey && this.context.tOrDefault(actionKey)
this.setState({ transactionAction: action })
}
render () {
const { className } = this.props
const { transactionAction } = this.state
return (
<div className={className}>
{ transactionAction || '--' }
</div>
)
}
}

@ -0,0 +1,4 @@
import withMethodData from '../../higher-order-components/with-method-data'
import TransactionAction from './transaction-action.component'
export default withMethodData(TransactionAction)

@ -0,0 +1 @@
export { default } from './transaction-list-item.container'

@ -0,0 +1,71 @@
.transaction-list-item {
box-sizing: border-box;
height: 74px;
padding: 0 21px;
display: flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid $geyser;
cursor: pointer;
@media screen and (max-width: $break-small) {
padding: 0 12px;
}
&__identicon-wrapper {
padding-top: 2px;
}
&__action-block {
padding: 0 8px 0 12px;
width: 180px;
@media screen and (max-width: $break-small) {
padding: 0 8px;
width: 160px;
}
}
&__action {
text-transform: capitalize;
padding-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media screen and (max-width: $break-small) {
padding-bottom: 0;
font-size: .875rem;
}
}
&__nonce {
font-size: .75rem;
color: #5e6064;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__transaction-amounts {
flex: 1;
}
&__primary-transaction-amount {
text-align: end;
@media screen and (max-width: $break-small) {
font-size: .75rem;
}
}
&__secondary-transaction-amount {
text-align: end;
font-size: .75rem;
color: #5e6064;
}
&:hover {
background: rgba($alto, .2);
}
}

@ -0,0 +1,82 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import Media from 'react-media'
import Identicon from '../identicon'
import TransactionStatus from '../transaction-status'
import TransactionAction from '../transaction-action'
import { formatDate } from '../../util'
import prefixForNetwork from '../../../lib/etherscan-prefix-for-network'
import { CONFIRM_TRANSACTION_ROUTE } from '../../routes'
import { UNAPPROVED_STATUS } from '../../constants/transactions'
import { hexToDecimal } from '../../helpers/conversions.util'
export default class TransactionListItem extends PureComponent {
static propTypes = {
history: PropTypes.object,
methodData: PropTypes.object,
transaction: PropTypes.object,
ethTransactionAmount: PropTypes.string,
fiatDisplayValue: PropTypes.string,
}
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 })
}
}
render () {
const {
transaction,
ethTransactionAmount,
fiatDisplayValue,
} = this.props
const { txParams = {} } = transaction
const nonce = hexToDecimal(txParams.nonce)
return (
<div
className="transaction-list-item"
onClick={this.handleClick}
>
<div className="transaction-list-item__identicon-wrapper">
<Media query="(max-width: 575px)">
{
matches => (
<Identicon
address={txParams.to}
diameter={matches ? 26 : 34}
/>
)
}
</Media>
</div>
<div className="transaction-list-item__action-block">
<TransactionAction
transaction={transaction}
className="transaction-list-item__action"
/>
<div className="transaction-list-item__nonce">
{ `#${nonce} - ${formatDate(transaction.time)}` }
</div>
</div>
<TransactionStatus status={transaction.status} />
<div className="transaction-list-item__transaction-amounts">
<div className="transaction-list-item__primary-transaction-amount">
{ `-${fiatDisplayValue}` }
</div>
<div className="transaction-list-item__secondary-transaction-amount">
{ `-${ethTransactionAmount} ETH` }
</div>
</div>
</div>
)
}
}

@ -0,0 +1,28 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TransactionListItem from './transaction-list-item.component'
import { getEthFromWeiHex, getValueFromWeiHex } from '../../helpers/conversions.util'
import { formatCurrency } from '../../helpers/confirm-transaction/util'
const mapStateToProps = (state, ownProps) => {
const { metamask } = state
const { currentCurrency, conversionRate } = metamask
const { transaction: { txParams: { value } = {} } = {} } = ownProps
const ethTransactionAmount = getEthFromWeiHex({ value, conversionRate })
const fiatTransactionAmount = getValueFromWeiHex({
value, conversionRate, toCurrency: currentCurrency, numberOfDecimals: 2,
})
const fiatFormattedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
const fiatDisplayValue = `${fiatFormattedAmount} ${currentCurrency.toUpperCase()}`
return {
ethTransactionAmount,
fiatDisplayValue,
}
}
export default compose(
withRouter,
connect(mapStateToProps),
)(TransactionListItem)

@ -0,0 +1 @@
export { default } from './transaction-list.container'

@ -0,0 +1,40 @@
.transaction-list {
display: flex;
flex-direction: column;
flex: 1;
overflow-y: hidden;
&__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;
}
}

@ -0,0 +1,90 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import TransactionListItem from '../transaction-list-item'
export default class TransactionList extends PureComponent {
static contextTypes = {
t: PropTypes.func,
}
static defaultProps = {
pendingTransactions: [],
completedTransactions: [],
}
static propTypes = {
pendingTransactions: PropTypes.array,
completedTransactions: PropTypes.array,
}
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('pending')} (${pendingTransactions.length})` }
</div>
{
pendingTransactions.map(transaction => {
return (
<TransactionListItem
transaction={transaction}
key={transaction.id}
/>
)
})
}
</div>
)
}
<div className="transaction-list__completed-transactions">
<div className="transaction-list__header">
{ t('history') }
</div>
{
completedTransactions.length > 0
? (
completedTransactions.map(transaction => {
return (
<TransactionListItem
transaction={transaction}
key={transaction.id}
/>
)
})
)
: this.renderEmpty()
}
</div>
</div>
)
}
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()
// pendingTransactions.length + completedTransactions.length > 0
// ? this.renderTransactions()
// : this.renderEmpty()
}
</div>
)
}
}

@ -0,0 +1,20 @@
import { connect } from 'react-redux'
import { withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import TransactionList from './transaction-list.component'
import {
pendingTransactionsSelector,
completedTransactionsSelector,
} from '../../selectors/transactions'
const mapStateToProps = state => {
return {
pendingTransactions: pendingTransactionsSelector(state),
completedTransactions: completedTransactionsSelector(state),
}
}
export default compose(
withRouter,
connect(mapStateToProps)
)(TransactionList)

@ -10,6 +10,12 @@
justify-content: center;
align-items: center;
@media screen and (max-width: $break-small) {
height: 24px;
width: 74px;
font-size: .5rem;
}
&--confirmed {
background-color: #eafad7;
color: #609a1c;

@ -15,6 +15,9 @@ const Tooltip = require('./tooltip')
const TxList = require('./tx-list')
const SelectedAccount = require('./selected-account')
import Media from 'react-media'
import MenuBar from './menu-bar'
module.exports = compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
@ -104,49 +107,11 @@ TxView.prototype.renderButtons = function () {
}
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' })]),
]),
]),
return h('div.tx-view.flex-column', [
h(Media, {
query: '(max-width: 575px)',
render: () => h(MenuBar),
}),
this.renderHeroBalance(),

@ -0,0 +1,18 @@
export const UNAPPROVED_STATUS = 'unapproved'
export const REJECTED_STATUS = 'rejected'
export const APPROVED_STATUS = 'approved'
export const SIGNED_STATUS = 'signed'
export const SUBMITTED_STATUS = 'submitted'
export const CONFIRMED_STATUS = 'confirmed'
export const FAILED_STATUS = 'failed'
export const DROPPED_STATUS = 'dropped'
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'
export const TOKEN_METHOD_TRANSFER_FROM = 'transferfrom'
export const SEND_ETHER_ACTION_KEY = 'sendEther'
export const DEPLOY_CONTRACT_ACTION_KEY = 'contractDeployment'
export const APPROVE_ACTION_KEY = 'approve'
export const SEND_TOKEN_ACTION_KEY = 'sendToken'
export const TRANSFER_FROM_ACTION_KEY = 'transferFrom'

@ -1,130 +0,0 @@
.hero-balance {
@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;
}
.balance-container {
display: flex;
margin: 0;
justify-content: flex-start;
align-items: center;
@media screen and (max-width: $break-small) {
flex-direction: column;
flex: 0 0 auto;
max-width: 100%;
}
@media screen and (min-width: $break-large) {
flex-direction: row;
flex-grow: 3;
min-width: 0;
}
}
.balance-display {
.token-amount {
color: $black;
max-width: 100%;
.token-balance {
display: flex;
}
}
@media screen and (max-width: $break-small) {
max-width: 100%;
text-align: center;
.token-amount {
font-size: 1.75rem;
margin-top: 1rem;
.token-balance {
flex-direction: column;
}
}
.fiat-amount {
font-size: 115%;
margin-top: 8.5%;
color: #a0a0a0;
}
}
@media screen and (min-width: $break-large) {
margin: 0 .8em;
justify-content: flex-start;
align-items: flex-start;
min-width: 0;
.token-amount {
font-size: 1.5rem;
}
.fiat-amount {
margin-top: .25%;
font-size: 105%;
}
}
@media #{$sub-mid-size-breakpoint-range} {
margin-left: .4em;
margin-right: .4em;
justify-content: flex-start;
align-items: flex-start;
.token-amount {
font-size: 1rem;
}
.fiat-amount {
margin-top: .25%;
font-size: 1rem;
}
}
}
.hero-balance-buttons {
@media screen and (max-width: $break-small) {
width: 100%;
// height: 100px; // needed a round number to set the heights of the buttons inside
flex: 0 0 auto;
padding: 16px 0;
}
@media screen and (min-width: $break-large) {
flex-grow: 2;
justify-content: flex-end;
}
}
}
.hero-balance-button {
min-width: initial;
width: 6rem;
@media #{$sub-mid-size-breakpoint-range} {
padding: .4rem;
width: 4rem;
display: flex;
flex: 1;
justify-content: center;
}
}

@ -19,8 +19,6 @@
@import './loading-overlay.scss';
// Balances
@import './hero-balance.scss';
@import './wallet-balance.scss';
// Tx List and Sections

@ -49,13 +49,6 @@ $wallet-view-bg: $alabaster;
}
}
.open-in-browser {
cursor: pointer;
display: flex;
justify-content: center;
padding: 10px;
}
// wallet view and sidebar
.wallet-view {

@ -6,8 +6,7 @@ import {
import {
getTokenData,
getMethodData,
getTransactionAmount,
getValueFromWeiHex,
getTransactionFee,
getHexGasTotal,
addFiat,
@ -17,6 +16,7 @@ import {
isSmartContractAddress,
} from '../helpers/confirm-transaction/util'
import { getMethodData } from '../helpers/transactions.util'
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
@ -301,10 +301,10 @@ export function updateTxDataAndCalculate (txData) {
const { txParams: { value, gas: gasLimit = '0x0', gasPrice = '0x0' } = {} } = txData
const fiatTransactionAmount = getTransactionAmount({
const fiatTransactionAmount = getValueFromWeiHex({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
const ethTransactionAmount = getTransactionAmount({
const ethTransactionAmount = getValueFromWeiHex({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
})

@ -7,9 +7,6 @@ import BigNumber from 'bignumber.js'
abiDecoder.addABI(abi)
import MethodRegistry from 'eth-method-registry'
const registry = new MethodRegistry({ provider: global.ethereumProvider })
import {
conversionUtil,
addCurrencies,
@ -23,18 +20,6 @@ export function getTokenData (data = {}) {
return abiDecoder.decodeMethod(data)
}
export async function getMethodData (data = {}) {
const prefixedData = ethUtil.addHexPrefix(data)
const fourBytePrefix = prefixedData.slice(0, 10)
const sig = await registry.lookup(fourBytePrefix)
const parsedResult = registry.parse(sig)
return {
name: parsedResult.name,
params: parsedResult.args,
}
}
export function increaseLastGasPrice (lastGasPrice) {
return ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
@ -76,7 +61,7 @@ export function addFiat (...args) {
})
}
export function getTransactionAmount ({
export function getValueFromWeiHex ({
value,
toCurrency,
conversionRate,

@ -92,9 +92,9 @@ describe('Confirm Transaction utils', () => {
})
})
describe('getTransactionAmount', () => {
describe('getValueFromWeiHex', () => {
it('should get the transaction amount in ETH', () => {
const ethTransactionAmount = utils.getTransactionAmount({
const ethTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'ETH', conversionRate: 468.58, numberOfDecimals: 6,
})
@ -102,7 +102,7 @@ describe('Confirm Transaction utils', () => {
})
it('should get the transaction amount in fiat', () => {
const fiatTransactionAmount = utils.getTransactionAmount({
const fiatTransactionAmount = utils.getValueFromWeiHex({
value: '0xde0b6b3a7640000', toCurrency: 'usd', conversionRate: 468.58, numberOfDecimals: 2,
})

@ -0,0 +1,37 @@
import { conversionUtil } from '../conversion-util'
export function hexToDecimal (hexValue) {
return conversionUtil(hexValue, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
}
export function getEthFromWeiHex ({
value,
conversionRate,
}) {
return getValueFromWeiHex({
value,
conversionRate,
toCurrency: 'ETH',
numberOfDecimals: 6,
})
}
export function getValueFromWeiHex ({
value,
toCurrency,
conversionRate,
numberOfDecimals,
}) {
return conversionUtil(value, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
fromCurrency: 'ETH',
toCurrency,
numberOfDecimals,
fromDenomination: 'WEI',
conversionRate,
})
}

@ -0,0 +1,57 @@
import ethUtil from 'ethereumjs-util'
import MethodRegistry from 'eth-method-registry'
const registry = new MethodRegistry({ provider: global.ethereumProvider })
import {
TOKEN_METHOD_TRANSFER,
TOKEN_METHOD_APPROVE,
TOKEN_METHOD_TRANSFER_FROM,
SEND_ETHER_ACTION_KEY,
DEPLOY_CONTRACT_ACTION_KEY,
APPROVE_ACTION_KEY,
SEND_TOKEN_ACTION_KEY,
TRANSFER_FROM_ACTION_KEY,
} from '../constants/transactions'
export function isConfirmDeployContract (txData = {}) {
const { txParams = {} } = txData
return !txParams.to
}
export function getTransactionActionKey (transaction, methodData) {
const { txParams: { data } = {} } = transaction
if (isConfirmDeployContract(transaction)) {
return DEPLOY_CONTRACT_ACTION_KEY
}
if (data) {
const { name } = methodData
const methodName = name && name.toLowerCase()
switch (methodName) {
case TOKEN_METHOD_TRANSFER:
return SEND_TOKEN_ACTION_KEY
case TOKEN_METHOD_APPROVE:
return APPROVE_ACTION_KEY
case TOKEN_METHOD_TRANSFER_FROM:
return TRANSFER_FROM_ACTION_KEY
default:
return name
}
} else {
return SEND_ETHER_ACTION_KEY
}
}
export async function getMethodData (data = {}) {
const prefixedData = ethUtil.addHexPrefix(data)
const fourBytePrefix = prefixedData.slice(0, 10)
const sig = await registry.lookup(fourBytePrefix)
const parsedResult = registry.parse(sig)
return {
name: parsedResult.name,
params: parsedResult.args,
}
}

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'
import PropTypes from 'prop-types'
import { getMethodData } from '../../helpers/confirm-transaction/util'
import { getMethodData } from '../../helpers/transactions.util'
export default function withMethodData (WrappedComponent) {
return class MethodDataWrappedComponent extends PureComponent {
@ -13,7 +13,11 @@ export default function withMethodData (WrappedComponent) {
}
state = {
methodData: {},
methodData: {
data: {},
},
isFetching: false,
error: null,
}
componentDidMount () {
@ -25,18 +29,24 @@ export default function withMethodData (WrappedComponent) {
const { txParams: { data = '' } = {} } = transaction
if (data) {
const methodData = await getMethodData(data)
this.setState({ methodData })
this.setState({ isFetching: true })
try {
const methodData = await getMethodData(data)
this.setState({ methodData, isFetching: false })
} catch (error) {
this.setState({ isFetching: false, error })
}
}
}
render () {
const { methodData } = this.state
const { methodData, isFetching, error } = this.state
return (
<WrappedComponent
{ ...this.props }
methodData={methodData}
methodData={{ data: methodData, isFetching, error }}
/>
)
}

@ -13,6 +13,9 @@ class I18nProvider extends Component {
t (key, ...args) {
return t(current, key, ...args) || t(en, key, ...args) || `[${key}]`
},
tOrDefault (key, ...args) {
return t(current, key, ...args) || t(en, key, ...args) || key
},
}
}
@ -28,6 +31,7 @@ I18nProvider.propTypes = {
I18nProvider.childContextTypes = {
t: PropTypes.func,
tOrDefault: PropTypes.func,
}
const mapStateToProps = state => {

@ -1,5 +1,9 @@
const valuesFor = require('./util').valuesFor
const abi = require('human-standard-token-abi')
import { createSelector } from 'reselect'
import {
transactionsSelector,
} from './selectors/transactions'
const {
multiplyCurrencies,
@ -101,21 +105,49 @@ function getCurrentAccountWithSendEtherInfo (state) {
return accounts.find(({ address }) => address === currentAddress)
}
function transactionsSelector (state) {
const { network, selectedTokenAddress } = state.metamask
const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
const shapeShiftTxList = (network === '1') ? state.metamask.shapeShiftTxList : undefined
const transactions = state.metamask.selectedAddressTxList || []
const txsToRender = !shapeShiftTxList ? transactions.concat(unapprovedMsgs) : transactions.concat(unapprovedMsgs, shapeShiftTxList)
// // function shapeShiftTxListSelector (state) {
// // return state.metamask.shapeShiftTxList || []
// // }
// const transactionsSelector = createSelector(
// selectedTokenAddressSelector,
// unapprovedMsgsSelector,
// shapeShiftTxListSelector,
// selectedAddressTxListSelector,
// (selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => {
// const unapprovedMsgsList = valuesFor(unapprovedMsgs)
// const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList)
// return selectedTokenAddress
// ? txsToRender
// .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
// .sort((a, b) => b.time - a.time)
// : txsToRender
// .sort((a, b) => b.time - a.time)
// }
// )
// // function transactionsSelector (state) {
// // const { selectedTokenAddress } = state.metamask
// // const unapprovedMsgs = valuesFor(state.metamask.unapprovedMsgs)
// // const shapeShiftTxList = shapeShiftTxListSelector(state)
// // const transactions = state.metamask.selectedAddressTxList || []
// // const txsToRender = transactions.concat(unapprovedMsgs, shapeShiftTxList)
// // return selectedTokenAddress
// // ? txsToRender
// // .filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
// // .sort((a, b) => b.time - a.time)
// // : txsToRender
// // .sort((a, b) => b.time - a.time)
// // }
export const pendingTransactionsSelector = createSelector(
transactionsSelector,
transactions => {
// console.log({txsToRender, selectedTokenAddress})
return selectedTokenAddress
? txsToRender
.filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
.sort((a, b) => b.time - a.time)
: txsToRender
.sort((a, b) => b.time - a.time)
}
}
)
function getGasIsLoading (state) {
return state.appState.gasIsLoading

@ -0,0 +1,50 @@
import { createSelector } from 'reselect'
import { valuesFor } from '../util'
import {
UNAPPROVED_STATUS,
APPROVED_STATUS,
SUBMITTED_STATUS,
} from '../constants/transactions'
export const shapeShiftTxListSelector = state => state.metamask.shapeShiftTxList
export const selectedTokenAddressSelector = state => state.metamask.selectedTokenAddress
export const unapprovedMsgsSelector = state => state.metamask.unapprovedMsgs
export const selectedAddressTxListSelector = state => state.metamask.selectedAddressTxList
const pendingStatusHash = {
[UNAPPROVED_STATUS]: true,
[APPROVED_STATUS]: true,
[SUBMITTED_STATUS]: true,
}
export const transactionsSelector = createSelector(
selectedTokenAddressSelector,
unapprovedMsgsSelector,
shapeShiftTxListSelector,
selectedAddressTxListSelector,
(selectedTokenAddress, unapprovedMsgs = {}, shapeShiftTxList = [], transactions = []) => {
const unapprovedMsgsList = valuesFor(unapprovedMsgs)
const txsToRender = transactions.concat(unapprovedMsgsList, shapeShiftTxList)
return selectedTokenAddress
? txsToRender
.filter(({ txParams }) => txParams && txParams.to === selectedTokenAddress)
.sort((a, b) => b.time - a.time)
: txsToRender
.sort((a, b) => b.time - a.time)
}
)
export const pendingTransactionsSelector = createSelector(
transactionsSelector,
(transactions = []) => (
transactions.filter(transaction => transaction.status in pendingStatusHash)
)
)
export const completedTransactionsSelector = createSelector(
transactionsSelector,
(transactions = []) => (
transactions.filter(transaction => !(transaction.status in pendingStatusHash))
)
)
Loading…
Cancel
Save