Skip to content

Commit d23e31c

Browse files
authored
feat: Add Perps Withdraw button into Developer Options, show new Perps Withdraw UI (#27792)
## **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? --> Adds the UI scaffolding for the Perps Withdraw confirmation flow. When triggered from Developer Options, the "Perps Withdraw" button navigates to a full-screen `CustomAmountInfo` confirmation page that displays the user's available Perps balance and uses the post-quote "Receive as" token picker pattern (same as Predict Withdraw). This is the UI-only foundation — the end-to-end withdrawal execution (HyperLiquid signatures, Relay integration) will follow in a subsequent PR. **Changes:** - Added "Perps Withdraw" button and `useAddPerpsTransactionBatch` hook in Developer Options - Created `PerpsWithdrawInfo` component that wires `CustomAmountInfo` with Perps balance display - Created `PerpsWithdrawBalance` component showing available Perps balance from live account data - Added `perpsWithdraw` to `FULL_SCREEN_CONFIRMATIONS` and `POST_QUOTE_TRANSACTION_TYPES` - Added `perpsWithdraw` routing in `info-root.tsx` - Updated `useButtonLabel` to show "Withdraw" for `perpsWithdraw` transactions - Added locale strings for Perps Withdraw title and available balance ## **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: Add Perps Withdraw button into Developer Options, show new Perps Withdraw UI ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/CONF-1072 ## **Manual testing steps** ```gherkin Feature: Perps Withdraw confirmation UI Background: Given I am logged into MetaMask Mobile And I have Developer Options enabled (export MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS="true" in .js.env) Scenario: user triggers Perps Withdraw from Developer Options Given I am on the Developer Options screen (Settings -> Developer options) And I scroll to the Perps Withdraw section When user taps the "Withdraw" button under "Perps Withdraw" Then the Perps Withdraw confirmation page should appear And the page title should show "Withdraw" And the available Perps balance should be displayed And the "Withdraw" action button should be visible at the bottom ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** N/A ### **After** <!-- [screenshots/recordings] --> ## **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. <!-- Generated with the help of the pr-description AI skill --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Adds a new `perpsWithdraw` transaction type path through the confirmations UI (routing, labels, constants) and a developer-only trigger that creates batched transactions; changes are mostly UI scaffolding but touch confirmation flow selection logic and transaction batching. > > **Overview** > Introduces a **Perps Withdraw** confirmation flow scaffold: a new Developer Options button triggers a `perpsWithdraw` transaction batch on Arbitrum USDC and navigates into the Perps confirmation stack. > > Adds a new `PerpsWithdrawInfo` full-screen `CustomAmountInfo` view that registers Arbitrum USDC, uses the post-quote *Receive as* pattern, and shows an *available Perps balance* via the new `PerpsWithdrawBalance` component. > > Updates confirmations plumbing to recognize `TransactionType.perpsWithdraw` (info routing, full-screen + post-quote type lists, withdraw button labeling, locales) and adds targeted unit tests/snapshots. Also bumps `@metamask/bridge-status-controller` and `@metamask/transaction-controller` dependency versions. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6d73fcc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: dan437 <80175477+dan437@users.noreply.github.com>
1 parent 4661cdb commit d23e31c

21 files changed

Lines changed: 642 additions & 69 deletions

File tree

app/components/Views/Settings/DeveloperOptions/__snapshots__/index.test.tsx.snap

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,76 @@ exports[`DeveloperOptions renders correctly 1`] = `
849849
Withdraw
850850
</Text>
851851
</TouchableOpacity>
852+
<Text
853+
accessibilityRole="text"
854+
style={
855+
{
856+
"color": "#131416",
857+
"fontFamily": "Geist-Bold",
858+
"fontSize": 24,
859+
"letterSpacing": 0,
860+
"lineHeight": 32,
861+
"marginTop": 16,
862+
}
863+
}
864+
>
865+
Perps Withdraw
866+
</Text>
867+
<Text
868+
accessibilityRole="text"
869+
style={
870+
{
871+
"color": "#66676a",
872+
"fontFamily": "Geist-Regular",
873+
"fontSize": 16,
874+
"letterSpacing": 0,
875+
"lineHeight": 24,
876+
"marginTop": 8,
877+
}
878+
}
879+
>
880+
Trigger a Perps withdraw confirmation.
881+
</Text>
882+
<TouchableOpacity
883+
accessibilityRole="button"
884+
accessible={true}
885+
activeOpacity={1}
886+
onPress={[Function]}
887+
onPressIn={[Function]}
888+
onPressOut={[Function]}
889+
style={
890+
{
891+
"alignItems": "center",
892+
"alignSelf": "stretch",
893+
"backgroundColor": "#b4b4b528",
894+
"borderColor": "transparent",
895+
"borderRadius": 12,
896+
"borderWidth": 1,
897+
"flexDirection": "row",
898+
"height": 48,
899+
"justifyContent": "center",
900+
"marginTop": 16,
901+
"overflow": "hidden",
902+
"paddingHorizontal": 16,
903+
}
904+
}
905+
testID="confirmations-developer-options-perps-withdraw-button"
906+
>
907+
<Text
908+
accessibilityRole="text"
909+
style={
910+
{
911+
"color": "#131416",
912+
"fontFamily": "Geist-Medium",
913+
"fontSize": 16,
914+
"letterSpacing": 0,
915+
"lineHeight": 24,
916+
}
917+
}
918+
>
919+
Withdraw
920+
</Text>
921+
</TouchableOpacity>
852922
<View
853923
style={
854924
[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { ORIGIN_METAMASK } from '@metamask/controller-utils';
2+
import { CHAIN_IDS, TransactionType } from '@metamask/transaction-controller';
3+
import { Hex } from '@metamask/utils';
4+
import { fireEvent, act, render } from '@testing-library/react-native';
5+
import React from 'react';
6+
import { useTheme } from '@react-navigation/native';
7+
import { useSelector } from 'react-redux';
8+
import { selectSelectedInternalAccountAddress } from '../../../../../../selectors/accountsController';
9+
import { selectDefaultEndpointByChainId } from '../../../../../../selectors/networkController';
10+
import { addTransactionBatch } from '../../../../../../util/transaction-controller';
11+
import { generateTransferData } from '../../../../../../util/transactions';
12+
import Routes from '../../../../../../constants/navigation/Routes';
13+
import { useStyles } from '../../../../../../component-library/hooks';
14+
import { useConfirmNavigation } from '../../../hooks/useConfirmNavigation';
15+
import { ConfirmationLoader } from '../../confirm/confirm-component';
16+
import { ConfirmationsDeveloperOptions } from './confirmations-developer-options';
17+
import { ConfirmationsDeveloperOptionsTestIds } from './confirmations-developer-options.testIds';
18+
19+
jest.mock('react-redux', () => ({
20+
...jest.requireActual('react-redux'),
21+
useSelector: jest.fn(),
22+
}));
23+
24+
jest.mock('@react-navigation/native', () => ({
25+
...jest.requireActual('@react-navigation/native'),
26+
useTheme: jest.fn(),
27+
}));
28+
29+
jest.mock('../../../../../../component-library/hooks', () => ({
30+
useStyles: jest.fn(),
31+
}));
32+
33+
jest.mock('../../../../../../selectors/accountsController', () => ({
34+
...jest.requireActual('../../../../../../selectors/accountsController'),
35+
selectSelectedInternalAccountAddress: jest.fn(),
36+
}));
37+
38+
jest.mock('../../../../../../selectors/networkController', () => ({
39+
...jest.requireActual('../../../../../../selectors/networkController'),
40+
selectDefaultEndpointByChainId: jest.fn(),
41+
}));
42+
43+
jest.mock('../../../../../../util/transaction-controller', () => ({
44+
addTransactionBatch: jest.fn(),
45+
}));
46+
47+
jest.mock('../../../../../../util/transactions', () => ({
48+
generateTransferData: jest.fn(),
49+
}));
50+
51+
jest.mock('../../../hooks/useConfirmNavigation', () => ({
52+
useConfirmNavigation: jest.fn(),
53+
}));
54+
55+
const MOCK_ACCOUNT = '0x1234567890123456789012345678901234567890' as Hex;
56+
const MOCK_TRANSFER_DATA = '0xabcdef' as Hex;
57+
const MOCK_ARBITRUM_USDC = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' as Hex;
58+
const MOCK_NETWORK_CLIENT_ID = 'arbitrum-mainnet';
59+
const mockNavigateToConfirmation = jest.fn();
60+
61+
describe('ConfirmationsDeveloperOptions', () => {
62+
const mockUseSelector = jest.mocked(useSelector);
63+
const mockUseTheme = jest.mocked(useTheme);
64+
const mockUseStyles = jest.mocked(useStyles);
65+
const mockSelectSelectedInternalAccountAddress = jest.mocked(
66+
selectSelectedInternalAccountAddress,
67+
);
68+
const mockSelectDefaultEndpointByChainId = jest.mocked(
69+
selectDefaultEndpointByChainId,
70+
);
71+
const mockAddTransactionBatch = jest.mocked(addTransactionBatch);
72+
const mockGenerateTransferData = jest.mocked(generateTransferData);
73+
const mockUseConfirmNavigation = jest.mocked(useConfirmNavigation);
74+
75+
beforeEach(() => {
76+
jest.clearAllMocks();
77+
78+
mockUseTheme.mockReturnValue({
79+
colors: {},
80+
} as never);
81+
82+
mockUseStyles.mockReturnValue({
83+
styles: {
84+
accessory: {},
85+
desc: {},
86+
heading: {},
87+
},
88+
} as never);
89+
90+
mockSelectSelectedInternalAccountAddress.mockReturnValue(MOCK_ACCOUNT);
91+
mockSelectDefaultEndpointByChainId.mockReturnValue({
92+
networkClientId: MOCK_NETWORK_CLIENT_ID,
93+
} as never);
94+
mockGenerateTransferData.mockReturnValue(MOCK_TRANSFER_DATA);
95+
mockAddTransactionBatch.mockResolvedValue(undefined as never);
96+
mockUseConfirmNavigation.mockReturnValue({
97+
navigateToConfirmation: mockNavigateToConfirmation,
98+
} as never);
99+
mockUseSelector.mockImplementation(((
100+
selector: (state: object) => unknown,
101+
) => selector({})) as typeof useSelector);
102+
});
103+
104+
afterEach(() => {
105+
jest.resetAllMocks();
106+
});
107+
108+
it('renders the Perps Withdraw developer option', () => {
109+
const { getByText, getByTestId } = render(
110+
<ConfirmationsDeveloperOptions />,
111+
);
112+
113+
expect(getByText('Perps Withdraw')).toBeOnTheScreen();
114+
expect(
115+
getByText('Trigger a Perps withdraw confirmation.'),
116+
).toBeOnTheScreen();
117+
expect(
118+
getByTestId(ConfirmationsDeveloperOptionsTestIds.PERPS_WITHDRAW_BUTTON),
119+
).toBeOnTheScreen();
120+
});
121+
122+
it('navigates to the Perps confirmation flow when the Perps Withdraw button is pressed', async () => {
123+
const { getByTestId } = render(<ConfirmationsDeveloperOptions />);
124+
125+
await act(async () => {
126+
fireEvent.press(
127+
getByTestId(ConfirmationsDeveloperOptionsTestIds.PERPS_WITHDRAW_BUTTON),
128+
);
129+
});
130+
131+
expect(mockGenerateTransferData).toHaveBeenCalledWith('transfer', {
132+
toAddress: MOCK_ARBITRUM_USDC,
133+
amount: '0x0',
134+
});
135+
expect(mockNavigateToConfirmation).toHaveBeenCalledWith({
136+
loader: ConfirmationLoader.CustomAmount,
137+
stack: Routes.PERPS.ROOT,
138+
});
139+
expect(mockSelectDefaultEndpointByChainId).toHaveBeenCalledWith(
140+
{},
141+
CHAIN_IDS.ARBITRUM,
142+
);
143+
expect(mockAddTransactionBatch).toHaveBeenCalledWith({
144+
from: MOCK_ACCOUNT,
145+
origin: ORIGIN_METAMASK,
146+
networkClientId: MOCK_NETWORK_CLIENT_ID,
147+
disableHook: true,
148+
disableSequential: true,
149+
transactions: [
150+
{
151+
params: {
152+
to: MOCK_ARBITRUM_USDC,
153+
data: MOCK_TRANSFER_DATA,
154+
},
155+
type: TransactionType.perpsWithdraw,
156+
},
157+
],
158+
});
159+
});
160+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const ConfirmationsDeveloperOptionsTestIds = {
2+
PERPS_WITHDRAW_BUTTON:
3+
'confirmations-developer-options-perps-withdraw-button',
4+
} as const;

app/components/Views/confirmations/components/developer/confirmations-developer-options/confirmations-developer-options.tsx

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { generateTransferData } from '../../../../../../util/transactions';
2323
import { useConfirmNavigation } from '../../../hooks/useConfirmNavigation';
2424
import { selectSelectedInternalAccountAddress } from '../../../../../../selectors/accountsController';
2525
import { RootState } from '../../../../../../reducers';
26+
import { ConfirmationsDeveloperOptionsTestIds } from './confirmations-developer-options.testIds';
27+
import { ARBITRUM_USDC } from '../../../constants/perps';
2628

2729
const POLYGON_USDCE_ADDRESS =
2830
'0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' as Hex;
@@ -36,10 +38,32 @@ export function ConfirmationsDeveloperOptions() {
3638
<PredictDeposit />
3739
<PredictClaim />
3840
<PredictWithdraw />
41+
<PerpsWithdraw />
3942
</>
4043
);
4144
}
4245

46+
function PerpsWithdraw() {
47+
const { addTransactionBatchAndNavigate } = useAddPerpsTransactionBatch();
48+
49+
const handleWithdraw = useCallback(() => {
50+
addTransactionBatchAndNavigate({
51+
loader: ConfirmationLoader.CustomAmount,
52+
transactionType: TransactionType.perpsWithdraw,
53+
});
54+
}, [addTransactionBatchAndNavigate]);
55+
56+
return (
57+
<DeveloperButton
58+
title="Perps Withdraw"
59+
description="Trigger a Perps withdraw confirmation."
60+
buttonLabel="Withdraw"
61+
onPress={handleWithdraw}
62+
testID={ConfirmationsDeveloperOptionsTestIds.PERPS_WITHDRAW_BUTTON}
63+
/>
64+
);
65+
}
66+
4367
function PredictWithdraw() {
4468
const { addTransactionBatchAndNavigate } = useAddTransactionBatch();
4569

@@ -164,15 +188,71 @@ function useAddTransactionBatch() {
164188
};
165189
}
166190

191+
function useAddPerpsTransactionBatch() {
192+
const selectedAccount = useSelector(selectSelectedInternalAccountAddress);
193+
const { navigateToConfirmation } = useConfirmNavigation();
194+
195+
const { networkClientId } =
196+
useSelector((state: RootState) =>
197+
selectDefaultEndpointByChainId(state, CHAIN_IDS.ARBITRUM),
198+
) ?? {};
199+
200+
const transferData = generateTransferData('transfer', {
201+
toAddress: ARBITRUM_USDC.address,
202+
amount: '0x0',
203+
}) as Hex;
204+
205+
const addTransactionBatchAndNavigate = useCallback(
206+
async ({
207+
loader,
208+
transactionType,
209+
}: {
210+
loader?: ConfirmationLoader;
211+
transactionType: TransactionType;
212+
}) => {
213+
navigateToConfirmation({
214+
loader,
215+
stack: Routes.PERPS.ROOT,
216+
});
217+
218+
addTransactionBatch({
219+
from: selectedAccount as Hex,
220+
origin: ORIGIN_METAMASK,
221+
networkClientId,
222+
disableHook: true,
223+
disableSequential: true,
224+
transactions: [
225+
{
226+
params: {
227+
to: ARBITRUM_USDC.address,
228+
data: transferData,
229+
},
230+
type: transactionType,
231+
},
232+
],
233+
}).catch((e) => {
234+
console.error('Perps transaction error', e);
235+
});
236+
},
237+
[navigateToConfirmation, networkClientId, selectedAccount, transferData],
238+
);
239+
240+
return {
241+
addTransactionBatchAndNavigate,
242+
};
243+
}
244+
167245
function DeveloperButton({
168246
buttonLabel,
169247
description,
170248
onPress,
249+
testID,
171250
title,
172251
}: {
173252
buttonLabel: string;
174253
description: string;
175254
onPress: () => void;
255+
testID?: string;
176256
title: string;
177257
}) {
178258
const theme = useTheme();
@@ -199,6 +279,7 @@ function DeveloperButton({
199279
size={ButtonSize.Lg}
200280
label={buttonLabel}
201281
onPress={onPress}
282+
testID={testID}
202283
width={ButtonWidthTypes.Full}
203284
style={styles.accessory}
204285
/>

0 commit comments

Comments
 (0)