Skip to content

Commit 33ae98a

Browse files
authored
feat: Implement network-row component for redesigned transfer transactions (#15281)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR implements `network-row` component for transfer confirmations. ## **Related issues** Fixes: MetaMask/MetaMask-planning#4796 ## **Manual testing steps** Only possible to test it out via setting `FEATURE_FLAG_REDESIGNED_TRANSFER` to `true`. This flow is not yet manually QA'ed hence the `No QA Needed` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** Wallet initiated example ![Screenshot 2025-05-12 at 10 43 36](https://github.com/user-attachments/assets/1be30b37-c3bf-4cbf-b049-9728ebf3ecd8) DApp initiated example ![Screenshot 2025-05-12 at 10 43 18](https://github.com/user-attachments/assets/8fa5ddf8-d01f-417e-b430-8c879595c8b1) ## **Pre-merge author checklist** - [X] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [X] I've completed the PR template to the best of my ability - [X] I’ve included tests if applicable - [X] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [X] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent 92f9b5c commit 33ae98a

File tree

11 files changed

+231
-14
lines changed

11 files changed

+231
-14
lines changed

app/components/Views/confirmations/components/info/transfer/transfer.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ describe('Transfer', () => {
9393
expect(mockUseClearConfirmationOnBackSwipe).toHaveBeenCalled();
9494
expect(getByText('0xDc477...0c164')).toBeDefined();
9595
expect(getByText('Network Fee')).toBeDefined();
96+
expect(getByText('Network')).toBeDefined();
9697
expect(getNavbar).toHaveBeenCalled();
9798
expect(getNavbar).toHaveBeenCalledWith({
9899
title: 'Review',

app/components/Views/confirmations/components/info/transfer/transfer.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import FromTo from '../../rows/transactions/from-to';
1212
import GasFeesDetails from '../../rows/transactions/gas-fee-details';
1313
import AdvancedDetailsRow from '../../rows/transactions/advanced-details-row/advanced-details-row';
1414
import TokenHero from '../../rows/transactions/token-hero';
15+
import NetworkRow from '../../rows/transactions/network-row';
1516
import styleSheet from './transfer.styles';
1617

1718
const Transfer = () => {
@@ -28,6 +29,7 @@ const Transfer = () => {
2829
<View>
2930
<TokenHero />
3031
<FromTo />
32+
<NetworkRow />
3133
<View style={styles.simulationsDetailsContainer}>
3234
<SimulationDetails
3335
transaction={transactionMetadata as TransactionMeta}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './network-row';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { StyleSheet } from 'react-native';
2+
3+
const styleSheet = () =>
4+
StyleSheet.create({
5+
infoRowOverride: {
6+
paddingBottom: 4,
7+
},
8+
networkRowContainer: {
9+
flexDirection: 'row',
10+
alignItems: 'center',
11+
},
12+
avatarNetwork: {
13+
marginRight: 4,
14+
},
15+
});
16+
17+
export default styleSheet;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import React from 'react';
2+
import { merge } from 'lodash';
3+
import { Hex } from '@metamask/utils';
4+
5+
import renderWithProvider from '../../../../../../../util/test/renderWithProvider';
6+
import { transferConfirmationState } from '../../../../../../../util/test/confirm-data-helpers';
7+
import { MMM_ORIGIN } from '../../../../constants/confirmations';
8+
import NetworkRow from './network-row';
9+
10+
jest.mock('../../../../hooks/metrics/useConfirmationMetricEvents');
11+
jest.mock('../../../../../../../core/Engine', () => ({
12+
context: {
13+
GasFeeController: {
14+
startPolling: jest.fn(),
15+
stopPollingByPollingToken: jest.fn(),
16+
},
17+
NetworkController: {
18+
getNetworkConfigurationByNetworkClientId: jest.fn(),
19+
},
20+
TokenListController: {
21+
fetchTokenList: jest.fn(),
22+
},
23+
},
24+
}));
25+
26+
const MOCK_DAPP_ORIGIN = 'https://exampledapp.com';
27+
28+
const mockNetworkImage = { uri: 'https://mocknetwork.com/image.png' };
29+
jest.mock('../../../../../../../util/networks', () => ({
30+
getNetworkImageSource: jest.fn(() => mockNetworkImage),
31+
}));
32+
33+
const networkConfigurationMock = {
34+
name: 'Test Network',
35+
chainId: '0x1' as Hex,
36+
};
37+
38+
// Mock state with transaction from MetaMask Mobile origin
39+
const internalTransactionState = merge({}, transferConfirmationState, {
40+
engine: {
41+
backgroundState: {
42+
TransactionController: {
43+
transactions: [
44+
{
45+
txParams: {
46+
from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
47+
},
48+
origin: MMM_ORIGIN,
49+
},
50+
],
51+
},
52+
NetworkController: {
53+
networkConfigurationsByChainId: {
54+
'0x1': networkConfigurationMock,
55+
},
56+
},
57+
},
58+
},
59+
});
60+
61+
// Mock state with transaction from external dapp origin
62+
const dappOriginTransactionState = merge({}, transferConfirmationState, {
63+
engine: {
64+
backgroundState: {
65+
TransactionController: {
66+
transactions: [
67+
{
68+
txParams: {
69+
from: '0xdc47789de4ceff0e8fe9d15d728af7f17550c164',
70+
},
71+
origin: MOCK_DAPP_ORIGIN,
72+
},
73+
],
74+
},
75+
NetworkController: {
76+
networkConfigurationsByChainId: {
77+
'0x1': networkConfigurationMock,
78+
},
79+
},
80+
},
81+
},
82+
});
83+
84+
describe('NetworkRow', () => {
85+
it('displays the correct network name', async () => {
86+
const { getByText } = renderWithProvider(<NetworkRow />, {
87+
state: internalTransactionState,
88+
});
89+
90+
expect(getByText('Test Network')).toBeDefined();
91+
expect(getByText('Network')).toBeDefined();
92+
});
93+
94+
it('displays origin info for dapp transactions', async () => {
95+
const { getByText } = renderWithProvider(<NetworkRow />, {
96+
state: dappOriginTransactionState,
97+
});
98+
99+
expect(getByText('Test Network')).toBeDefined();
100+
expect(getByText('Request from')).toBeDefined();
101+
expect(getByText(MOCK_DAPP_ORIGIN)).toBeDefined();
102+
});
103+
104+
it('does not display origin info for internal transactions', async () => {
105+
const { getByText, queryByText } = renderWithProvider(<NetworkRow />, {
106+
state: internalTransactionState,
107+
});
108+
109+
expect(getByText('Test Network')).toBeDefined();
110+
expect(queryByText('Request from')).toBeNull();
111+
expect(queryByText(MMM_ORIGIN)).toBeNull();
112+
});
113+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
import { View } from 'react-native';
3+
import { useSelector } from 'react-redux';
4+
import { Hex } from '@metamask/utils';
5+
6+
import { useTransactionMetadataRequest } from '../../../../hooks/transactions/useTransactionMetadataRequest';
7+
import { selectNetworkConfigurationByChainId } from '../../../../../../../selectors/networkController';
8+
import Text, {
9+
TextVariant,
10+
} from '../../../../../../../component-library/components/Texts/Text';
11+
import { useStyles } from '../../../../../../../component-library/hooks';
12+
import { getNetworkImageSource } from '../../../../../../../util/networks';
13+
import { strings } from '../../../../../../../../locales/i18n';
14+
import { RootState } from '../../../../../../../reducers';
15+
import InfoSection from '../../../UI/info-row/info-section';
16+
import InfoRow from '../../../UI/info-row/info-row';
17+
import { MMM_ORIGIN } from '../../../../constants/confirmations';
18+
import styleSheet from './network-row.styles';
19+
import AvatarNetwork from '../../../../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork';
20+
import { AvatarSize } from '../../../../../../../component-library/components/Avatars/Avatar/Avatar.types';
21+
22+
const NetworkRow = () => {
23+
const { styles } = useStyles(styleSheet, {});
24+
const transactionMetadata = useTransactionMetadataRequest();
25+
const chainId = transactionMetadata?.chainId;
26+
const origin = transactionMetadata?.origin;
27+
const networkConfiguration = useSelector((state: RootState) =>
28+
selectNetworkConfigurationByChainId(state, chainId),
29+
);
30+
const isDappOrigin = origin !== MMM_ORIGIN;
31+
const networkImage = getNetworkImageSource({ chainId: chainId as Hex });
32+
33+
if (!transactionMetadata) {
34+
return null;
35+
}
36+
37+
return (
38+
<InfoSection>
39+
<InfoRow
40+
label={strings('transactions.network')}
41+
style={styles.infoRowOverride}
42+
>
43+
<View style={styles.networkRowContainer}>
44+
{networkImage && (
45+
<AvatarNetwork
46+
size={AvatarSize.Xs}
47+
imageSource={networkImage}
48+
style={styles.avatarNetwork}
49+
/>
50+
)}
51+
<Text variant={TextVariant.BodyMD}>{networkConfiguration?.name}</Text>
52+
</View>
53+
</InfoRow>
54+
55+
{isDappOrigin && (
56+
<InfoRow
57+
label={strings('transactions.request_from')}
58+
style={styles.infoRowOverride}
59+
>
60+
<Text variant={TextVariant.BodyMD}>{origin}</Text>
61+
</InfoRow>
62+
)}
63+
</InfoSection>
64+
);
65+
};
66+
67+
export default NetworkRow;

app/components/Views/confirmations/utils/deeplink.test.ts

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { TransactionType } from '@metamask/transaction-controller';
2-
import { ParseOutput } from 'eth-url-parser';
32

43
import { ETH_ACTIONS } from '../../../../constants/deeplinks';
54
import { selectConfirmationRedesignFlagsFromRemoteFeatureFlags } from '../../../../selectors/featureFlagController/confirmations';
@@ -9,8 +8,11 @@ import { generateTransferData } from '../../../../util/transactions';
98
import {
109
addTransactionForDeeplink,
1110
isDeeplinkRedesignedConfirmationCompatible,
11+
type DeeplinkRequest,
1212
} from './deeplink';
1313

14+
const ORIGIN_MOCK = 'example.test-dapp.com';
15+
1416
jest.mock('../../../../core/Engine', () => ({
1517
context: {
1618
RemoteFeatureFlagController: {
@@ -142,7 +144,8 @@ describe('addTransactionForDeeplink', () => {
142144
value: '1000',
143145
},
144146
target_address: TO_ADDRESS_MOCK,
145-
} as unknown as ParseOutput);
147+
origin: ORIGIN_MOCK,
148+
} as unknown as DeeplinkRequest);
146149

147150
expect(mockAddTransaction).toHaveBeenCalledWith(
148151
{
@@ -152,7 +155,7 @@ describe('addTransactionForDeeplink', () => {
152155
},
153156
{
154157
networkClientId: 'another-network',
155-
origin: 'deeplink',
158+
origin: ORIGIN_MOCK,
156159
type: TransactionType.simpleSend,
157160
},
158161
);
@@ -164,7 +167,8 @@ describe('addTransactionForDeeplink', () => {
164167
value: '1000',
165168
},
166169
target_address: TO_ADDRESS_MOCK,
167-
} as unknown as ParseOutput);
170+
origin: ORIGIN_MOCK,
171+
} as unknown as DeeplinkRequest);
168172

169173
expect(mockAddTransaction).toHaveBeenCalledWith(
170174
{
@@ -174,7 +178,7 @@ describe('addTransactionForDeeplink', () => {
174178
},
175179
{
176180
networkClientId: 'mainnet',
177-
origin: 'deeplink',
181+
origin: ORIGIN_MOCK,
178182
type: TransactionType.simpleSend,
179183
},
180184
);
@@ -187,7 +191,8 @@ describe('addTransactionForDeeplink', () => {
187191
value: '1000',
188192
},
189193
target_address: TO_ADDRESS_MOCK,
190-
} as unknown as ParseOutput);
194+
origin: ORIGIN_MOCK,
195+
} as unknown as DeeplinkRequest);
191196

192197
expect(mockAddTransaction).toHaveBeenCalledTimes(1);
193198

@@ -196,7 +201,8 @@ describe('addTransactionForDeeplink', () => {
196201
value: '9999',
197202
},
198203
target_address: TO_ADDRESS_MOCK,
199-
} as unknown as ParseOutput);
204+
origin: ORIGIN_MOCK,
205+
} as unknown as DeeplinkRequest);
200206

201207
expect(mockAddTransaction).toHaveBeenCalledTimes(1);
202208
});
@@ -214,7 +220,8 @@ describe('addTransactionForDeeplink', () => {
214220
uint256: '1000',
215221
},
216222
target_address: ERC20_ADDRESS_MOCK,
217-
} as unknown as ParseOutput);
223+
origin: ORIGIN_MOCK,
224+
} as unknown as DeeplinkRequest);
218225

219226
expect(mockGenerateTransferData).toHaveBeenCalledWith(
220227
'transfer',
@@ -232,7 +239,7 @@ describe('addTransactionForDeeplink', () => {
232239
},
233240
{
234241
networkClientId: 'mainnet',
235-
origin: 'deeplink',
242+
origin: ORIGIN_MOCK,
236243
type: TransactionType.tokenMethodTransfer,
237244
},
238245
);

app/components/Views/confirmations/utils/deeplink.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { ETH_ACTIONS } from '../../../../constants/deeplinks';
1616
import Engine from '../../../../core/Engine';
1717
import Logger from '../../../../util/Logger';
1818

19+
export type DeeplinkRequest = ParseOutput & { origin: string };
20+
1921
const getNetworkClientIdForChainId = (chainId: Hex) => {
2022
const { NetworkController } = Engine.context;
2123
const selectedNetworkClientId = getGlobalNetworkClientId();
@@ -60,7 +62,8 @@ export async function addTransactionForDeeplink({
6062
function_name,
6163
parameters,
6264
target_address,
63-
}: ParseOutput) {
65+
origin,
66+
}: DeeplinkRequest) {
6467
const { AccountsController } = Engine.context;
6568

6669
// Temporary solution for preventing back to back deeplink requests
@@ -82,12 +85,12 @@ export async function addTransactionForDeeplink({
8285
chainId = CHAIN_IDS.MAINNET;
8386
}
8487

85-
// This should be anything *except* 'MMM' (MetaMask Mobile) to avoid layout issues in redesigned confirmations
86-
const origin = 'deeplink';
8788
const networkClientId = getNetworkClientIdForChainId(chainId);
8889
const from = safeToChecksumAddress(selectedAccountAddress) as string;
8990
const to = safeToChecksumAddress(target_address);
90-
const checkSummedParamAddress = safeToChecksumAddress(parameters?.address ?? '');
91+
const checkSummedParamAddress = safeToChecksumAddress(
92+
parameters?.address ?? '',
93+
);
9194

9295
if (function_name === ETH_ACTIONS.TRANSFER) {
9396
// ERC20 transfer

app/core/DeeplinkManager/Handlers/handleEthereumUrl.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ describe('handleEthereumUrl', () => {
215215
function_name: ETH_ACTIONS.TRANSFER,
216216
chain_id: 1,
217217
source: url,
218+
origin,
218219
});
219220
});
220221

app/core/DeeplinkManager/Handlers/handleEthereumUrl.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ async function handleEthereumUrl({
4949
}
5050

5151
if (isDeeplinkRedesignedConfirmationCompatible(ethUrl.function_name)) {
52-
await addTransactionForDeeplink(txMeta);
52+
await addTransactionForDeeplink({
53+
...txMeta,
54+
origin,
55+
});
5356
return;
5457
}
5558

locales/languages/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,8 @@
18451845
"sign_description_2": "tap on Get Signature",
18461846
"sign_get_signature": "Get Signature",
18471847
"transaction_id": "Transaction ID",
1848+
"network": "Network",
1849+
"request_from": "Request from",
18481850
"network_fee": "Network Fee",
18491851
"network_fee_tooltip": "Amount paid to process the transaction on network."
18501852
},

0 commit comments

Comments
 (0)