1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-03 14:44:27 +01:00
metamask-extension/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
Mark Stacey b93046bdab
Use string literals for transaction category localized messages (#10391)
We now use string literals for all transaction category localized
messages. This makes it easier to verify that we have translations for
each of them, and that we aren't leaving any unused translations around.
2021-02-08 12:36:58 -03:30

757 lines
21 KiB
JavaScript

import ethUtil from 'ethereumjs-util';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../shared/constants/app';
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 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';
import {
TRANSACTION_CATEGORIES,
TRANSACTION_STATUSES,
} from '../../../../shared/constants/transaction';
import { getTransactionCategoryTitle } from '../../helpers/utils/transactions.util';
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,
primaryTotalTextOverride: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node,
]),
secondaryTotalTextOverride: PropTypes.string,
hideData: PropTypes.bool,
hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string,
onEdit: PropTypes.func,
setMetaMetricsSendCount: PropTypes.func,
metaMetricsSendCount: PropTypes.number,
subtitleComponent: PropTypes.node,
title: 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,
isMainnet: PropTypes.bool,
};
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 === TRANSACTION_STATUSES.DROPPED ||
transactionStatus === TRANSACTION_STATUSES.CONFIRMED;
if (
nextNonce !== prevNextNonce ||
customNonceValue !== prevCustomNonceValue
) {
if (nextNonce !== null && 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 {
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) ||
TRANSACTION_CATEGORIES.CONTRACT_INTERACTION,
origin,
},
});
showCustomizeGasModal();
}
renderDetails() {
const {
primaryTotalTextOverride,
secondaryTotalTextOverride,
hexTransactionFee,
hexTransactionTotal,
useNonceField,
customNonceValue,
updateCustomNonce,
advancedInlineGasShown,
customGas,
insufficientBalance,
updateGasAndCalculate,
hideFiatConversion,
nextNonce,
getNextNonce,
isMainnet,
} = this.props;
const notMainnetOrTest = !(isMainnet || process.env.IN_TEST);
return (
<div className="confirm-page-container-content__details">
<div className="confirm-page-container-content__gas-fee">
<ConfirmDetailRow
label="Gas Fee"
value={hexTransactionFee}
headerText={notMainnetOrTest ? '' : 'Edit'}
headerTextClassName={
notMainnetOrTest ? '' : 'confirm-detail-row__header-text--edit'
}
onHeaderClick={notMainnetOrTest ? null : () => this.handleEditGas()}
secondaryText={
hideFiatConversion
? this.context.t('noConversionRateAvailable')
: ''
}
/>
{advancedInlineGasShown || notMainnetOrTest ? (
<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) ||
TRANSACTION_CATEGORIES.CONTRACT_INTERACTION,
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 {
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) ||
TRANSACTION_CATEGORIES.CONTRACT_INTERACTION,
origin,
},
});
updateCustomNonce('');
cancelTransaction(txData).then(() => {
clearConfirmTransaction();
history.push(mostRecentOverviewPage);
});
}
handleSubmit() {
const { metricsEvent } = this.context;
const {
txData: { origin },
sendTransaction,
clearConfirmTransaction,
txData,
history,
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) ||
TRANSACTION_CATEGORIES.CONTRACT_INTERACTION,
origin,
},
});
setMetaMetricsSendCount(metaMetricsSendCount + 1).then(() => {
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, hexTransactionAmount } = this.props;
// Title string passed in by props takes priority
if (title) {
return null;
}
return (
<UserPreferencedCurrencyDisplay
value={hexTransactionAmount}
type={PRIMARY}
showEthLogo
ethLogoHeight="26"
hideLabel
/>
);
}
renderSubtitleComponent() {
const { subtitleComponent, hexTransactionAmount } = this.props;
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,
title,
hideSubtitle,
identiconAddress,
contentComponent,
onEdit,
nonce,
customNonceValue,
assetImage,
unapprovedTxCount,
transactionCategory,
hideSenderToRecipient,
showAccountInHeader,
txData,
} = 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 = getTransactionCategoryTitle(t, 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()}
subtitleComponent={this.renderSubtitleComponent()}
hideSubtitle={hideSubtitle}
detailsComponent={this.renderDetails()}
dataComponent={this.renderData(functionType)}
contentComponent={contentComponent}
nonce={customNonceValue || nonce}
unapprovedTxCount={unapprovedTxCount}
assetImage={assetImage}
identiconAddress={identiconAddress}
errorMessage={submitError}
errorKey={errorKey}
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={!valid || submitting}
onEdit={() => this.handleEdit()}
onCancelAll={() => this.handleCancelAll()}
onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()}
hideSenderToRecipient={hideSenderToRecipient}
origin={txData.origin}
/>
);
}
}
export function getMethodName(camelCase) {
if (!camelCase || typeof camelCase !== 'string') {
return '';
}
return camelCase
.replace(/([a-z])([A-Z])/gu, '$1 $2')
.replace(/([A-Z])([a-z])/gu, ' $1$2')
.replace(/ +/gu, ' ');
}