Skip to content

Commit 4e88440

Browse files
authored
feat: MUSD-108 Pre-select payment token for mUSD conversion flow (MetaMask#23225)
<!-- 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** Adds payment token pre-selection support in `useAutomaticTransactionPayToken` hook. mUSD conversion flow uses this to preselect based on the asset CTA clicked. <!-- 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? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: added payment token preselection in mUSD conversion flow ## **Related issues** Fixes: [MUSD-108: Bring back mUSD confirmations enhancements](https://consensyssoftware.atlassian.net/browse/MUSD-108) ## **Manual testing steps** ```gherkin Feature: mUSD Conversion Scenario: Payment Token Preselection Given user has non-zero asset balance When user user clicks "Convert" CTA next to supported token (e.g. USDC) Then user is redirected to the mUSD conversion confirmation page with the correct asset preselected as the payment token. ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> Asset is not preselected ### **After** <!-- [screenshots/recordings] --> https://github.com/user-attachments/assets/fc056a92-2f26-425f-84aa-5e1623a37b88 ## **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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds preferred payment token selection to `useAutomaticTransactionPayToken` and wires mUSD conversion to preselect it; updates tests and docs. > > - **Payments/Confirmations**: > - **`useAutomaticTransactionPayToken`**: > - Adds `SetPayTokenRequest` and optional `preferredToken` param. > - Selection prefers `preferredToken` when available; ignores it for hardware wallets; otherwise falls back to first available or target required token. > - **Tests**: Extend to cover preferred token availability, hardware wallet fallback, empty tokens, and non-matching cases. > - **mUSD Conversion Flow**: > - **`musd-conversion-info` → `CustomAmountInfo`**: Pass `preferredPaymentToken` param down as `preferredToken` to the hook. > - **`useMusdConversion`**: JSDoc example updated; clarifies async gas estimation; navigates with `preferredPaymentToken` and `outputChainId`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0c3c367. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent f00dfb1 commit 4e88440

5 files changed

Lines changed: 154 additions & 17 deletions

File tree

app/components/UI/Earn/hooks/useMusdConversion.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,9 @@ export interface MusdConversionConfig {
4949
* await initiateConversion({
5050
* outputChainId: CHAIN_IDS.MAINNET,
5151
* preferredPaymentToken: {
52-
* address: USDC_ADDRESS_ARBITRUM,
52+
* address: USDC_ADDRESS_MAINNET,
5353
* chainId: CHAIN_IDS.MAINNET,
5454
* },
55-
* allowedPaymentTokens: musdConversionPaymentTokensAllowlist,
5655
* navigationStack: Routes.EARN.ROOT,
5756
* });
5857
*/
@@ -67,8 +66,8 @@ export const useMusdConversion = () => {
6766
const selectedAddress = selectedAccount?.address;
6867

6968
/**
70-
* Creates a placeholder transaction and navigating to confirmation.
71-
* Navigation happens immediately, then transaction creation happens in background.
69+
* Creates a placeholder transaction and navigates to confirmation.
70+
* Navigation happens immediately. Transaction creation and gas estimation happen asynchronously.
7271
*/
7372
const initiateConversion = useCallback(
7473
async (config: MusdConversionConfig): Promise<string> => {

app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import styleSheet from './custom-amount-info.styles';
1414
import { useTransactionCustomAmount } from '../../../hooks/transactions/useTransactionCustomAmount';
1515
import { useTransactionCustomAmountAlerts } from '../../../hooks/transactions/useTransactionCustomAmountAlerts';
1616
import useClearConfirmationOnBackSwipe from '../../../hooks/ui/useClearConfirmationOnBackSwipe';
17-
import { useAutomaticTransactionPayToken } from '../../../hooks/pay/useAutomaticTransactionPayToken';
17+
import {
18+
SetPayTokenRequest,
19+
useAutomaticTransactionPayToken,
20+
} from '../../../hooks/pay/useAutomaticTransactionPayToken';
1821
import { AlertMessage } from '../../alerts/alert-message';
1922
import {
2023
CustomAmount,
@@ -55,12 +58,16 @@ export interface CustomAmountInfoProps {
5558
currency?: string;
5659
disablePay?: boolean;
5760
hasMax?: boolean;
61+
preferredToken?: SetPayTokenRequest;
5862
}
5963

6064
export const CustomAmountInfo: React.FC<CustomAmountInfoProps> = memo(
61-
({ children, currency, disablePay, hasMax }) => {
65+
({ children, currency, disablePay, hasMax, preferredToken }) => {
6266
useClearConfirmationOnBackSwipe();
63-
useAutomaticTransactionPayToken({ disable: disablePay });
67+
useAutomaticTransactionPayToken({
68+
disable: disablePay,
69+
preferredToken,
70+
});
6471
useTransactionPayMetrics();
6572

6673
const { styles } = useStyles(styleSheet, {});

app/components/Views/confirmations/components/info/musd-conversion-info/musd-conversion-info.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ import {
88
} from '../../../../../UI/Earn/constants/musd';
99
import { useAddToken } from '../../../hooks/tokens/useAddToken';
1010
import { MusdConversionConfig } from '../../../../../UI/Earn/hooks/useMusdConversion';
11-
import { CHAIN_IDS } from '@metamask/transaction-controller';
1211
import { useParams } from '../../../../../../util/navigation/navUtils';
12+
import { CHAIN_IDS } from '@metamask/transaction-controller';
1313

1414
export const MusdConversionInfo = () => {
15-
// TEMP: Will be brought back in subsequent PR.
16-
// const preferredPaymentToken = route.params?.preferredPaymentToken;
17-
const { outputChainId } = useParams<MusdConversionConfig>({
18-
outputChainId: CHAIN_IDS.MAINNET,
19-
});
15+
const { outputChainId, preferredPaymentToken } =
16+
useParams<MusdConversionConfig>({
17+
outputChainId: CHAIN_IDS.MAINNET,
18+
});
2019

2120
useNavbar(strings('earn.musd_conversion.earn_rewards_with'));
2221

@@ -38,5 +37,5 @@ export const MusdConversionInfo = () => {
3837
tokenAddress: tokenToAddAddress,
3938
});
4039

41-
return <CustomAmountInfo />;
40+
return <CustomAmountInfo preferredToken={preferredPaymentToken} />;
4241
};

app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.test.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { merge } from 'lodash';
22
import { renderHookWithProvider } from '../../../../../util/test/renderWithProvider';
3-
import { useAutomaticTransactionPayToken } from './useAutomaticTransactionPayToken';
3+
import {
4+
useAutomaticTransactionPayToken,
5+
SetPayTokenRequest,
6+
} from './useAutomaticTransactionPayToken';
47
import { useTransactionPayToken } from './useTransactionPayToken';
58
import { simpleSendTransactionControllerMock } from '../../__mocks__/controllers/transaction-controller-mock';
69
import { transactionApprovalControllerMock } from '../../__mocks__/controllers/approval-controller-mock';
@@ -21,8 +24,11 @@ jest.mock('./useTransactionPayAvailableTokens');
2124
const TOKEN_ADDRESS_1_MOCK = '0x1234567890abcdef1234567890abcdef12345678';
2225
const TOKEN_ADDRESS_2_MOCK = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd';
2326
const TOKEN_ADDRESS_3_MOCK = '0xabc1234567890abcdef1234567890abcdef12345678';
27+
const PREFERRED_TOKEN_ADDRESS_MOCK =
28+
'0x9999999999999999999999999999999999999999';
2429
const CHAIN_ID_1_MOCK = '0x1';
2530
const CHAIN_ID_2_MOCK = '0x2';
31+
const PREFERRED_CHAIN_ID_MOCK = '0x3';
2632

2733
const STATE_MOCK = merge(
2834
{},
@@ -43,9 +49,15 @@ const STATE_MOCK = merge(
4349
},
4450
);
4551

46-
function runHook({ disable = false } = {}) {
52+
function runHook({
53+
disable = false,
54+
preferredToken,
55+
}: {
56+
disable?: boolean;
57+
preferredToken?: SetPayTokenRequest;
58+
} = {}) {
4759
return renderHookWithProvider(
48-
() => useAutomaticTransactionPayToken({ disable }),
60+
() => useAutomaticTransactionPayToken({ disable, preferredToken }),
4961
{
5062
state: STATE_MOCK,
5163
},
@@ -166,4 +178,101 @@ describe('useAutomaticTransactionPayToken', () => {
166178

167179
expect(setPayTokenMock).not.toHaveBeenCalled();
168180
});
181+
182+
it('selects preferred payment token when provided with available tokens', () => {
183+
useTransactionPayAvailableTokensMock.mockReturnValue([
184+
{
185+
address: TOKEN_ADDRESS_1_MOCK,
186+
chainId: CHAIN_ID_1_MOCK,
187+
},
188+
{
189+
address: PREFERRED_TOKEN_ADDRESS_MOCK,
190+
chainId: PREFERRED_CHAIN_ID_MOCK,
191+
},
192+
{
193+
address: TOKEN_ADDRESS_2_MOCK,
194+
chainId: CHAIN_ID_2_MOCK,
195+
},
196+
] as AssetType[]);
197+
198+
runHook({
199+
preferredToken: {
200+
address: PREFERRED_TOKEN_ADDRESS_MOCK as Hex,
201+
chainId: PREFERRED_CHAIN_ID_MOCK as Hex,
202+
},
203+
});
204+
205+
expect(setPayTokenMock).toHaveBeenCalledWith({
206+
address: PREFERRED_TOKEN_ADDRESS_MOCK,
207+
chainId: PREFERRED_CHAIN_ID_MOCK,
208+
});
209+
});
210+
211+
it('ignores preferred payment token when using hardware wallet', () => {
212+
useTransactionPayAvailableTokensMock.mockReturnValue([
213+
{
214+
address: TOKEN_ADDRESS_2_MOCK,
215+
chainId: CHAIN_ID_2_MOCK,
216+
},
217+
{
218+
address: TOKEN_ADDRESS_3_MOCK,
219+
chainId: CHAIN_ID_1_MOCK,
220+
},
221+
] as AssetType[]);
222+
223+
isHardwareAccountMock.mockReturnValue(true);
224+
225+
runHook({
226+
preferredToken: {
227+
address: PREFERRED_TOKEN_ADDRESS_MOCK as Hex,
228+
chainId: PREFERRED_CHAIN_ID_MOCK as Hex,
229+
},
230+
});
231+
232+
expect(setPayTokenMock).toHaveBeenCalledWith({
233+
address: TOKEN_ADDRESS_1_MOCK,
234+
chainId: CHAIN_ID_1_MOCK,
235+
});
236+
});
237+
238+
it('selects target token when preferred payment token provided but no tokens available', () => {
239+
useTransactionPayAvailableTokensMock.mockReturnValue([] as AssetType[]);
240+
241+
runHook({
242+
preferredToken: {
243+
address: PREFERRED_TOKEN_ADDRESS_MOCK as Hex,
244+
chainId: PREFERRED_CHAIN_ID_MOCK as Hex,
245+
},
246+
});
247+
248+
expect(setPayTokenMock).toHaveBeenCalledWith({
249+
address: TOKEN_ADDRESS_1_MOCK,
250+
chainId: CHAIN_ID_1_MOCK,
251+
});
252+
});
253+
254+
it('selects first available token when preferred token not in available tokens', () => {
255+
useTransactionPayAvailableTokensMock.mockReturnValue([
256+
{
257+
address: TOKEN_ADDRESS_1_MOCK,
258+
chainId: CHAIN_ID_1_MOCK,
259+
},
260+
{
261+
address: TOKEN_ADDRESS_2_MOCK,
262+
chainId: CHAIN_ID_2_MOCK,
263+
},
264+
] as AssetType[]);
265+
266+
runHook({
267+
preferredToken: {
268+
address: PREFERRED_TOKEN_ADDRESS_MOCK as Hex,
269+
chainId: PREFERRED_CHAIN_ID_MOCK as Hex,
270+
},
271+
});
272+
273+
expect(setPayTokenMock).toHaveBeenCalledWith({
274+
address: TOKEN_ADDRESS_1_MOCK,
275+
chainId: CHAIN_ID_1_MOCK,
276+
});
277+
});
169278
});

app/components/Views/confirmations/hooks/pay/useAutomaticTransactionPayToken.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,19 @@ import { useTransactionPayRequiredTokens } from './useTransactionPayData';
99
import { useTransactionPayAvailableTokens } from './useTransactionPayAvailableTokens';
1010
import { AssetType } from '../../types/token';
1111

12+
export interface SetPayTokenRequest {
13+
address: Hex;
14+
chainId: Hex;
15+
}
16+
1217
const log = createProjectLogger('transaction-pay');
1318

1419
export function useAutomaticTransactionPayToken({
1520
disable = false,
21+
preferredToken,
1622
}: {
1723
disable?: boolean;
24+
preferredToken?: SetPayTokenRequest;
1825
} = {}) {
1926
const isUpdated = useRef(false);
2027
const { setPayToken } = useTransactionPayToken();
@@ -52,6 +59,7 @@ export function useAutomaticTransactionPayToken({
5259
isHardwareWallet,
5360
targetToken,
5461
tokens: tokensWithBalance,
62+
preferredToken,
5563
});
5664

5765
if (!automaticToken) {
@@ -70,6 +78,7 @@ export function useAutomaticTransactionPayToken({
7078
}, [
7179
disable,
7280
isHardwareWallet,
81+
preferredToken,
7382
requiredTokens,
7483
setPayToken,
7584
targetToken,
@@ -79,10 +88,12 @@ export function useAutomaticTransactionPayToken({
7988

8089
function getBestToken({
8190
isHardwareWallet,
91+
preferredToken,
8292
targetToken,
8393
tokens,
8494
}: {
8595
isHardwareWallet: boolean;
96+
preferredToken?: SetPayTokenRequest;
8697
targetToken?: { address: Hex; chainId: Hex };
8798
tokens: AssetType[];
8899
}): { address: Hex; chainId: Hex } | undefined {
@@ -97,6 +108,18 @@ function getBestToken({
97108
return targetTokenFallback;
98109
}
99110

111+
if (preferredToken) {
112+
const preferredTokenAvailable = tokens.some(
113+
(token) =>
114+
token.address.toLowerCase() === preferredToken.address.toLowerCase() &&
115+
token.chainId?.toLowerCase() === preferredToken.chainId.toLowerCase(),
116+
);
117+
118+
if (preferredTokenAvailable) {
119+
return preferredToken;
120+
}
121+
}
122+
100123
if (tokens?.length) {
101124
return {
102125
address: tokens[0].address as Hex,

0 commit comments

Comments
 (0)