1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-25 03:20:23 +01:00
metamask-extension/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
Mark Stacey df85ab6e10
Implement asset page (#8696)
A new page has been created for viewing assets. This replaces the old
`selectedToken` state, which previously would augment the home page
to show token-specific information.

The new asset page shows the standard token overview as seen previously
on the home page, plus a history filtered to show just transactions
relevant to that token.

The actions that were available in the old token list menu have been
moved to a "Token Options" menu that mirrors the "Account Options"
menu.

The `selectedTokenAddress` state has been removed, as it is no longer
being used for anything.

`getMetaMetricState` has been renamed to `getBackgroundMetaMetricState`
because its sole purpose is extracting data from the background state
to send metrics from the background. It's not really a selector, but
it was convenient for it to use the same selectors the UI uses to
extract background data, so I left it there for now.

A new Redux store has been added to track state related to browser history.
The most recent "overview" page (i.e. the home page or the asset page) is
currently being tracked, so that actions taken from the asset page can return
the user back to the asset page when the action has finished.
2020-06-01 14:54:32 -03:00

734 lines
21 KiB
JavaScript

import ethUtil from 'ethereumjs-util'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../app/scripts/lib/enums'
import { getEnvironmentType } from '../../../../app/scripts/lib/util'
import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container'
import { isBalanceSufficient } from '../send/send.utils'
import { CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes'
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_ERROR_KEY,
GAS_LIMIT_TOO_LOW_ERROR_KEY,
} from '../../helpers/constants/error-keys'
import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display'
import { PRIMARY, SECONDARY } from '../../helpers/constants/common'
import { hexToDecimal } from '../../helpers/utils/conversions.util'
import AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'
import TextField from '../../components/ui/text-field'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
}
static propTypes = {
// react-router props
history: PropTypes.object,
// Redux props
balance: PropTypes.string,
cancelTransaction: PropTypes.func,
cancelAllTransactions: PropTypes.func,
clearConfirmTransaction: PropTypes.func,
conversionRate: PropTypes.number,
fromAddress: PropTypes.string,
fromName: PropTypes.string,
hexTransactionAmount: PropTypes.string,
hexTransactionFee: PropTypes.string,
hexTransactionTotal: PropTypes.string,
isTxReprice: PropTypes.bool,
methodData: PropTypes.object,
nonce: PropTypes.string,
useNonceField: PropTypes.bool,
customNonceValue: PropTypes.string,
updateCustomNonce: PropTypes.func,
assetImage: PropTypes.string,
sendTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func,
showRejectTransactionsConfirmationModal: PropTypes.func,
toAddress: PropTypes.string,
tokenData: PropTypes.object,
tokenProps: PropTypes.object,
toName: PropTypes.string,
toEns: PropTypes.string,
toNickname: PropTypes.string,
transactionStatus: PropTypes.string,
txData: PropTypes.object,
unapprovedTxCount: PropTypes.number,
currentNetworkUnapprovedTxs: PropTypes.object,
updateGasAndCalculate: PropTypes.func,
customGas: PropTypes.object,
// Component props
actionKey: PropTypes.string,
contentComponent: PropTypes.node,
dataComponent: PropTypes.node,
detailsComponent: PropTypes.node,
errorKey: PropTypes.string,
errorMessage: PropTypes.string,
primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
secondaryTotalTextOverride: PropTypes.string,
hideData: PropTypes.bool,
hideDetails: PropTypes.bool,
hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string,
onCancel: PropTypes.func,
onEdit: PropTypes.func,
onEditGas: PropTypes.func,
onSubmit: PropTypes.func,
setMetaMetricsSendCount: PropTypes.func,
metaMetricsSendCount: PropTypes.number,
subtitle: PropTypes.string,
subtitleComponent: PropTypes.node,
summaryComponent: PropTypes.node,
title: PropTypes.string,
titleComponent: PropTypes.node,
valid: PropTypes.bool,
warning: PropTypes.string,
advancedInlineGasShown: PropTypes.bool,
insufficientBalance: PropTypes.bool,
hideFiatConversion: PropTypes.bool,
transactionCategory: PropTypes.string,
getNextNonce: PropTypes.func,
nextNonce: PropTypes.number,
tryReverseResolveAddress: PropTypes.func.isRequired,
hideSenderToRecipient: PropTypes.bool,
showAccountInHeader: PropTypes.bool,
mostRecentOverviewPage: PropTypes.string.isRequired,
}
state = {
submitting: false,
submitError: null,
submitWarning: '',
}
componentDidUpdate (prevProps) {
const {
transactionStatus,
showTransactionConfirmedModal,
history,
clearConfirmTransaction,
mostRecentOverviewPage,
nextNonce,
customNonceValue,
toAddress,
tryReverseResolveAddress,
} = this.props
const {
customNonceValue: prevCustomNonceValue,
nextNonce: prevNextNonce,
toAddress: prevToAddress,
transactionStatus: prevTxStatus,
} = prevProps
const statusUpdated = transactionStatus !== prevTxStatus
const txDroppedOrConfirmed = transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS
if (nextNonce !== prevNextNonce || customNonceValue !== prevCustomNonceValue) {
if (customNonceValue > nextNonce) {
this.setState({ submitWarning: this.context.t('nextNonceWarning', [nextNonce]) })
} else {
this.setState({ submitWarning: '' })
}
}
if (statusUpdated && txDroppedOrConfirmed) {
showTransactionConfirmedModal({
onSubmit: () => {
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
},
})
}
if (toAddress && toAddress !== prevToAddress) {
tryReverseResolveAddress(toAddress)
}
}
getErrorKey () {
const {
balance,
conversionRate,
hexTransactionFee,
txData: {
simulationFails,
txParams: {
value: amount,
} = {},
} = {},
customGas,
} = this.props
const insufficientBalance = balance && !isBalanceSufficient({
amount,
gasTotal: hexTransactionFee || '0x0',
balance,
conversionRate,
})
if (insufficientBalance) {
return {
valid: false,
errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
}
}
if (hexToDecimal(customGas.gasLimit) < 21000) {
return {
valid: false,
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
}
}
if (simulationFails) {
return {
valid: true,
errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY,
}
}
return {
valid: true,
}
}
handleEditGas () {
const { onEditGas, showCustomizeGasModal, actionKey, txData: { origin }, methodData = {} } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'User clicks "Edit" on gas',
},
customVariables: {
recipientKnown: null,
functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction',
origin,
},
})
if (onEditGas) {
onEditGas()
} else {
showCustomizeGasModal()
}
}
renderDetails () {
const {
detailsComponent,
primaryTotalTextOverride,
secondaryTotalTextOverride,
hexTransactionFee,
hexTransactionTotal,
hideDetails,
useNonceField,
customNonceValue,
updateCustomNonce,
advancedInlineGasShown,
customGas,
insufficientBalance,
updateGasAndCalculate,
hideFiatConversion,
nextNonce,
getNextNonce,
} = this.props
if (hideDetails) {
return null
}
return (
detailsComponent || (
<div className="confirm-page-container-content__details">
<div className="confirm-page-container-content__gas-fee">
<ConfirmDetailRow
label="Gas Fee"
value={hexTransactionFee}
headerText="Edit"
headerTextClassName="confirm-detail-row__header-text--edit"
onHeaderClick={() => this.handleEditGas()}
secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : ''}
/>
{advancedInlineGasShown
? (
<AdvancedGasInputs
updateCustomGasPrice={(newGasPrice) => updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice })}
updateCustomGasLimit={(newGasLimit) => updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit })}
customGasPrice={customGas.gasPrice}
customGasLimit={customGas.gasLimit}
insufficientBalance={insufficientBalance}
customPriceIsSafe
isSpeedUp={false}
/>
)
: null
}
</div>
<div className={useNonceField ? 'confirm-page-container-content__gas-fee' : null}>
<ConfirmDetailRow
label="Total"
value={hexTransactionTotal}
primaryText={primaryTotalTextOverride}
secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : secondaryTotalTextOverride}
headerText="Amount + Gas Fee"
headerTextClassName="confirm-detail-row__header-text--total"
primaryValueTextColor="#2f9ae0"
/>
</div>
{useNonceField ? (
<div>
<div className="confirm-detail-row">
<div className="confirm-detail-row__label">
{ this.context.t('nonceFieldHeading') }
</div>
<div className="custom-nonce-input">
<TextField
type="number"
min="0"
placeholder={ typeof nextNonce === 'number' ? nextNonce.toString() : null }
onChange={({ target: { value } }) => {
if (!value.length || Number(value) < 0) {
updateCustomNonce('')
} else {
updateCustomNonce(String(Math.floor(value)))
}
getNextNonce()
}}
fullWidth
margin="dense"
value={ customNonceValue || '' }
/>
</div>
</div>
</div>
) : null}
</div>
)
)
}
renderData (functionType) {
const { t } = this.context
const {
txData: {
txParams: {
data,
} = {},
} = {},
methodData: {
params,
} = {},
hideData,
dataComponent,
} = this.props
if (hideData) {
return null
}
return dataComponent || (
<div className="confirm-page-container-content__data">
<div className="confirm-page-container-content__data-box-label">
{`${t('functionType')}:`}
<span className="confirm-page-container-content__function-type">
{ functionType }
</span>
</div>
{
params && (
<div className="confirm-page-container-content__data-box">
<div className="confirm-page-container-content__data-field-label">
{ `${t('parameters')}:` }
</div>
<div>
<pre>{ JSON.stringify(params, null, 2) }</pre>
</div>
</div>
)
}
<div className="confirm-page-container-content__data-box-label">
{`${t('hexData')}: ${ethUtil.toBuffer(data).length} bytes`}
</div>
<div className="confirm-page-container-content__data-box">
{ data }
</div>
</div>
)
}
handleEdit () {
const { txData, tokenData, tokenProps, onEdit, actionKey, txData: { origin }, methodData = {} } = this.props
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Edit Transaction',
},
customVariables: {
recipientKnown: null,
functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction',
origin,
},
})
onEdit({ txData, tokenData, tokenProps })
}
handleCancelAll () {
const {
cancelAllTransactions,
clearConfirmTransaction,
history,
mostRecentOverviewPage,
showRejectTransactionsConfirmationModal,
unapprovedTxCount,
} = this.props
showRejectTransactionsConfirmationModal({
unapprovedTxCount,
onSubmit: async () => {
this._removeBeforeUnload()
await cancelAllTransactions()
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
},
})
}
handleCancel () {
const { metricsEvent } = this.context
const {
onCancel,
txData,
cancelTransaction,
history,
mostRecentOverviewPage,
clearConfirmTransaction,
actionKey,
txData: { origin },
methodData = {},
updateCustomNonce,
} = this.props
this._removeBeforeUnload()
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Cancel',
},
customVariables: {
recipientKnown: null,
functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction',
origin,
},
})
updateCustomNonce('')
if (onCancel) {
onCancel(txData)
} else {
cancelTransaction(txData)
.then(() => {
clearConfirmTransaction()
history.push(mostRecentOverviewPage)
})
}
}
handleSubmit () {
const { metricsEvent } = this.context
const {
txData: { origin },
sendTransaction,
clearConfirmTransaction,
txData,
history,
onSubmit,
actionKey,
mostRecentOverviewPage,
metaMetricsSendCount = 0,
setMetaMetricsSendCount,
methodData = {},
updateCustomNonce,
} = this.props
const { submitting } = this.state
if (submitting) {
return
}
this.setState({
submitting: true,
submitError: null,
}, () => {
this._removeBeforeUnload()
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Transaction Completed',
},
customVariables: {
recipientKnown: null,
functionType: actionKey || getMethodName(methodData.name) || 'contractInteraction',
origin,
},
})
setMetaMetricsSendCount(metaMetricsSendCount + 1)
.then(() => {
if (onSubmit) {
Promise.resolve(onSubmit(txData))
.then(() => {
this.setState({
submitting: false,
})
updateCustomNonce('')
})
} else {
sendTransaction(txData)
.then(() => {
clearConfirmTransaction()
this.setState({
submitting: false,
}, () => {
history.push(mostRecentOverviewPage)
updateCustomNonce('')
})
})
.catch((error) => {
this.setState({
submitting: false,
submitError: error.message,
})
updateCustomNonce('')
})
}
})
})
}
renderTitleComponent () {
const { title, titleComponent, hexTransactionAmount } = this.props
// Title string passed in by props takes priority
if (title) {
return null
}
return titleComponent || (
<UserPreferencedCurrencyDisplay
value={hexTransactionAmount}
type={PRIMARY}
showEthLogo
ethLogoHeight="26"
hideLabel
/>
)
}
renderSubtitleComponent () {
const { subtitle, subtitleComponent, hexTransactionAmount } = this.props
// Subtitle string passed in by props takes priority
if (subtitle) {
return null
}
return subtitleComponent || (
<UserPreferencedCurrencyDisplay
value={hexTransactionAmount}
type={SECONDARY}
showEthLogo
hideLabel
/>
)
}
handleNextTx (txId) {
const { history, clearConfirmTransaction } = this.props
if (txId) {
clearConfirmTransaction()
history.push(`${CONFIRM_TRANSACTION_ROUTE}/${txId}`)
}
}
getNavigateTxData () {
const { currentNetworkUnapprovedTxs, txData: { id } = {} } = this.props
const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs)
const currentPosition = enumUnapprovedTxs.indexOf(id ? id.toString() : '')
return {
totalTx: enumUnapprovedTxs.length,
positionOfCurrentTx: currentPosition + 1,
nextTxId: enumUnapprovedTxs[currentPosition + 1],
prevTxId: enumUnapprovedTxs[currentPosition - 1],
showNavigation: enumUnapprovedTxs.length > 1,
firstTx: enumUnapprovedTxs[0],
lastTx: enumUnapprovedTxs[enumUnapprovedTxs.length - 1],
ofText: this.context.t('ofTextNofM'),
requestsWaitingText: this.context.t('requestsAwaitingAcknowledgement'),
}
}
_beforeUnload = () => {
const { txData: { origin, id } = {}, cancelTransaction } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Cancel Tx Via Notification Close',
},
customVariables: {
origin,
},
})
cancelTransaction({ id })
}
_removeBeforeUnload = () => {
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
window.removeEventListener('beforeunload', this._beforeUnload)
}
}
componentDidMount () {
const { toAddress, txData: { origin } = {}, getNextNonce, tryReverseResolveAddress } = this.props
const { metricsEvent } = this.context
metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'Confirm: Started',
},
customVariables: {
origin,
},
})
if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) {
window.addEventListener('beforeunload', this._beforeUnload)
}
getNextNonce()
if (toAddress) {
tryReverseResolveAddress(toAddress)
}
}
componentWillUnmount () {
this._removeBeforeUnload()
}
render () {
const { t } = this.context
const {
isTxReprice,
fromName,
fromAddress,
toName,
toAddress,
toEns,
toNickname,
methodData,
valid: propsValid = true,
errorMessage,
errorKey: propsErrorKey,
title,
subtitle,
hideSubtitle,
identiconAddress,
summaryComponent,
contentComponent,
onEdit,
nonce,
customNonceValue,
assetImage,
warning,
unapprovedTxCount,
transactionCategory,
hideSenderToRecipient,
showAccountInHeader,
} = this.props
const { submitting, submitError, submitWarning } = this.state
const { name } = methodData
const { valid, errorKey } = this.getErrorKey()
const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData()
let functionType = getMethodName(name)
if (!functionType) {
if (transactionCategory) {
functionType = t(transactionCategory) || transactionCategory
} else {
functionType = t('contractInteraction')
}
}
return (
<ConfirmPageContainer
fromName={fromName}
fromAddress={fromAddress}
showAccountInHeader={showAccountInHeader}
toName={toName}
toAddress={toAddress}
toEns={toEns}
toNickname={toNickname}
showEdit={onEdit && !isTxReprice}
action={functionType}
title={title}
titleComponent={this.renderTitleComponent()}
subtitle={subtitle}
subtitleComponent={this.renderSubtitleComponent()}
hideSubtitle={hideSubtitle}
summaryComponent={summaryComponent}
detailsComponent={this.renderDetails()}
dataComponent={this.renderData(functionType)}
contentComponent={contentComponent}
nonce={customNonceValue || nonce}
unapprovedTxCount={unapprovedTxCount}
assetImage={assetImage}
identiconAddress={identiconAddress}
errorMessage={errorMessage || submitError}
errorKey={propsErrorKey || errorKey}
warning={warning || submitWarning}
totalTx={totalTx}
positionOfCurrentTx={positionOfCurrentTx}
nextTxId={nextTxId}
prevTxId={prevTxId}
showNavigation={showNavigation}
onNextTx={(txId) => this.handleNextTx(txId)}
firstTx={firstTx}
lastTx={lastTx}
ofText={ofText}
requestsWaitingText={requestsWaitingText}
disabled={!propsValid || !valid || submitting}
onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()}
hideSenderToRecipient={hideSenderToRecipient}
/>
)
}
}
export function getMethodName (camelCase) {
if (!camelCase || typeof camelCase !== 'string') {
return ''
}
return camelCase
.replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/([A-Z])([a-z])/g, ' $1$2')
.replace(/ +/g, ' ')
}