import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import copyToClipboard from 'copy-to-clipboard'; import { getBlockExplorerLink } from '@metamask/etherscan-link'; import SenderToRecipient from '../../ui/sender-to-recipient'; import { DEFAULT_VARIANT } from '../../ui/sender-to-recipient/sender-to-recipient.constants'; import Disclosure from '../../ui/disclosure'; import TransactionActivityLog from '../transaction-activity-log'; import TransactionBreakdown from '../transaction-breakdown'; import Button from '../../ui/button'; import Tooltip from '../../ui/tooltip'; import CancelButton from '../cancel-button'; import Popover from '../../ui/popover'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) import Box from '../../ui/box/box'; import { Icon, IconName, Text } from '../../component-library'; import { IconColor } from '../../../helpers/constants/design-system'; ///: END:ONLY_INCLUDE_IN import { SECOND } from '../../../../shared/constants/time'; import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { TransactionType } from '../../../../shared/constants/transaction'; import { getURLHostName } from '../../../helpers/utils/util'; import TransactionDecoding from '../transaction-decoding'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; export default class TransactionListItemDetails extends PureComponent { static contextTypes = { t: PropTypes.func, trackEvent: PropTypes.func, }; static defaultProps = { recipientEns: null, }; static propTypes = { onCancel: PropTypes.func, onRetry: PropTypes.func, showCancel: PropTypes.bool, showSpeedUp: PropTypes.bool, showRetry: PropTypes.bool, isEarliestNonce: PropTypes.bool, primaryCurrency: PropTypes.string, transactionGroup: PropTypes.object, title: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, recipientEns: PropTypes.string, recipientAddress: PropTypes.string, recipientName: PropTypes.string, recipientMetadataName: PropTypes.string, rpcPrefs: PropTypes.object, senderAddress: PropTypes.string.isRequired, tryReverseResolveAddress: PropTypes.func.isRequired, senderNickname: PropTypes.string.isRequired, recipientNickname: PropTypes.string, transactionStatus: PropTypes.func, isCustomNetwork: PropTypes.bool, history: PropTypes.object, blockExplorerLinkText: PropTypes.object, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) getCustodianTransactionDeepLink: PropTypes.func, selectedIdentity: PropTypes.object, transactionNote: PropTypes.string, ///: END:ONLY_INCLUDE_IN }; state = { justCopied: false, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) custodyTransactionDeepLink: null, ///: END:ONLY_INCLUDE_IN }; handleBlockExplorerClick = () => { const { transactionGroup: { primaryTransaction }, rpcPrefs, isCustomNetwork, history, onClose, } = this.props; const blockExplorerLink = getBlockExplorerLink( primaryTransaction, rpcPrefs, ); if (!rpcPrefs.blockExplorerUrl && isCustomNetwork) { onClose(); history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); } else { this.context.trackEvent({ category: MetaMetricsEventCategory.Transactions, event: 'Clicked Block Explorer Link', properties: { link_type: 'Transaction Block Explorer', action: 'Transaction Details', block_explorer_domain: getURLHostName(blockExplorerLink), }, }); global.platform.openTab({ url: blockExplorerLink, }); } }; handleCancel = (event) => { const { onCancel, onClose } = this.props; onCancel(event); onClose(); }; handleRetry = (event) => { const { onClose, onRetry } = this.props; onRetry(event); onClose(); }; handleCopyTxId = () => { const { transactionGroup } = this.props; const { primaryTransaction: transaction } = transactionGroup; const { hash } = transaction; this.context.trackEvent({ category: MetaMetricsEventCategory.Navigation, event: 'Copied Transaction ID', properties: { action: 'Activity Log', legacy_event: true, }, }); this.setState({ justCopied: true }, () => { copyToClipboard(hash); setTimeout(() => this.setState({ justCopied: false }), SECOND); }); }; componentDidMount() { const { recipientAddress, tryReverseResolveAddress, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) selectedIdentity, transactionGroup, ///: END:ONLY_INCLUDE_IN } = this.props; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) this._mounted = true; const address = selectedIdentity?.address; const custodyId = transactionGroup?.primaryTransaction?.custodyId; if (this._mounted && address && custodyId) { this.getCustodianTransactionDeepLink(address, custodyId); } ///: END:ONLY_INCLUDE_IN if (recipientAddress) { tryReverseResolveAddress(recipientAddress); } } ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) getCustodianTransactionDeepLink = async (address, custodyId) => { const { getCustodianTransactionDeepLink } = this.props; const custodyTransactionDeepLink = await getCustodianTransactionDeepLink( address, custodyId, ); if (custodyTransactionDeepLink && this._mounted) { this.setState({ custodyTransactionDeepLink }); } }; componentWillUnmount() { this._mounted = false; } ///: END:ONLY_INCLUDE_IN render() { const { t } = this.context; const { justCopied, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) custodyTransactionDeepLink, ///: END:ONLY_INCLUDE_IN } = this.state; const { transactionGroup, primaryCurrency, showSpeedUp, showRetry, recipientEns, recipientAddress, recipientName, recipientMetadataName, senderAddress, isEarliestNonce, senderNickname, title, onClose, recipientNickname, showCancel, transactionStatus: TransactionStatus, blockExplorerLinkText, ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) transactionNote, ///: END:ONLY_INCLUDE_IN } = this.props; const { primaryTransaction: transaction, initialTransaction: { type }, } = transactionGroup; const { hash } = transaction; return ( <Popover title={title} onClose={onClose}> <div className="transaction-list-item-details"> <div className="transaction-list-item-details__operations"> <div className="transaction-list-item-details__header-buttons"> {showSpeedUp && ( <Button type="primary" onClick={this.handleRetry} className="transaction-list-item-details__header-button-rounded-button" data-testid="speedup-button" > {t('speedUp')} </Button> )} {showCancel && ( <CancelButton transaction={transaction} cancelTransaction={this.handleCancel} detailsModal /> )} {showRetry && ( <Tooltip title={t('retryTransaction')}> <Button type="raised" onClick={this.handleRetry} className="transaction-list-item-details__header-button" data-testid="rety-button" > <i className="fa fa-sync"></i> </Button> </Tooltip> )} </div> </div> <div className="transaction-list-item-details__header"> <div className="transaction-list-item-details__tx-status" data-testid="transaction-list-item-details-tx-status" > <div>{t('status')}</div> <div> <TransactionStatus /> </div> </div> <div className="transaction-list-item-details__tx-hash"> <div> <Button type="link" onClick={this.handleBlockExplorerClick} disabled={!hash} > {blockExplorerLinkText.firstPart === 'addBlockExplorer' ? t('addBlockExplorer') : t('viewOnBlockExplorer')} </Button> </div> <div> <Tooltip wrapperClassName="transaction-list-item-details__header-button" containerClassName="transaction-list-item-details__header-button-tooltip-container" title={justCopied ? t('copiedExclamation') : null} > <Button type="link" onClick={this.handleCopyTxId} disabled={!hash} > {t('copyTransactionId')} </Button> </Tooltip> </div> { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) custodyTransactionDeepLink && custodyTransactionDeepLink.url && ( <Tooltip wrapperClassName="transaction-list-item-details__header-button" containerClassName="transaction-list-item-details__header-button-tooltip-container" title={t('viewinCustodianApp')} > <Button type="raised" onClick={() => { window.open(custodyTransactionDeepLink.url); }} > <Icon name={IconName.Custody} color={IconColor.primaryDefault} /> </Button> </Tooltip> ) ///: END:ONLY_INCLUDE_IN } </div> </div> <div className="transaction-list-item-details__body"> <div className="transaction-list-item-details__sender-to-recipient-header"> <div>{t('from')}</div> <div>{t('to')}</div> </div> <div className="transaction-list-item-details__sender-to-recipient-container"> <SenderToRecipient warnUserOnAccountMismatch={false} variant={DEFAULT_VARIANT} addressOnly recipientEns={recipientEns} recipientAddress={recipientAddress} recipientNickname={recipientNickname} recipientName={recipientName} recipientMetadataName={recipientMetadataName} senderName={senderNickname} senderAddress={senderAddress} onRecipientClick={() => { this.context.trackEvent({ category: MetaMetricsEventCategory.Navigation, event: 'Copied "To" Address', properties: { action: 'Activity Log', legacy_event: true, }, }); }} onSenderClick={() => { this.context.trackEvent({ category: MetaMetricsEventCategory.Navigation, event: 'Copied "From" Address', properties: { action: 'Activity Log', legacy_event: true, }, }); }} /> </div> <div className="transaction-list-item-details__cards-container"> <TransactionBreakdown nonce={transactionGroup.initialTransaction.txParams.nonce} isTokenApprove={ type === TransactionType.tokenMethodApprove || type === TransactionType.tokenMethodSetApprovalForAll } transaction={transaction} primaryCurrency={primaryCurrency} className="transaction-list-item-details__transaction-breakdown" /> { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) transactionNote && transactionNote.length !== 0 && ( <Box className="transaction-list-item-details__transaction-breakdown"> <Text as="h4" className="transaction-breakdown__title"> {t('transactionNote')} </Text> <Text as="p" className="transaction-breakdown__description"> {transactionNote} </Text> </Box> ) ///: END:ONLY_INCLUDE_IN } {transactionGroup.initialTransaction.type !== TransactionType.incoming && ( <Disclosure title={t('activityLog')} size="small"> <TransactionActivityLog transactionGroup={transactionGroup} className="transaction-list-item-details__transaction-activity-log" onCancel={this.handleCancel} onRetry={this.handleRetry} isEarliestNonce={isEarliestNonce} /> </Disclosure> )} {transactionGroup.initialTransaction?.txParams?.data ? ( <Disclosure title="Transaction data" size="small"> <TransactionDecoding title={t('transactionData')} to={transactionGroup.initialTransaction.txParams?.to} inputData={ transactionGroup.initialTransaction.txParams?.data } /> </Disclosure> ) : null} </div> </div> </div> </Popover> ); } }