diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 7748b2764..c351b97af 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -4933,6 +4933,9 @@ "viewOnOpensea": { "message": "View on Opensea" }, + "viewinCustodianApp": { + "message": "View in custodian app" + }, "viewinExplorer": { "message": "View $1 in explorer", "description": "$1 is the action type. e.g (Account, Transaction, Swap)" diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index 493b461b8..7393cd6f4 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -11,6 +11,11 @@ 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'; @@ -52,10 +57,18 @@ export default class TransactionListItemDetails extends PureComponent { 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 = () => { @@ -124,16 +137,57 @@ export default class TransactionListItemDetails extends PureComponent { }; componentDidMount() { - const { recipientAddress, tryReverseResolveAddress } = this.props; + 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 } = this.state; + const { + justCopied, + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + custodyTransactionDeepLink, + ///: END:ONLY_INCLUDE_IN + } = this.state; const { transactionGroup, primaryCurrency, @@ -152,6 +206,9 @@ export default class TransactionListItemDetails extends PureComponent { showCancel, transactionStatus: TransactionStatus, blockExplorerLinkText, + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + transactionNote, + ///: END:ONLY_INCLUDE_IN } = this.props; const { primaryTransaction: transaction, @@ -229,6 +286,30 @@ export default class TransactionListItemDetails extends PureComponent { + { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + custodyTransactionDeepLink && + custodyTransactionDeepLink.url && ( + + + + ) + ///: END:ONLY_INCLUDE_IN + }
@@ -281,6 +362,20 @@ export default class TransactionListItemDetails extends PureComponent { primaryCurrency={primaryCurrency} className="transaction-list-item-details__transaction-breakdown" /> + { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + transactionNote && transactionNote.length !== 0 && ( + + + {t('transactionNote')} + + + {transactionNote} + + + ) + ///: END:ONLY_INCLUDE_IN + } {transactionGroup.initialTransaction.type !== TransactionType.incoming && ( diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js index c2781102f..db0f05e27 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js @@ -1,6 +1,7 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { waitFor } from '@testing-library/react'; import { TransactionStatus } from '../../../../shared/constants/transaction'; import { GAS_LIMITS } from '../../../../shared/constants/gas'; import { renderWithProvider } from '../../../../test/lib/render-helpers'; @@ -13,6 +14,14 @@ jest.mock('../../../store/actions.ts', () => ({ addPollingTokenToAppState: jest.fn(), })); +let mockGetCustodianTransactionDeepLink = jest.fn(); + +jest.mock('../../../store/institutional/institution-background', () => ({ + mmiActionsFactory: () => ({ + getCustodianTransactionDeepLink: () => mockGetCustodianTransactionDeepLink, + }), +})); + describe('TransactionListItemDetails Component', () => { const transaction = { history: [], @@ -26,6 +35,10 @@ describe('TransactionListItemDetails Component', () => { to: '0x2', value: '0x2386f26fc10000', }, + metadata: { + note: 'some note', + }, + custodyId: '1', }; const transactionGroup = { @@ -58,7 +71,7 @@ describe('TransactionListItemDetails Component', () => { rpcPrefs, }; - it('should render title with title prop', () => { + it('should render title with title prop', async () => { const mockStore = configureMockStore([thunk])(mockState); const { queryByText } = renderWithProvider( @@ -66,7 +79,9 @@ describe('TransactionListItemDetails Component', () => { mockStore, ); - expect(queryByText(props.title)).toBeInTheDocument(); + await waitFor(() => { + expect(queryByText(props.title)).toBeInTheDocument(); + }); }); describe('Retry button', () => { @@ -122,4 +137,55 @@ describe('TransactionListItemDetails Component', () => { expect(queryByTestId('speedup-button')).toBeInTheDocument(); }); }); + + describe('Institutional', () => { + it('should render correctly if custodyTransactionDeepLink has a url', async () => { + mockGetCustodianTransactionDeepLink = jest + .fn() + .mockReturnValue({ url: 'https://url.com' }); + + const mockStore = configureMockStore([thunk])(mockState); + + renderWithProvider(, mockStore); + + await waitFor(() => { + const custodianViewButton = document.querySelector( + '[data-original-title="View in custodian app"]', + ); + + // Assert that the custodian view button is rendered + expect(custodianViewButton).toBeInTheDocument(); + }); + }); + + it('should render correctly if transactionNote is provided', async () => { + const newTransaction = { + ...transaction, + metadata: { + note: 'some note', + }, + custodyId: '1', + }; + + const newTransactionGroup = { + ...transactionGroup, + transactions: [newTransaction], + primaryTransaction: newTransaction, + initialTransaction: newTransaction, + }; + const mockStore = configureMockStore([thunk])(mockState); + + const { queryByText } = renderWithProvider( + , + mockStore, + ); + + await waitFor(() => { + expect(queryByText('some note')).toBeInTheDocument(); + }); + }); + }); }); diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js index 86b341533..0f4d5d254 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js @@ -2,6 +2,9 @@ import { connect } from 'react-redux'; import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; import { tryReverseResolveAddress } from '../../../store/actions'; +///: BEGIN:ONLY_INCLUDE_IN(build-mmi) +import { mmiActionsFactory } from '../../../store/institutional/institution-background'; +///: END:ONLY_INCLUDE_IN import { getAddressBook, getBlockExplorerLinkText, @@ -11,6 +14,10 @@ import { getAccountName, getMetadataContractName, getMetaMaskIdentities, + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + getSelectedIdentity, + getKnownMethodData, + ///: END:ONLY_INCLUDE_IN } from '../../../selectors'; import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; import TransactionListItemDetails from './transaction-list-item-details.component'; @@ -40,6 +47,13 @@ const mapStateToProps = (state, ownProps) => { const isCustomNetwork = getIsCustomNetwork(state); + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const data = ownProps.transactionGroup?.primaryTransaction?.txParams?.data; + const methodData = getKnownMethodData(state, data) || {}; + const transactionNote = + ownProps.transactionGroup?.primaryTransaction?.metadata?.note; + ///: END:ONLY_INCLUDE_IN + return { rpcPrefs, recipientEns, @@ -49,14 +63,29 @@ const mapStateToProps = (state, ownProps) => { blockExplorerLinkText: getBlockExplorerLinkText(state), recipientName, recipientMetadataName, + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + methodData, + transactionNote, + selectedIdentity: getSelectedIdentity(state), + ///: END:ONLY_INCLUDE_IN }; }; const mapDispatchToProps = (dispatch) => { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const mmiActions = mmiActionsFactory(); + ///: END:ONLY_INCLUDE_IN return { tryReverseResolveAddress: (address) => { return dispatch(tryReverseResolveAddress(address)); }, + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + getCustodianTransactionDeepLink: (address, txId) => { + return dispatch( + mmiActions.getCustodianTransactionDeepLink(address, txId), + ); + }, + ///: END:ONLY_INCLUDE_IN }; }; diff --git a/ui/components/app/transaction-list-item/index.scss b/ui/components/app/transaction-list-item/index.scss index ae3e28039..069510062 100644 --- a/ui/components/app/transaction-list-item/index.scss +++ b/ui/components/app/transaction-list-item/index.scss @@ -67,4 +67,13 @@ text-overflow: ellipsis; white-space: nowrap; } + + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + &__icon-badge { + position: absolute; + top: 18px; + left: 18px; + transform: scale(0.8); + } + ///: END:ONLY_INCLUDE_IN } diff --git a/ui/components/app/transaction-list-item/transaction-list-item.component.js b/ui/components/app/transaction-list-item/transaction-list-item.component.js index f412e7fbc..77867fff8 100644 --- a/ui/components/app/transaction-list-item/transaction-list-item.component.js +++ b/ui/components/app/transaction-list-item/transaction-list-item.component.js @@ -13,6 +13,10 @@ import { CONFIRM_TRANSACTION_ROUTE } from '../../../helpers/constants/routes'; import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp'; import TransactionStatusLabel from '../transaction-status-label/transaction-status-label'; import TransactionIcon from '../transaction-icon'; +///: BEGIN:ONLY_INCLUDE_IN(build-mmi) +import { IconColor } from '../../../helpers/constants/design-system'; +import { Icon, IconName, IconSize } from '../../component-library'; +///: END:ONLY_INCLUDE_IN import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { TransactionGroupCategory, @@ -125,6 +129,9 @@ function TransactionListItemInner({ const isApproval = category === TransactionGroupCategory.approval; const isUnapproved = status === TransactionStatus.unapproved; const isSwap = category === TransactionGroupCategory.swap; + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const isCustodian = Boolean(transactionGroup.primaryTransaction.custodyId); + ///: END:ONLY_INCLUDE_IN const className = classnames('transaction-list-item', { 'transaction-list-item--unconfirmed': @@ -144,10 +151,29 @@ function TransactionListItemInner({ setShowDetails((prev) => !prev); }, [isUnapproved, history, id]); + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + const debugTransactionMeta = { + 'data-hash': transactionGroup.primaryTransaction.hash, + ...(isCustodian + ? { + 'data-custodiantransactionid': + transactionGroup.primaryTransaction.custodyId, + } + : {}), + }; + ///: END:ONLY_INCLUDE_IN + const speedUpButton = useMemo(() => { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + if (isCustodian) { + return null; + } + ///: END:ONLY_INCLUDE_IN + if (!shouldShowSpeedUp || !isPending || isUnapproved) { return null; } + return (
+ { + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + + ///: END:ONLY_INCLUDE_IN + } {showDetails && ( ( { }; }); +jest.mock('../../../store/actions.ts', () => ({ + tryReverseResolveAddress: jest.fn().mockReturnValue({ type: 'TYPE' }), +})); + +jest.mock('../../../store/institutional/institution-background', () => ({ + mmiActionsFactory: () => ({ + getCustodianTransactionDeepLink: jest + .fn() + .mockReturnValue({ type: 'TYPE' }), + }), +})); + +const mockStore = configureStore(); + const generateUseSelectorRouter = (opts) => (selector) => { if (selector === getConversionRate) { return 1; @@ -146,5 +162,49 @@ describe('TransactionListItem', () => { fireEvent.click(cancelButton); expect(getByText('Cancel transaction')).toBeInTheDocument(); }); + + it('should have a custodian Tx and show the custody icon', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + balance: '2AA1EFB94E0000', + }), + ); + + const newTransactionGroup = { + ...transactionGroup, + ...(transactionGroup.primaryTransaction.custodyId = '1'), + }; + + const { queryByTestId } = renderWithProvider( + , + ); + expect(queryByTestId('custody-icon')).toBeInTheDocument(); + }); + + it('should click the custody list item and view the send screen', () => { + const store = mockStore(mockState); + + useSelector.mockImplementation( + generateUseSelectorRouter({ + balance: '2AA1EFB94E0000', + }), + ); + + const newTransactionGroup = { + ...transactionGroup, + ...(transactionGroup.primaryTransaction.custodyId = '1'), + }; + + const { queryByTestId } = renderWithProvider( + , + store, + ); + + const custodyListItem = queryByTestId('custody-icon'); + fireEvent.click(custodyListItem); + + const sendTextExists = screen.queryAllByText('Send'); + expect(sendTextExists).toBeTruthy(); + }); }); }); diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index a86df8c5f..be50d01f9 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -123,23 +123,36 @@ export default function TransactionList({
{`${t('queue')} (${pendingTransactions.length})`}
- {pendingTransactions.map((transactionGroup, index) => - transactionGroup.initialTransaction.transactionType === - TransactionType.smart ? ( - - ) : ( - - ), - )} + {pendingTransactions + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + .sort( + (a, b) => b.primaryTransaction.time - a.primaryTransaction.time, + ) + ///: END:ONLY_INCLUDE_IN + .map((transactionGroup, index) => { + ///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask) + if ( + transactionGroup.initialTransaction.transactionType === + TransactionType.smart + ) { + return ( + + ); + } + ///: END:ONLY_INCLUDE_IN + return ( + + ); + })} )}
@@ -148,6 +161,11 @@ export default function TransactionList({ ) : null} {completedTransactions.length > 0 ? ( completedTransactions + ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) + .sort( + (a, b) => b.primaryTransaction.time - a.primaryTransaction.time, + ) + ///: END:ONLY_INCLUDE_IN .slice(0, limit) .map((transactionGroup, index) => transactionGroup.initialTransaction?.transactionType ===