Skip to content

Commit ed09cd4

Browse files
feat: Integrate deeplink and dapp initated transfer confirmations (#14916)
<!-- 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 aims to integrate deeplink and dapp initated redesigned transfer confirmations. **Labels:** Note that transfer confirmations can only be activated by setting env variable. Since this indicates feature is not yet released - `no-changelog` `No QA Needed` is added to the PR. ## **Related issues** Fixes: MetaMask/MetaMask-planning#4771 ## **Manual testing steps** - Deeplink and dapp initiated transfer confirmations will show redesigned transfer confirmation. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** Deeplink native transfer https://github.com/user-attachments/assets/855dfe42-f69d-472e-8625-46460b8e5350 Deeplink ETH USDC transfer https://github.com/user-attachments/assets/49d28788-e36c-4d4f-bea9-a65bda5ddc7b Deeplink Linea USDC transfer https://github.com/user-attachments/assets/26408d45-6876-46e1-bdf5-ab52e995b336 DApp initiated transfers https://github.com/user-attachments/assets/fc79886a-2c4c-49f4-bb79-e54b64007812 ## **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. --------- Co-authored-by: Nico MASSART <[email protected]>
1 parent 7ee569e commit ed09cd4

38 files changed

+697
-66
lines changed

app/components/UI/SimulationDetails/SimulationDetails.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@ export const SimulationDetails: React.FC<SimulationDetailsProps> = ({
140140
isTransactionsRedesign = false,
141141
}: SimulationDetailsProps) => {
142142
const { styles } = useStyles(styleSheet, { isTransactionsRedesign });
143-
const { chainId, id: transactionId, simulationData } = transaction;
144-
const balanceChangesResult = useBalanceChanges({ chainId, simulationData });
143+
const { chainId, id: transactionId, simulationData, networkClientId } = transaction;
144+
const balanceChangesResult = useBalanceChanges({ chainId, simulationData, networkClientId });
145145
const loading = !simulationData || balanceChangesResult.pending;
146146

147147
useSimulationMetrics({

app/components/UI/SimulationDetails/useBalanceChanges.test.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const mockFetchTokenContractExchangeRates =
4343
fetchTokenContractExchangeRates as jest.Mock;
4444

4545
const ETH_TO_FIAT_RATE = 3;
46-
46+
const NETWORK_CLIENT_ID_MOCK = 'mainnet';
4747
const ERC20_TOKEN_ADDRESS_1_MOCK: Hex = '0x0erc20_1';
4848
const ERC20_TOKEN_ADDRESS_2_MOCK: Hex = '0x0erc20_2';
4949
const ERC20_TOKEN_ADDRESS_3_MOCK: Hex = '0x0erc20_3';
@@ -103,6 +103,7 @@ describe('useBalanceChanges', () => {
103103
useBalanceChanges({
104104
chainId: CHAIN_ID_MOCK,
105105
simulationData: undefined,
106+
networkClientId: NETWORK_CLIENT_ID_MOCK,
106107
}),
107108
);
108109
expect(result.current).toEqual({ pending: true, value: [] });
@@ -127,6 +128,7 @@ describe('useBalanceChanges', () => {
127128
useBalanceChanges({
128129
chainId: CHAIN_ID_MOCK,
129130
simulationData,
131+
networkClientId: NETWORK_CLIENT_ID_MOCK,
130132
}),
131133
);
132134

@@ -154,6 +156,7 @@ describe('useBalanceChanges', () => {
154156
useBalanceChanges({
155157
chainId: CHAIN_ID_MOCK,
156158
simulationData,
159+
networkClientId: NETWORK_CLIENT_ID_MOCK,
157160
}),
158161
);
159162

@@ -174,6 +177,7 @@ describe('useBalanceChanges', () => {
174177
useBalanceChanges({
175178
chainId: CHAIN_ID_MOCK,
176179
simulationData,
180+
networkClientId: NETWORK_CLIENT_ID_MOCK,
177181
}),
178182
);
179183
};
@@ -327,6 +331,7 @@ describe('useBalanceChanges', () => {
327331
useBalanceChanges({
328332
chainId: CHAIN_ID_MOCK,
329333
simulationData,
334+
networkClientId: NETWORK_CLIENT_ID_MOCK,
330335
}),
331336
);
332337
};
@@ -394,6 +399,7 @@ describe('useBalanceChanges', () => {
394399
useBalanceChanges({
395400
chainId: CHAIN_ID_MOCK,
396401
simulationData,
402+
networkClientId: NETWORK_CLIENT_ID_MOCK,
397403
}),
398404
);
399405

app/components/UI/SimulationDetails/useBalanceChanges.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ function getAssetAmount(
6060
}
6161

6262
// Fetches the decimals for the given token address.
63-
async function fetchErc20Decimals(address: Hex): Promise<number> {
63+
async function fetchErc20Decimals(address: Hex, networkClientId: string): Promise<number> {
6464
try {
65-
const { decimals } = await getTokenDetails(address);
65+
const { decimals } = await getTokenDetails(address,undefined,undefined,networkClientId);
6666
return decimals ? parseInt(decimals, 10) : ERC20_DEFAULT_DECIMALS;
6767
} catch {
6868
return ERC20_DEFAULT_DECIMALS;
@@ -72,12 +72,13 @@ async function fetchErc20Decimals(address: Hex): Promise<number> {
7272
// Fetches token details for all the token addresses in the SimulationTokenBalanceChanges
7373
async function fetchAllErc20Decimals(
7474
addresses: Hex[],
75+
networkClientId: string,
7576
): Promise<Record<Hex, number>> {
7677
const uniqueAddresses = [
7778
...new Set(addresses.map((address) => address.toLowerCase() as Hex)),
7879
];
7980
const allDecimals = await Promise.all(
80-
uniqueAddresses.map(fetchErc20Decimals),
81+
uniqueAddresses.map((address) => fetchErc20Decimals(address, networkClientId)),
8182
);
8283
return Object.fromEntries(
8384
allDecimals.map((decimals, i) => [uniqueAddresses[i], decimals]),
@@ -183,9 +184,11 @@ function getTokenBalanceChanges(
183184
export default function useBalanceChanges({
184185
chainId,
185186
simulationData,
187+
networkClientId,
186188
}: {
187189
chainId: Hex;
188190
simulationData?: SimulationData;
191+
networkClientId: string;
189192
}): { pending: boolean; value: BalanceChange[] } {
190193
const nativeFiatRate = useSelector((state: RootState) => selectConversionRateByChainId(state, chainId)) as number;
191194
const fiatCurrency = useSelector(selectCurrentCurrency);
@@ -202,7 +205,7 @@ export default function useBalanceChanges({
202205
.map((tbc: any) => tbc.address);
203206

204207
const erc20Decimals = useAsyncResultOrThrow(
205-
() => fetchAllErc20Decimals(erc20TokenAddresses),
208+
() => fetchAllErc20Decimals(erc20TokenAddresses, networkClientId),
206209
[JSON.stringify(erc20TokenAddresses)],
207210
);
208211

app/components/Views/confirmations/components/confirm/confirm-component.test.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ jest.mock('../../../../../core/Engine', () => ({
8787
},
8888
},
8989
},
90+
TokenListController: {
91+
fetchTokenList: jest.fn(),
92+
},
9093
},
9194
controllerMessenger: {
9295
subscribe: jest.fn(),

app/components/Views/confirmations/components/confirm/confirm-root.test.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ jest.mock('@react-navigation/native', () => ({
1919
}),
2020
}));
2121

22+
jest.mock('../../../../../core/Engine', () => ({
23+
context: {
24+
TokenListController: {
25+
fetchTokenList: jest.fn(),
26+
},
27+
},
28+
}));
29+
2230
describe('Confirm', () => {
2331
beforeEach(() => {
2432
jest.clearAllMocks();

app/components/Views/confirmations/components/footer/footer.test.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ const mockTrackAlertMetrics = jest.fn();
5151
trackAlertMetrics: mockTrackAlertMetrics,
5252
});
5353

54+
jest.mock('../../../../../core/Engine', () => ({
55+
context: {
56+
TokenListController: {
57+
fetchTokenList: jest.fn(),
58+
},
59+
},
60+
}));
61+
5462
const ALERT_MESSAGE_MOCK = 'This is a test alert message.';
5563
const ALERT_DETAILS_MOCK = ['Detail 1', 'Detail 2'];
5664
const mockAlerts = [

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

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jest.mock('../../../../../../core/Engine', () => ({
1616
startPolling: jest.fn(),
1717
stopPollingByPollingToken: jest.fn(),
1818
},
19+
TokenListController: {
20+
fetchTokenList: jest.fn(),
21+
},
1922
},
2023
}));
2124

app/components/Views/confirmations/components/rows/transactions/advanced-details-row/advanced-details-row.test.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ jest.mock('../../../../../../hooks/useEditNonce', () => ({
2525
useEditNonce: jest.fn(),
2626
}));
2727

28+
jest.mock('../../../../../../../core/Engine', () => ({
29+
context: {
30+
TokenListController: {
31+
fetchTokenList: jest.fn(),
32+
},
33+
},
34+
}));
35+
2836
describe('AdvancedDetailsRow', () => {
2937
const mockUseEditNonce = {
3038
setShowNonceModal: jest.fn(),

app/components/Views/confirmations/components/rows/transactions/from-to/from-to.test.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jest.mock('../../../../../../../core/Engine', () => ({
1717
NetworkController: {
1818
getNetworkConfigurationByNetworkClientId: jest.fn(),
1919
},
20+
TokenListController: {
21+
fetchTokenList: jest.fn(),
22+
},
2023
},
2124
}));
2225

app/components/Views/confirmations/components/rows/transactions/gas-fee-details/gas-fee-details.test.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ jest.mock('../../../../../../../core/Engine', () => ({
1919
NetworkController: {
2020
getNetworkConfigurationByNetworkClientId: jest.fn(),
2121
},
22+
TokenListController: {
23+
fetchTokenList: jest.fn(),
24+
},
2225
},
2326
}));
2427

app/components/Views/confirmations/components/rows/transactions/token-hero/token-hero.styles.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { StyleSheet } from 'react-native';
22
import { Theme } from '../../../../../../../util/theme/models';
33

4-
const styleSheet = (params: { theme: Theme }) => {
5-
const { theme } = params;
4+
const styleSheet = (params: {
5+
theme: Theme;
6+
vars: { isFlatConfirmation: boolean };
7+
}) => {
8+
const { theme, vars } = params;
9+
const { isFlatConfirmation } = vars;
610

711
return StyleSheet.create({
812
assetAmountContainer: {
@@ -25,7 +29,8 @@ const styleSheet = (params: { theme: Theme }) => {
2529
height: 48,
2630
},
2731
container: {
28-
paddingVertical: 16,
32+
paddingBottom: 16,
33+
paddingTop: isFlatConfirmation ? 16 : 0,
2934
},
3035
});
3136
};

app/components/Views/confirmations/components/rows/transactions/token-hero/token-hero.test.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import { merge } from 'lodash';
88
import { RootState } from '../../../../../../../reducers';
99
import { decGWEIToHexWEI } from '../../../../../../../util/conversions';
1010

11+
jest.mock('../../../../../../../core/Engine', () => ({
12+
context: {
13+
TokenListController: {
14+
fetchTokenList: jest.fn(),
15+
},
16+
},
17+
}));
18+
1119
describe('TokenHero', () => {
1220
it('contains token and fiat values for staking deposit', async () => {
1321
const { getByText } = renderWithProvider(<TokenHero />, {

app/components/Views/confirmations/components/rows/transactions/token-hero/token-hero.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useStyles } from '../../../../../../../component-library/hooks';
1414
import images from '../../../../../../../images/image-icons';
1515
import TokenIcon from '../../../../../../UI/Swaps/components/TokenIcon';
1616
import { useTokenValues } from '../../../../hooks/useTokenValues';
17+
import { useFlatConfirmation } from '../../../../hooks/ui/useFlatConfirmation';
1718
import { TooltipModal } from '../../../UI/Tooltip/Tooltip';
1819
import styleSheet from './token-hero.styles';
1920

@@ -75,7 +76,10 @@ const AssetFiatConversion = ({
7576
);
7677

7778
const TokenHero = ({ amountWei }: { amountWei?: string }) => {
78-
const { styles } = useStyles(styleSheet, {});
79+
const { isFlatConfirmation } = useFlatConfirmation();
80+
const { styles } = useStyles(styleSheet, {
81+
isFlatConfirmation,
82+
});
7983
const { tokenAmountValue, tokenAmountDisplayValue, fiatDisplayValue } =
8084
useTokenValues({ amountWei });
8185
const [isModalVisible, setIsModalVisible] = useState(false);

app/components/Views/confirmations/components/title/title.test.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@ import {
55
siweSignatureConfirmationState,
66
typedSignV4ConfirmationState,
77
typedSignV4NFTConfirmationState,
8+
transferConfirmationState,
89
} from '../../../../../util/test/confirm-data-helpers';
910
import renderWithProvider from '../../../../../util/test/renderWithProvider';
1011
import Title from './title';
1112

13+
jest.mock('../../../../../core/Engine', () => ({
14+
context: {
15+
TokenListController: {
16+
fetchTokenList: jest.fn(),
17+
},
18+
},
19+
}));
1220

1321
describe('Confirm Title', () => {
1422
it('renders the title and subtitle for a permit signature', () => {
@@ -62,4 +70,12 @@ describe('Confirm Title', () => {
6270
getByText('Review request details before you confirm.'),
6371
).toBeTruthy();
6472
});
73+
74+
it('renders correct title for transfer', () => {
75+
const { getByText } = renderWithProvider(<Title />, {
76+
state: transferConfirmationState,
77+
});
78+
expect(getByText('Transfer request')).toBeTruthy();
79+
});
80+
6581
});

app/components/Views/confirmations/components/title/title.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { SignatureRequest } from '@metamask/signature-controller';
33
import { TransactionMeta, TransactionType } from '@metamask/transaction-controller';
44
import React from 'react';
55
import { View } from 'react-native';
6+
import { ApprovalType } from '@metamask/controller-utils';
67

78
import { strings } from '../../../../../../locales/i18n';
89
import Text from '../../../../../component-library/components/Texts/Text';
@@ -12,8 +13,8 @@ import { useSignatureRequest } from '../../hooks/signatures/useSignatureRequest'
1213
import { useStandaloneConfirmation } from '../../hooks/ui/useStandaloneConfirmation';
1314
import { useTransactionMetadataRequest } from '../../hooks/transactions/useTransactionMetadataRequest';
1415
import { isPermitDaiRevoke, isRecognizedPermit, isSIWESignatureRequest, parseTypedDataMessageFromSignatureRequest } from '../../utils/signature';
16+
import { REDESIGNED_TRANSFER_TYPES } from '../../constants/confirmations';
1517
import styleSheet from './title.styles';
16-
import { ApprovalType } from '@metamask/controller-utils';
1718

1819
const getTitleAndSubTitle = (
1920
approvalRequest?: ApprovalRequest<{ data: string }>,
@@ -78,6 +79,15 @@ const getTitleAndSubTitle = (
7879
subTitle: strings('confirm.sub_title.contract_interaction'),
7980
};
8081
}
82+
if (
83+
REDESIGNED_TRANSFER_TYPES.includes(
84+
transactionMetadata?.type as TransactionType,
85+
)
86+
) {
87+
return {
88+
title: strings('confirm.title.transfer'),
89+
};
90+
}
8191
return {};
8292
}
8393
default:

app/components/Views/confirmations/external/staking/components/staking-contract-interaction-details/staking-contract-interaction-details.test.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@ import { stakingDepositConfirmationState, stakingWithdrawalConfirmationState } f
33
import renderWithProvider from '../../../../../../../util/test/renderWithProvider';
44
import StakingContractInteractionDetails from './staking-contract-interaction-details';
55

6+
jest.mock('../../../../../../../core/Engine', () => ({
7+
context: {
8+
TokenListController: {
9+
fetchTokenList: jest.fn(),
10+
},
11+
},
12+
}));
13+
614
describe('StakingContractInteractionDetails', () => {
7-
it('should render correctly with staking deposit variant', () => {
15+
it('renders staking deposit variant', () => {
816
const { getByText } = renderWithProvider(<StakingContractInteractionDetails />, {
917
state: stakingDepositConfirmationState,
1018
});
@@ -14,7 +22,7 @@ describe('StakingContractInteractionDetails', () => {
1422
expect(getByText('Ethereum Mainnet')).toBeDefined();
1523
});
1624

17-
it('should render correctly with staking withdrawal variant', () => {
25+
it('renders staking withdrawal variant', () => {
1826
const { getByText } = renderWithProvider(<StakingContractInteractionDetails />, {
1927
state: stakingWithdrawalConfirmationState,
2028
});

app/components/Views/confirmations/external/staking/components/staking-details/staking-details.test.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ import { useConfirmationMetricEvents } from '../../../../hooks/metrics/useConfir
1313
import StakingDetails from './staking-details';
1414

1515
jest.mock('../../../../hooks/metrics/useConfirmationMetricEvents');
16+
jest.mock('../../../../../../../core/Engine', () => ({
17+
context: {
18+
TokenListController: {
19+
fetchTokenList: jest.fn(),
20+
},
21+
},
22+
}));
1623

1724
describe('StakingDetails', () => {
1825
const useConfirmationMetricEventsMock = jest.mocked(

app/components/Views/confirmations/external/staking/hooks/useStakingDetails.test.ts

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { renderHookWithProvider } from '../../../../../../util/test/renderWithPr
55
import { useStakingDetails } from './useStakingDetails';
66
import { mockEarnControllerRootState } from '../../../../../UI/Stake/testUtils';
77

8+
jest.mock('../../../../../../core/Engine', () => ({
9+
context: {
10+
TokenListController: {
11+
fetchTokenList: jest.fn(),
12+
},
13+
},
14+
}));
15+
816
describe('useStakingDetails', () => {
917
const mockEarnControllerState =
1018
mockEarnControllerRootState().engine.backgroundState.EarnController;

app/components/Views/confirmations/external/staking/info/staking-claim/staking-claim.test.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jest.mock('../../../../../../../core/Engine', () => ({
1616
startPolling: jest.fn(),
1717
stopPollingByPollingToken: jest.fn(),
1818
},
19+
TokenListController: {
20+
fetchTokenList: jest.fn(),
21+
},
1922
},
2023
}));
2124

0 commit comments

Comments
 (0)