From 0f6d6f3082169e3ac848ef6ed58b30fe0fe7ce36 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Mon, 12 May 2025 10:43:01 +0200 Subject: [PATCH 1/2] Implement network component for redesigned transactions --- .../info/transfer/transfer.test.tsx | 1 + .../components/info/transfer/transfer.tsx | 2 + .../rows/transactions/network-row/index.ts | 1 + .../network-row/network-row.styles.ts | 17 +++ .../network-row/network-row.test.tsx | 113 ++++++++++++++++++ .../transactions/network-row/network-row.tsx | 67 +++++++++++ .../confirmations/utils/deeplink.test.ts | 24 ++-- .../Views/confirmations/utils/deeplink.ts | 11 +- .../Handlers/handleEthereumUrl.test.ts | 1 + .../Handlers/handleEthereumUrl.ts | 5 +- locales/languages/en.json | 2 + 11 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 app/components/Views/confirmations/components/rows/transactions/network-row/index.ts create mode 100644 app/components/Views/confirmations/components/rows/transactions/network-row/network-row.styles.ts create mode 100644 app/components/Views/confirmations/components/rows/transactions/network-row/network-row.test.tsx create mode 100644 app/components/Views/confirmations/components/rows/transactions/network-row/network-row.tsx diff --git a/app/components/Views/confirmations/components/info/transfer/transfer.test.tsx b/app/components/Views/confirmations/components/info/transfer/transfer.test.tsx index fc9c1819b368..ac7b08820322 100644 --- a/app/components/Views/confirmations/components/info/transfer/transfer.test.tsx +++ b/app/components/Views/confirmations/components/info/transfer/transfer.test.tsx @@ -93,6 +93,7 @@ describe('Transfer', () => { expect(mockUseClearConfirmationOnBackSwipe).toHaveBeenCalled(); expect(getByText('0xDc477...0c164')).toBeDefined(); expect(getByText('Network Fee')).toBeDefined(); + expect(getByText('Network')).toBeDefined(); expect(getNavbar).toHaveBeenCalled(); expect(getNavbar).toHaveBeenCalledWith({ title: 'Review', diff --git a/app/components/Views/confirmations/components/info/transfer/transfer.tsx b/app/components/Views/confirmations/components/info/transfer/transfer.tsx index 0f0a79ecb982..839d019c7932 100644 --- a/app/components/Views/confirmations/components/info/transfer/transfer.tsx +++ b/app/components/Views/confirmations/components/info/transfer/transfer.tsx @@ -12,6 +12,7 @@ import FromTo from '../../rows/transactions/from-to'; import GasFeesDetails from '../../rows/transactions/gas-fee-details'; import AdvancedDetailsRow from '../../rows/transactions/advanced-details-row/advanced-details-row'; import TokenHero from '../../rows/transactions/token-hero'; +import NetworkRow from '../../rows/transactions/network-row'; import styleSheet from './transfer.styles'; const Transfer = () => { @@ -28,6 +29,7 @@ const Transfer = () => { + + StyleSheet.create({ + infoRowOverride: { + paddingBottom: 4, + }, + networkRowContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + avatarNetwork: { + marginRight: 4, + }, + }); + +export default styleSheet; diff --git a/app/components/Views/confirmations/components/rows/transactions/network-row/network-row.test.tsx b/app/components/Views/confirmations/components/rows/transactions/network-row/network-row.test.tsx new file mode 100644 index 000000000000..ddfc26a97557 --- /dev/null +++ b/app/components/Views/confirmations/components/rows/transactions/network-row/network-row.test.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { merge } from 'lodash'; +import { Hex } from '@metamask/utils'; + +import renderWithProvider from '../../../../../../../util/test/renderWithProvider'; +import { transferConfirmationState } from '../../../../../../../util/test/confirm-data-helpers'; +import { MMM_ORIGIN } from '../../../../constants/confirmations'; +import NetworkRow from './network-row'; + +jest.mock('../../../../hooks/metrics/useConfirmationMetricEvents'); +jest.mock('../../../../../../../core/Engine', () => ({ + context: { + GasFeeController: { + startPolling: jest.fn(), + stopPollingByPollingToken: jest.fn(), + }, + NetworkController: { + getNetworkConfigurationByNetworkClientId: jest.fn(), + }, + TokenListController: { + fetchTokenList: jest.fn(), + }, + }, +})); + +const MOCK_DAPP_ORIGIN = 'https://exampledapp.com'; + +const mockNetworkImage = { uri: 'https://mocknetwork.com/image.png' }; +jest.mock('../../../../../../../util/networks', () => ({ + getNetworkImageSource: jest.fn(() => mockNetworkImage), +})); + +const networkConfigurationMock = { + name: 'Test Network', + chainId: '0x1' as Hex, +}; + +// Mock state with transaction from MetaMask Mobile origin +const internalTransactionState = merge({}, transferConfirmationState, { + engine: { + backgroundState: { + TransactionController: { + transactions: [ + { + txParams: { + from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164', + }, + origin: MMM_ORIGIN, + }, + ], + }, + NetworkController: { + networkConfigurationsByChainId: { + '0x1': networkConfigurationMock, + }, + }, + }, + }, +}); + +// Mock state with transaction from external dapp origin +const dappOriginTransactionState = merge({}, transferConfirmationState, { + engine: { + backgroundState: { + TransactionController: { + transactions: [ + { + txParams: { + from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164', + }, + origin: MOCK_DAPP_ORIGIN, + }, + ], + }, + NetworkController: { + networkConfigurationsByChainId: { + '0x1': networkConfigurationMock, + }, + }, + }, + }, +}); + +describe('NetworkRow', () => { + it('displays the correct network name', async () => { + const { getByText } = renderWithProvider(, { + state: internalTransactionState, + }); + + expect(getByText('Test Network')).toBeDefined(); + expect(getByText('Network')).toBeDefined(); + }); + + it('displays origin info for dapp transactions', async () => { + const { getByText } = renderWithProvider(, { + state: dappOriginTransactionState, + }); + + expect(getByText('Test Network')).toBeDefined(); + expect(getByText('Request from')).toBeDefined(); + expect(getByText(MOCK_DAPP_ORIGIN)).toBeDefined(); + }); + + it('does not display origin info for internal transactions', async () => { + const { getByText, queryByText } = renderWithProvider(, { + state: internalTransactionState, + }); + + expect(getByText('Test Network')).toBeDefined(); + expect(queryByText('Request from')).toBeNull(); + expect(queryByText(MMM_ORIGIN)).toBeNull(); + }); +}); diff --git a/app/components/Views/confirmations/components/rows/transactions/network-row/network-row.tsx b/app/components/Views/confirmations/components/rows/transactions/network-row/network-row.tsx new file mode 100644 index 000000000000..ecdf979399fc --- /dev/null +++ b/app/components/Views/confirmations/components/rows/transactions/network-row/network-row.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { View } from 'react-native'; +import { useSelector } from 'react-redux'; +import { Hex } from '@metamask/utils'; + +import { useTransactionMetadataRequest } from '../../../../hooks/transactions/useTransactionMetadataRequest'; +import { selectNetworkConfigurationByChainId } from '../../../../../../../selectors/networkController'; +import Text, { + TextVariant, +} from '../../../../../../../component-library/components/Texts/Text'; +import { useStyles } from '../../../../../../../component-library/hooks'; +import { getNetworkImageSource } from '../../../../../../../util/networks'; +import { strings } from '../../../../../../../../locales/i18n'; +import { RootState } from '../../../../../../../reducers'; +import InfoSection from '../../../UI/info-row/info-section'; +import InfoRow from '../../../UI/info-row/info-row'; +import { MMM_ORIGIN } from '../../../../constants/confirmations'; +import styleSheet from './network-row.styles'; +import AvatarNetwork from '../../../../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork'; +import { AvatarSize } from '../../../../../../../component-library/components/Avatars/Avatar/Avatar.types'; + +const NetworkRow = () => { + const { styles } = useStyles(styleSheet, {}); + const transactionMetadata = useTransactionMetadataRequest(); + const chainId = transactionMetadata?.chainId; + const origin = transactionMetadata?.origin; + const networkConfiguration = useSelector((state: RootState) => + selectNetworkConfigurationByChainId(state, chainId), + ); + const isDappOrigin = origin !== MMM_ORIGIN; + const networkImage = getNetworkImageSource({ chainId: chainId as Hex }); + + if (!transactionMetadata) { + return null; + } + + return ( + + + + {networkImage && ( + + )} + {networkConfiguration?.name} + + + + {isDappOrigin && ( + + {origin} + + )} + + ); +}; + +export default NetworkRow; diff --git a/app/components/Views/confirmations/utils/deeplink.test.ts b/app/components/Views/confirmations/utils/deeplink.test.ts index c8e13925f052..a11d76aaa17f 100644 --- a/app/components/Views/confirmations/utils/deeplink.test.ts +++ b/app/components/Views/confirmations/utils/deeplink.test.ts @@ -10,6 +10,9 @@ import { addTransactionForDeeplink, isDeeplinkRedesignedConfirmationCompatible, } from './deeplink'; +import { type DeeplinkRequest } from './deeplink'; + +const ORIGIN_MOCK = 'example.test-dapp.com'; jest.mock('../../../../core/Engine', () => ({ context: { @@ -142,7 +145,8 @@ describe('addTransactionForDeeplink', () => { value: '1000', }, target_address: TO_ADDRESS_MOCK, - } as unknown as ParseOutput); + origin: ORIGIN_MOCK, + } as unknown as DeeplinkRequest); expect(mockAddTransaction).toHaveBeenCalledWith( { @@ -152,7 +156,7 @@ describe('addTransactionForDeeplink', () => { }, { networkClientId: 'another-network', - origin: 'deeplink', + origin: ORIGIN_MOCK, type: TransactionType.simpleSend, }, ); @@ -164,7 +168,8 @@ describe('addTransactionForDeeplink', () => { value: '1000', }, target_address: TO_ADDRESS_MOCK, - } as unknown as ParseOutput); + origin: ORIGIN_MOCK, + } as unknown as DeeplinkRequest); expect(mockAddTransaction).toHaveBeenCalledWith( { @@ -174,7 +179,7 @@ describe('addTransactionForDeeplink', () => { }, { networkClientId: 'mainnet', - origin: 'deeplink', + origin: ORIGIN_MOCK, type: TransactionType.simpleSend, }, ); @@ -187,7 +192,8 @@ describe('addTransactionForDeeplink', () => { value: '1000', }, target_address: TO_ADDRESS_MOCK, - } as unknown as ParseOutput); + origin: ORIGIN_MOCK, + } as unknown as DeeplinkRequest); expect(mockAddTransaction).toHaveBeenCalledTimes(1); @@ -196,7 +202,8 @@ describe('addTransactionForDeeplink', () => { value: '9999', }, target_address: TO_ADDRESS_MOCK, - } as unknown as ParseOutput); + origin: ORIGIN_MOCK, + } as unknown as DeeplinkRequest); expect(mockAddTransaction).toHaveBeenCalledTimes(1); }); @@ -214,7 +221,8 @@ describe('addTransactionForDeeplink', () => { uint256: '1000', }, target_address: ERC20_ADDRESS_MOCK, - } as unknown as ParseOutput); + origin: ORIGIN_MOCK, + } as unknown as DeeplinkRequest); expect(mockGenerateTransferData).toHaveBeenCalledWith( 'transfer', @@ -232,7 +240,7 @@ describe('addTransactionForDeeplink', () => { }, { networkClientId: 'mainnet', - origin: 'deeplink', + origin: ORIGIN_MOCK, type: TransactionType.tokenMethodTransfer, }, ); diff --git a/app/components/Views/confirmations/utils/deeplink.ts b/app/components/Views/confirmations/utils/deeplink.ts index b9c78f40eea4..f874a257afa5 100644 --- a/app/components/Views/confirmations/utils/deeplink.ts +++ b/app/components/Views/confirmations/utils/deeplink.ts @@ -16,6 +16,8 @@ import { ETH_ACTIONS } from '../../../../constants/deeplinks'; import Engine from '../../../../core/Engine'; import Logger from '../../../../util/Logger'; +export type DeeplinkRequest = ParseOutput & { origin: string }; + const getNetworkClientIdForChainId = (chainId: Hex) => { const { NetworkController } = Engine.context; const selectedNetworkClientId = getGlobalNetworkClientId(); @@ -60,7 +62,8 @@ export async function addTransactionForDeeplink({ function_name, parameters, target_address, -}: ParseOutput) { + origin, +}: DeeplinkRequest) { const { AccountsController } = Engine.context; // Temporary solution for preventing back to back deeplink requests @@ -82,12 +85,12 @@ export async function addTransactionForDeeplink({ chainId = CHAIN_IDS.MAINNET; } - // This should be anything *except* 'MMM' (MetaMask Mobile) to avoid layout issues in redesigned confirmations - const origin = 'deeplink'; const networkClientId = getNetworkClientIdForChainId(chainId); const from = safeToChecksumAddress(selectedAccountAddress) as string; const to = safeToChecksumAddress(target_address); - const checkSummedParamAddress = safeToChecksumAddress(parameters?.address ?? ''); + const checkSummedParamAddress = safeToChecksumAddress( + parameters?.address ?? '', + ); if (function_name === ETH_ACTIONS.TRANSFER) { // ERC20 transfer diff --git a/app/core/DeeplinkManager/Handlers/handleEthereumUrl.test.ts b/app/core/DeeplinkManager/Handlers/handleEthereumUrl.test.ts index 31b65676cddd..597d732f298b 100644 --- a/app/core/DeeplinkManager/Handlers/handleEthereumUrl.test.ts +++ b/app/core/DeeplinkManager/Handlers/handleEthereumUrl.test.ts @@ -215,6 +215,7 @@ describe('handleEthereumUrl', () => { function_name: ETH_ACTIONS.TRANSFER, chain_id: 1, source: url, + origin, }); }); diff --git a/app/core/DeeplinkManager/Handlers/handleEthereumUrl.ts b/app/core/DeeplinkManager/Handlers/handleEthereumUrl.ts index 0386c38193b1..d01de15f54bb 100644 --- a/app/core/DeeplinkManager/Handlers/handleEthereumUrl.ts +++ b/app/core/DeeplinkManager/Handlers/handleEthereumUrl.ts @@ -49,7 +49,10 @@ async function handleEthereumUrl({ } if (isDeeplinkRedesignedConfirmationCompatible(ethUrl.function_name)) { - await addTransactionForDeeplink(txMeta); + await addTransactionForDeeplink({ + ...txMeta, + origin, + }); return; } diff --git a/locales/languages/en.json b/locales/languages/en.json index e83b04a9a929..39181181f1e7 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -1838,6 +1838,8 @@ "sign_description_2": "tap on Get Signature", "sign_get_signature": "Get Signature", "transaction_id": "Transaction ID", + "network": "Network", + "request_from": "Request from", "network_fee": "Network Fee", "network_fee_tooltip": "Amount paid to process the transaction on network." }, From f7fc080d77477f54e3c044a147e70dbcfe8f8320 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Mon, 12 May 2025 10:54:27 +0200 Subject: [PATCH 2/2] Fix lint --- app/components/Views/confirmations/utils/deeplink.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/components/Views/confirmations/utils/deeplink.test.ts b/app/components/Views/confirmations/utils/deeplink.test.ts index a11d76aaa17f..aab42fb02a55 100644 --- a/app/components/Views/confirmations/utils/deeplink.test.ts +++ b/app/components/Views/confirmations/utils/deeplink.test.ts @@ -1,5 +1,4 @@ import { TransactionType } from '@metamask/transaction-controller'; -import { ParseOutput } from 'eth-url-parser'; import { ETH_ACTIONS } from '../../../../constants/deeplinks'; import { selectConfirmationRedesignFlagsFromRemoteFeatureFlags } from '../../../../selectors/featureFlagController/confirmations'; @@ -9,8 +8,8 @@ import { generateTransferData } from '../../../../util/transactions'; import { addTransactionForDeeplink, isDeeplinkRedesignedConfirmationCompatible, + type DeeplinkRequest, } from './deeplink'; -import { type DeeplinkRequest } from './deeplink'; const ORIGIN_MOCK = 'example.test-dapp.com';