Skip to content

Commit c64b1e4

Browse files
gambinishabretonc7scortisikotommasinimetamaskbot
authored
feat: Hip3 Perps Home Screen (#21862)
## **Description** Introduces new home screen + Hip-3 support UI. Initially broken out from #21410 to make it easier to iterate on, and isolate codeowners. More complete technical detail in original ticket. This includes the home page redesign with hip-3 support, along with [after hours UX](#21901). Figma designs: https://www.figma.com/design/kdRiGFFmRrkbCJyKf4c4Re/Equity-perps-and-more?node-id=153-3116&p=f&t=AS2BitV7DlFSsEWX-0 ## **Changelog** CHANGELOG entry: Introduce new Perps Home screen UI ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** https://github.com/user-attachments/assets/db35b6dd-ba76-4b95-bca1-f043bc71e27e ## **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] > Implements a new Perps Home UI with search/sections and bulk actions, overhauls Market List with tabs/sorting/filtering, adds HIP-3 2× fee logic and market-hours UX, refactors close flows and hooks, and updates routes, tests, and i18n. > > - **UI/Views**: > - New `PerpsHomeView` with positions/orders carousels, watchlist, navigation card, search bar, and bottom sheets for bulk actions. > - Added `PerpsCloseSummary` shared component for close flows; integrated into Close Position and Close All modals. > - Added bulk action modals: `PerpsCloseAllPositionsModal` and `PerpsCancelAllOrdersModal`. > - **Market List Overhaul**: > - New components: `PerpsMarketList`, `PerpsMarketListHeader`, `PerpsMarketFiltersBar`, `PerpsMarketTypeSection`, `PerpsStocksCommoditiesDropdown/BottomSheet`. > - Tabs (All/Crypto/Stocks & Commodities), favorites filter, search, and sort presets (volume, price change, funding, OI). > - Filters zero-volume/fallback markets by default; optional inclusion via flag. > - **Fees/Providers**: > - Add HIP-3 fee multiplier (2× base protocol rate) with `coin` propagated through fee calculations. > - Updated `HyperLiquidProvider` and controller/types; extended tests. > - **Market Hours**: > - `PerpsMarketHoursBanner` and tooltip with `marketHours` utils; integrated into Market Details. > - **Hooks**: > - New hooks: `usePerpsHomeData`, `usePerpsMarketListView`, `usePerpsSearch`, `usePerpsSorting`, `usePerpsCloseAllCalculations`, `usePerpsCancelAllOrders`, `usePerpsCloseAllPositions`. > - `usePerpsOrderForm`: prioritize leverage (route > existing position > saved config > default) and sync on position load. > - `usePerpsLiveFills` now returns `{fills,isInitialLoading}`. > - **Navigation/Routes**: > - Home view wired as main Perps screen; added modal routes for bulk actions. > - **Misc**: > - Numerous tests for new components/hooks; style and i18n updates (home labels, market-hours strings). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5e56909. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Arthur Breton <arthur.breton@consensys.net> Co-authored-by: abretonc7s <107169956+abretonc7s@users.noreply.github.com> Co-authored-by: Curtis David <Curtis.David7@gmail.com> Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com> Co-authored-by: metamaskbot <metamaskbot@users.noreply.github.com> Co-authored-by: Borislav Grigorov <b.s.grigorov@gmail.com> Co-authored-by: Nicholas Smith <nick.smith@consensys.net>
1 parent 91c23b3 commit c64b1e4

File tree

140 files changed

+16531
-2073
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

140 files changed

+16531
-2073
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { StyleSheet } from 'react-native';
2+
import type { Theme } from '../../../../../util/theme/models';
3+
4+
export const createStyles = (_theme: Theme) =>
5+
StyleSheet.create({
6+
contentContainer: {
7+
paddingHorizontal: 16,
8+
paddingVertical: 16,
9+
},
10+
loadingContainer: {
11+
paddingVertical: 32,
12+
alignItems: 'center',
13+
justifyContent: 'center',
14+
},
15+
loadingText: {
16+
marginTop: 12,
17+
},
18+
emptyContainer: {
19+
paddingVertical: 32,
20+
paddingHorizontal: 16,
21+
alignItems: 'center',
22+
justifyContent: 'center',
23+
},
24+
footerContainer: {
25+
paddingHorizontal: 16,
26+
},
27+
});
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import PerpsCancelAllOrdersView from './PerpsCancelAllOrdersView';
4+
import { usePerpsCancelAllOrders, usePerpsLiveOrders } from '../../hooks';
5+
6+
// Mock all dependencies
7+
jest.mock('@react-navigation/native', () => ({
8+
useNavigation: jest.fn(() => ({ navigate: jest.fn(), goBack: jest.fn() })),
9+
}));
10+
11+
jest.mock('../../../../../../locales/i18n', () => ({
12+
strings: jest.fn((key: string) => key),
13+
}));
14+
15+
jest.mock('../../hooks', () => ({
16+
usePerpsLiveOrders: jest.fn(),
17+
usePerpsCancelAllOrders: jest.fn(),
18+
}));
19+
20+
jest.mock('../../hooks/usePerpsToasts', () => ({
21+
__esModule: true,
22+
default: jest.fn(() => ({ showToast: jest.fn() })),
23+
}));
24+
25+
jest.mock('../../../../../util/theme', () => ({
26+
useTheme: jest.fn(() => ({
27+
colors: {
28+
accent03: { normal: '#00ff00', dark: '#008800' },
29+
accent01: { light: '#ffcccc', dark: '#cc0000' },
30+
primary: { default: '#0000ff' },
31+
background: { default: '#ffffff' },
32+
},
33+
})),
34+
}));
35+
36+
jest.mock('../../hooks/usePerpsEventTracking', () => ({
37+
usePerpsEventTracking: jest.fn(),
38+
}));
39+
40+
jest.mock(
41+
'../../../../../component-library/components/BottomSheets/BottomSheet',
42+
() => {
43+
const mockReact = jest.requireActual<typeof React>('react');
44+
return mockReact.forwardRef(
45+
(props: { children: React.ReactNode }, _ref) => <>{props.children}</>,
46+
);
47+
},
48+
);
49+
50+
jest.mock(
51+
'../../../../../component-library/components/BottomSheets/BottomSheetHeader',
52+
() => 'BottomSheetHeader',
53+
);
54+
55+
jest.mock(
56+
'../../../../../component-library/components/BottomSheets/BottomSheetFooter',
57+
() => {
58+
const { View, TouchableOpacity, Text } = jest.requireActual('react-native');
59+
60+
return {
61+
__esModule: true,
62+
default: ({
63+
buttonPropsArray,
64+
}: {
65+
buttonPropsArray?: {
66+
label: string;
67+
onPress: () => void;
68+
disabled?: boolean;
69+
}[];
70+
}) => (
71+
<View>
72+
{buttonPropsArray?.map((buttonProps, index) => (
73+
<TouchableOpacity
74+
key={index}
75+
onPress={buttonProps.onPress}
76+
disabled={buttonProps.disabled}
77+
>
78+
<Text>{buttonProps.label}</Text>
79+
</TouchableOpacity>
80+
))}
81+
</View>
82+
),
83+
ButtonsAlignment: {
84+
Horizontal: 'Horizontal',
85+
Vertical: 'Vertical',
86+
},
87+
};
88+
},
89+
);
90+
91+
const mockUsePerpsLiveOrders = usePerpsLiveOrders as jest.MockedFunction<
92+
typeof usePerpsLiveOrders
93+
>;
94+
const mockUsePerpsCancelAllOrders =
95+
usePerpsCancelAllOrders as jest.MockedFunction<
96+
typeof usePerpsCancelAllOrders
97+
>;
98+
99+
describe('PerpsCancelAllOrdersView', () => {
100+
const mockOrders = [
101+
{
102+
orderId: 'order-1',
103+
symbol: 'BTC',
104+
side: 'buy' as const,
105+
orderType: 'limit' as const,
106+
size: '0.1',
107+
originalSize: '0.1',
108+
price: '50000',
109+
filledSize: '0',
110+
remainingSize: '0.1',
111+
status: 'open' as const,
112+
timestamp: Date.now(),
113+
},
114+
{
115+
orderId: 'order-2',
116+
symbol: 'ETH',
117+
side: 'sell' as const,
118+
orderType: 'limit' as const,
119+
size: '1.0',
120+
originalSize: '1.0',
121+
price: '3000',
122+
filledSize: '0',
123+
remainingSize: '1.0',
124+
status: 'open' as const,
125+
timestamp: Date.now(),
126+
},
127+
];
128+
129+
const mockCancelAllHook = {
130+
isCanceling: false,
131+
orderCount: 2,
132+
handleCancelAll: jest.fn(),
133+
handleKeepOrders: jest.fn(),
134+
error: null,
135+
};
136+
137+
beforeEach(() => {
138+
jest.clearAllMocks();
139+
mockUsePerpsLiveOrders.mockReturnValue({
140+
orders: mockOrders,
141+
isInitialLoading: false,
142+
});
143+
mockUsePerpsCancelAllOrders.mockReturnValue(mockCancelAllHook);
144+
});
145+
146+
it('renders cancel all orders view with orders', () => {
147+
// Arrange & Act
148+
const { getByText } = render(<PerpsCancelAllOrdersView />);
149+
150+
// Assert
151+
expect(getByText('perps.cancel_all_modal.title')).toBeTruthy();
152+
expect(getByText('perps.cancel_all_modal.description')).toBeTruthy();
153+
});
154+
155+
it('renders empty state when no orders', () => {
156+
// Arrange
157+
mockUsePerpsLiveOrders.mockReturnValue({
158+
orders: [],
159+
isInitialLoading: false,
160+
});
161+
mockUsePerpsCancelAllOrders.mockReturnValue({
162+
...mockCancelAllHook,
163+
orderCount: 0,
164+
});
165+
166+
// Act
167+
const { getByText } = render(<PerpsCancelAllOrdersView />);
168+
169+
// Assert
170+
expect(getByText('perps.order.no_orders')).toBeTruthy();
171+
});
172+
173+
it('renders loading state when canceling', () => {
174+
// Arrange
175+
mockUsePerpsCancelAllOrders.mockReturnValue({
176+
...mockCancelAllHook,
177+
isCanceling: true,
178+
});
179+
180+
// Act
181+
const { getAllByText } = render(<PerpsCancelAllOrdersView />);
182+
183+
// Assert
184+
const cancelingElements = getAllByText('perps.cancel_all_modal.canceling');
185+
expect(cancelingElements.length).toBeGreaterThan(0);
186+
});
187+
188+
it('displays footer buttons with correct labels', () => {
189+
// Arrange & Act
190+
const { getByText } = render(<PerpsCancelAllOrdersView />);
191+
192+
// Assert
193+
expect(getByText('perps.cancel_all_modal.keep_orders')).toBeTruthy();
194+
expect(getByText('perps.cancel_all_modal.confirm')).toBeTruthy();
195+
});
196+
197+
it('shows canceling label on confirm button when in progress', () => {
198+
// Arrange
199+
mockUsePerpsCancelAllOrders.mockReturnValue({
200+
...mockCancelAllHook,
201+
isCanceling: true,
202+
});
203+
204+
// Act
205+
const { getAllByText } = render(<PerpsCancelAllOrdersView />);
206+
207+
// Assert
208+
const cancelingElements = getAllByText('perps.cancel_all_modal.canceling');
209+
expect(cancelingElements.length).toBeGreaterThan(0);
210+
});
211+
212+
it('renders with empty orders gracefully', () => {
213+
// Arrange
214+
mockUsePerpsLiveOrders.mockReturnValue({
215+
orders: [],
216+
isInitialLoading: false,
217+
});
218+
mockUsePerpsCancelAllOrders.mockReturnValue({
219+
...mockCancelAllHook,
220+
orderCount: 0,
221+
});
222+
223+
// Act
224+
const { getByText } = render(<PerpsCancelAllOrdersView />);
225+
226+
// Assert
227+
expect(getByText('perps.order.no_orders')).toBeTruthy();
228+
});
229+
});

0 commit comments

Comments
 (0)