Merge remote-tracking branch 'origin/develop' into Version-v8.0.0

* origin/develop:
  Use error.value.message with error.message as fallback (#8825)
  Fix dropped tx detection (#8824)
  delete targetDomainMetadata selector
  remove top/bottom margin on info icon (#8822)
  add send token button on fullscreen asset list (#8812)
  fix getTargetDomainMetadata selector
  select current address in permissions connect (#8811)
  Fix encrypt/decrypt beforeunload bugs (#8816)
feature/default_network_editable
Mark Stacey 5 years ago
commit d730c28cd9
  1. 6
      app/scripts/controllers/transactions/pending-tx-tracker.js
  2. 6
      app/scripts/lib/decrypt-message-manager.js
  3. 6
      app/scripts/lib/encryption-public-key-manager.js
  4. 2
      test/e2e/metamask-ui.spec.js
  5. 62
      ui/app/components/app/asset-list-item/asset-list-item.js
  6. 15
      ui/app/components/app/asset-list-item/asset-list-item.scss
  7. 9
      ui/app/components/app/permission-page-container/permission-page-container.container.js
  8. 13
      ui/app/components/app/token-cell/token-cell.js
  9. 19
      ui/app/components/app/token-cell/token-cell.test.js
  10. 2
      ui/app/components/ui/icon/index.scss
  11. 40
      ui/app/pages/confirm-decrypt-message/confirm-decrypt-message.component.js
  12. 28
      ui/app/pages/confirm-encryption-public-key/confirm-encryption-public-key.component.js
  13. 8
      ui/app/pages/permissions-connect/permissions-connect.component.js
  14. 16
      ui/app/pages/permissions-connect/permissions-connect.container.js
  15. 12
      ui/app/selectors/selectors.js

@ -79,7 +79,7 @@ export default class PendingTransactionTracker extends EventEmitter {
try {
await this._resubmitTx(txMeta, blockNumber)
} catch (err) {
const errorMessage = err.message.toLowerCase()
const errorMessage = err.value?.message?.toLowerCase() || err.message.toLowerCase()
const isKnownTx = (
// geth
errorMessage.includes('replacement transaction underpriced') ||
@ -209,9 +209,9 @@ export default class PendingTransactionTracker extends EventEmitter {
*/
async _checkIfTxWasDropped (txMeta) {
const { hash: txHash, txParams: { nonce, from } } = txMeta
const networkNonce = await this.query.getTransactionCount(from)
const networkNextNonce = await this.query.getTransactionCount(from)
if (parseInt(nonce) > parseInt(networkNonce)) {
if (parseInt(nonce, 16) >= parseInt(networkNextNonce, 16)) {
return false
}

@ -82,7 +82,7 @@ export default class DecryptMessageManager extends EventEmitter {
addUnapprovedMessageAsync (msgParams, req) {
return new Promise((resolve, reject) => {
if (!msgParams.from) {
reject(new Error('MetaMask Message for Decryption: from field is required.'))
reject(new Error('MetaMask Decryption: from field is required.'))
}
const msgId = this.addUnapprovedMessage(msgParams, req)
this.once(`${msgId}:finished`, (data) => {
@ -90,11 +90,11 @@ export default class DecryptMessageManager extends EventEmitter {
case 'decrypted':
return resolve(data.rawData)
case 'rejected':
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message for Decryption: User denied message decryption.'))
return reject(ethErrors.provider.userRejectedRequest('MetaMask Decryption: User denied message decryption.'))
case 'errored':
return reject(new Error('This message cannot be decrypted'))
default:
return reject(new Error(`MetaMask Message for Decryption: Unknown problem: ${JSON.stringify(msgParams)}`))
return reject(new Error(`MetaMask Decryption: Unknown problem: ${JSON.stringify(msgParams)}`))
}
})
})

@ -79,7 +79,7 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
addUnapprovedMessageAsync (address, req) {
return new Promise((resolve, reject) => {
if (!address) {
reject(new Error('MetaMask Message for EncryptionPublicKey: address field is required.'))
reject(new Error('MetaMask Message: address field is required.'))
}
const msgId = this.addUnapprovedMessage(address, req)
this.once(`${msgId}:finished`, (data) => {
@ -87,9 +87,9 @@ export default class EncryptionPublicKeyManager extends EventEmitter {
case 'received':
return resolve(data.rawData)
case 'rejected':
return reject(ethErrors.provider.userRejectedRequest('MetaMask Message for EncryptionPublicKey: User denied message EncryptionPublicKey.'))
return reject(ethErrors.provider.userRejectedRequest('MetaMask EncryptionPublicKey: User denied message EncryptionPublicKey.'))
default:
return reject(new Error(`MetaMask Message for EncryptionPublicKey: Unknown problem: ${JSON.stringify(address)}`))
return reject(new Error(`MetaMask EncryptionPublicKey: Unknown problem: ${JSON.stringify(address)}`))
}
})
})

@ -415,8 +415,6 @@ describe('MetaMask', function () {
await driver.delay(regularDelayMs)
await driver.clickElement(By.css('.permissions-connect-choose-account__account'))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Next')]`))
await driver.clickElement(By.xpath(`//button[contains(text(), 'Connect')]`))

@ -1,10 +1,17 @@
import React from 'react'
import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import Identicon from '../../ui/identicon'
import ListItem from '../../ui/list-item'
import Tooltip from '../../ui/tooltip-v2'
import InfoIcon from '../../ui/icon/info-icon.component'
import Button from '../../ui/button'
import { useI18nContext } from '../../../hooks/useI18nContext'
import { useMetricEvent } from '../../../hooks/useMetricEvent'
import { useDispatch } from 'react-redux'
import { updateSendToken } from '../../../store/actions'
import { useHistory } from 'react-router-dom'
import { SEND_ROUTE } from '../../../helpers/constants/routes'
const AssetListItem = ({
@ -13,11 +20,23 @@ const AssetListItem = ({
iconClassName,
onClick,
tokenAddress,
tokenSymbol,
tokenDecimals,
tokenImage,
warning,
primary,
secondary,
}) => {
const t = useI18nContext()
const dispatch = useDispatch()
const history = useHistory()
const sendTokenEvent = useMetricEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Clicked Send: Token',
},
})
const titleIcon = warning
? (
<Tooltip
@ -40,6 +59,38 @@ const AssetListItem = ({
)
: null
const sendTokenButton = useMemo(() => {
if (tokenAddress == null) {
return null
}
return (
<Button
type="link"
className="asset-list-item__send-token-button"
onClick={(e) => {
e.stopPropagation()
sendTokenEvent()
dispatch(updateSendToken({
address: tokenAddress,
decimals: tokenDecimals,
symbol: tokenSymbol,
}))
history.push(SEND_ROUTE)
}}
>
{t('sendSpecifiedTokens', [tokenSymbol])}
</Button>
)
}, [
tokenSymbol,
sendTokenEvent,
tokenAddress,
tokenDecimals,
history,
t,
dispatch,
])
return (
<ListItem
className={classnames('asset-list-item', className)}
@ -57,7 +108,12 @@ const AssetListItem = ({
/>
)}
midContent={midContent}
rightContent={<i className="fas fa-chevron-right asset-list-item__chevron-right" />}
rightContent={(
<>
<i className="fas fa-chevron-right asset-list-item__chevron-right" />
{sendTokenButton}
</>
)}
/>
)
}
@ -68,6 +124,8 @@ AssetListItem.propTypes = {
iconClassName: PropTypes.string,
onClick: PropTypes.func.isRequired,
tokenAddress: PropTypes.string,
tokenSymbol: PropTypes.string,
tokenDecimals: PropTypes.number,
tokenImage: PropTypes.string,
warning: PropTypes.node,
primary: PropTypes.string,

@ -17,6 +17,13 @@
margin-left: 8px;
}
&__send-token-button {
display: none;
text-transform: uppercase;
width: fit-content;
font-size: 14px;
}
@media (min-width: 576px) {
&__warning-tooltip {
display: none;
@ -25,5 +32,13 @@
.list-item__mid-content {
display: flex;
}
&__send-token-button {
display: inline-block;
}
&__chevron-right {
display: none;
}
}
}

@ -1,19 +1,14 @@
import { connect } from 'react-redux'
import PermissionPageContainer from './permission-page-container.component'
import {
getTargetDomainMetadata,
getMetaMaskIdentities,
} from '../../../selectors'
import { getMetaMaskIdentities } from '../../../selectors'
const mapStateToProps = (state, ownProps) => {
const { request, cachedOrigin, selectedIdentities } = ownProps
const targetDomainMetadata = getTargetDomainMetadata(state, request, cachedOrigin)
const { selectedIdentities } = ownProps
const allIdentities = getMetaMaskIdentities(state)
const allIdentitiesSelected = Object.keys(selectedIdentities).length === Object.keys(allIdentities).length && selectedIdentities.length > 1
return {
targetDomainMetadata,
allIdentitiesSelected,
}
}

@ -8,7 +8,15 @@ import { useI18nContext } from '../../../hooks/useI18nContext'
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount'
export default function TokenCell ({ address, outdatedBalance, symbol, string, image, onClick }) {
export default function TokenCell ({
address,
decimals,
outdatedBalance,
symbol,
string,
image,
onClick,
}) {
const userAddress = useSelector(getSelectedAddress)
const t = useI18nContext()
@ -37,6 +45,8 @@ export default function TokenCell ({ address, outdatedBalance, symbol, string, i
onClick={onClick.bind(null, address)}
tokenAddress={address}
tokenImage={image}
tokenSymbol={symbol}
tokenDecimals={decimals}
warning={warning}
primary={`${string || 0} ${symbol}`}
secondary={formattedFiat}
@ -49,6 +59,7 @@ TokenCell.propTypes = {
address: PropTypes.string,
outdatedBalance: PropTypes.bool,
symbol: PropTypes.string,
decimals: PropTypes.number,
string: PropTypes.string,
image: PropTypes.string,
onClick: PropTypes.func.isRequired,

@ -5,6 +5,7 @@ import { Provider } from 'react-redux'
import configureMockStore from 'redux-mock-store'
import { mount } from 'enzyme'
import sinon from 'sinon'
import { MemoryRouter } from 'react-router-dom'
import TokenCell from '.'
import Identicon from '../../ui/identicon'
@ -44,14 +45,16 @@ describe('Token Cell', function () {
onClick = sinon.stub()
wrapper = mount(
<Provider store={store}>
<TokenCell
address="0xAnotherToken"
symbol="TEST"
string="5.000"
currentCurrency="usd"
image="./test-image"
onClick={onClick}
/>
<MemoryRouter>
<TokenCell
address="0xAnotherToken"
symbol="TEST"
string="5.000"
currentCurrency="usd"
image="./test-image"
onClick={onClick}
/>
</MemoryRouter>
</Provider>
)
})

@ -1,5 +1,5 @@
.info-icon {
margin: 4px;
margin: 0 4px;
&--success {
fill: $success-green;

@ -52,9 +52,14 @@ export default class ConfirmDecryptMessage extends Component {
this._removeBeforeUnload()
}
_beforeUnload = (event) => {
const { clearConfirmTransaction, cancelDecryptMessage } = this.props
_beforeUnload = async (event) => {
const {
clearConfirmTransaction,
cancelDecryptMessage,
txData,
} = this.props
const { metricsEvent } = this.context
await cancelDecryptMessage(txData, event)
metricsEvent({
eventOpts: {
category: 'Messages',
@ -63,7 +68,6 @@ export default class ConfirmDecryptMessage extends Component {
},
})
clearConfirmTransaction()
cancelDecryptMessage(event)
}
_removeBeforeUnload = () => {
@ -103,11 +107,12 @@ export default class ConfirmDecryptMessage extends Component {
renderAccount = () => {
const { fromAccount } = this.state
const { t } = this.context
return (
<div className="request-decrypt-message__account">
<div className="request-decrypt-message__account-text">
{ `${this.context.t('account')}:` }
{ `${t('account')}:` }
</div>
<div className="request-decrypt-message__account-item">
@ -123,6 +128,7 @@ export default class ConfirmDecryptMessage extends Component {
renderBalance = () => {
const { conversionRate } = this.props
const { fromAccount: { balance } } = this.state
const { t } = this.context
const balanceInEther = conversionUtil(balance, {
fromNumericBase: 'hex',
@ -135,7 +141,7 @@ export default class ConfirmDecryptMessage extends Component {
return (
<div className="request-decrypt-message__balance">
<div className="request-decrypt-message__balance-text">
{ `${this.context.t('balance')}:` }
{ `${t('balance')}:` }
</div>
<div className="request-decrypt-message__balance-value">
{ `${balanceInEther} ETH` }
@ -168,10 +174,11 @@ export default class ConfirmDecryptMessage extends Component {
}
renderBody = () => {
const { txData } = this.props
const { decryptMessageInline, domainMetadata, txData } = this.props
const { t } = this.context
const origin = this.props.domainMetadata[txData.msgParams.origin]
const notice = this.context.t('decryptMessageNotice', [origin.name])
const origin = domainMetadata[txData.msgParams.origin]
const notice = t('decryptMessageNotice', [origin.name])
const {
hasCopied,
@ -228,7 +235,7 @@ export default class ConfirmDecryptMessage extends Component {
'request-decrypt-message__message-lock--pressed': hasDecrypted || hasError,
})}
onClick={(event) => {
this.props.decryptMessageInline(txData, event).then((result) => {
decryptMessageInline(txData, event).then((result) => {
if (!result.error) {
this.setState({ hasDecrypted: true, rawMessage: result.rawData })
} else {
@ -241,7 +248,7 @@ export default class ConfirmDecryptMessage extends Component {
<div
className="request-decrypt-message__message-lock-text"
>
{this.context.t('decryptMetamask')}
{t('decryptMetamask')}
</div>
</div>
</div>
@ -258,13 +265,13 @@ export default class ConfirmDecryptMessage extends Component {
>
<Tooltip
position="bottom"
title={hasCopied ? this.context.t('copiedExclamation') : this.context.t('copyToClipboard')}
title={hasCopied ? t('copiedExclamation') : t('copyToClipboard')}
wrapperClassName="request-decrypt-message__message-copy-tooltip"
>
<div
className="request-decrypt-message__message-copy-text"
>
{this.context.t('decryptCopy')}
{t('decryptCopy')}
</div>
<img src="images/copy-to-clipboard.svg" />
</Tooltip>
@ -286,6 +293,7 @@ export default class ConfirmDecryptMessage extends Component {
mostRecentOverviewPage,
txData,
} = this.props
const { metricsEvent, t } = this.context
return (
<div className="request-decrypt-message__footer">
@ -296,7 +304,7 @@ export default class ConfirmDecryptMessage extends Component {
onClick={async (event) => {
this._removeBeforeUnload()
await cancelDecryptMessage(txData, event)
this.context.metricsEvent({
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Request',
@ -307,7 +315,7 @@ export default class ConfirmDecryptMessage extends Component {
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('cancel') }
{ t('cancel') }
</Button>
<Button
type="secondary"
@ -316,7 +324,7 @@ export default class ConfirmDecryptMessage extends Component {
onClick={async (event) => {
this._removeBeforeUnload()
await decryptMessage(txData, event)
this.context.metricsEvent({
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Decrypt Message Request',
@ -327,7 +335,7 @@ export default class ConfirmDecryptMessage extends Component {
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('decrypt') }
{ t('decrypt') }
</Button>
</div>
)

@ -46,9 +46,14 @@ export default class ConfirmEncryptionPublicKey extends Component {
this._removeBeforeUnload()
}
_beforeUnload = (event) => {
const { clearConfirmTransaction, cancelEncryptionPublicKey } = this.props
_beforeUnload = async (event) => {
const {
clearConfirmTransaction,
cancelEncryptionPublicKey,
txData,
} = this.props
const { metricsEvent } = this.context
await cancelEncryptionPublicKey(txData, event)
metricsEvent({
eventOpts: {
category: 'Messages',
@ -57,7 +62,6 @@ export default class ConfirmEncryptionPublicKey extends Component {
},
})
clearConfirmTransaction()
cancelEncryptionPublicKey(event)
}
_removeBeforeUnload = () => {
@ -84,11 +88,12 @@ export default class ConfirmEncryptionPublicKey extends Component {
renderAccount = () => {
const { fromAccount } = this.state
const { t } = this.context
return (
<div className="request-encryption-public-key__account">
<div className="request-encryption-public-key__account-text">
{ `${this.context.t('account')}:` }
{ `${t('account')}:` }
</div>
<div className="request-encryption-public-key__account-item">
@ -103,6 +108,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
renderBalance = () => {
const { conversionRate } = this.props
const { t } = this.context
const { fromAccount: { balance } } = this.state
const balanceInEther = conversionUtil(balance, {
@ -116,7 +122,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
return (
<div className="request-encryption-public-key__balance">
<div className="request-encryption-public-key__balance-text">
{ `${this.context.t('balance')}:` }
{ `${t('balance')}:` }
</div>
<div className="request-encryption-public-key__balance-value">
{ `${balanceInEther} ETH` }
@ -149,10 +155,11 @@ export default class ConfirmEncryptionPublicKey extends Component {
}
renderBody = () => {
const { txData } = this.props
const { domainMetadata, txData } = this.props
const { t } = this.context
const origin = this.props.domainMetadata[txData.origin]
const notice = this.context.t('encryptionPublicKeyNotice', [origin.name])
const origin = domainMetadata[txData.origin]
const notice = t('encryptionPublicKeyNotice', [origin.name])
return (
<div className="request-encryption-public-key__body">
@ -191,6 +198,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
mostRecentOverviewPage,
txData,
} = this.props
const { t, metricsEvent } = this.context
return (
<div className="request-encryption-public-key__footer">
@ -201,7 +209,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
onClick={async (event) => {
this._removeBeforeUnload()
await cancelEncryptionPublicKey(txData, event)
this.context.metricsEvent({
metricsEvent({
eventOpts: {
category: 'Messages',
action: 'Encryption public key Request',
@ -232,7 +240,7 @@ export default class ConfirmEncryptionPublicKey extends Component {
history.push(mostRecentOverviewPage)
}}
>
{ this.context.t('provide') }
{ t('provide') }
</Button>
</div>
)

@ -17,6 +17,7 @@ export default class PermissionConnect extends Component {
getRequestAccountTabIds: PropTypes.func.isRequired,
getCurrentWindowTab: PropTypes.func.isRequired,
accounts: PropTypes.array.isRequired,
currentAddress: PropTypes.string.isRequired,
origin: PropTypes.string,
showNewAccountModal: PropTypes.func.isRequired,
newAccountNumber: PropTypes.number.isRequired,
@ -46,9 +47,7 @@ export default class PermissionConnect extends Component {
state = {
redirecting: false,
selectedAccountAddresses: this.props.accounts.length === 1
? new Set([this.props.accounts[0].address])
: new Set(),
selectedAccountAddresses: new Set([this.props.currentAddress]),
permissionsApproved: null,
origin: this.props.origin,
targetDomainMetadata: this.props.targetDomainMetadata || {},
@ -207,7 +206,6 @@ export default class PermissionConnect extends Component {
const {
selectedAccountAddresses,
permissionsApproved,
origin,
redirecting,
targetDomainMetadata,
} = this.state
@ -258,7 +256,7 @@ export default class PermissionConnect extends Component {
}}
rejectPermissionsRequest={(requestId) => this.cancelPermissionsRequest(requestId)}
selectedIdentities={accounts.filter((account) => selectedAccountAddresses.has(account.address))}
cachedOrigin={origin}
targetDomainMetadata={targetDomainMetadata}
/>
)}
/>

@ -6,7 +6,8 @@ import {
getNativeCurrency,
getAccountsWithLabels,
getLastConnectedInfo,
getTargetDomainMetadata,
getDomainMetadata,
getSelectedAddress,
} from '../../selectors'
import { formatDate } from '../../helpers/utils/util'
@ -28,6 +29,7 @@ const mapStateToProps = (state, ownProps) => {
location: { pathname },
} = ownProps
const permissionsRequests = getPermissionsRequests(state)
const currentAddress = getSelectedAddress(state)
const permissionsRequest = permissionsRequests
.find((permissionsRequest) => permissionsRequest.metadata.id === permissionsRequestId)
@ -40,6 +42,15 @@ const mapStateToProps = (state, ownProps) => {
const { origin } = metadata
const nativeCurrency = getNativeCurrency(state)
const domainMetadata = getDomainMetadata(state)
const targetDomainMetadata = origin
? domainMetadata[origin] || {
origin,
name: (new URL(origin)).hostname,
icon: null,
}
: null
const accountsWithLabels = getAccountsWithLabels(state)
const lastConnectedInfo = getLastConnectedInfo(state) || {}
@ -61,13 +72,12 @@ const mapStateToProps = (state, ownProps) => {
throw new Error('Incorrect path for permissions-connect component')
}
const targetDomainMetadata = getTargetDomainMetadata(state, permissionsRequest, origin)
return {
permissionsRequest,
permissionsRequestId,
hasPendingPermissionsRequests,
accounts: accountsWithLabels,
currentAddress,
origin,
newAccountNumber: accountsWithLabels.length + 1,
nativeCurrency,

@ -287,18 +287,6 @@ export function getDomainMetadata (state) {
return state.metamask.domainMetadata
}
export function getTargetDomainMetadata (state, request, defaultOrigin) {
const domainMetadata = getDomainMetadata(state)
const { metadata: requestMetadata = {} } = request || {}
const origin = requestMetadata.origin || defaultOrigin
const hostname = (new URL(origin).hostname)
const targetDomainMetadata = (domainMetadata[origin] || { name: hostname, icon: null })
targetDomainMetadata.origin = origin
return targetDomainMetadata
}
export const getBackgroundMetaMetricState = (state) => {
return {
network: getCurrentNetworkId(state),

Loading…
Cancel
Save