From ba74b18f114d2285789e2b29d69315c47779501f Mon Sep 17 00:00:00 2001 From: geositta Date: Mon, 20 Jul 2026 20:17:21 -0500 Subject: [PATCH 1/7] feat(perps): scaffold pro mode market layout (PerpsProMarketView) --- app/components/UI/Perps/Perps.testIds.ts | 16 ++++++++++++++++ app/components/UI/Perps/routes/index.tsx | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/components/UI/Perps/Perps.testIds.ts b/app/components/UI/Perps/Perps.testIds.ts index ccd6d5fefcde..af4b781685bf 100644 --- a/app/components/UI/Perps/Perps.testIds.ts +++ b/app/components/UI/Perps/Perps.testIds.ts @@ -456,6 +456,22 @@ export const PerpsMarketDetailsViewSelectorsIDs = { 'perps-market-details-service-interruption-banner', }; +// ======================================== +// PERPS PRO MARKET VIEW SELECTORS +// ======================================== + +export const PerpsProMarketViewSelectorsIDs = { + CONTAINER: 'perps-pro-market-view', + SCROLL_VIEW: 'perps-pro-market-scroll-view', + HEADER: 'perps-pro-market-header', + CHART_PANEL: 'perps-pro-market-chart-panel', + STATS_BAR: 'perps-pro-market-stats-bar', + LAYOUT: 'perps-pro-market-layout', + ORDER_FORM_PANEL: 'perps-pro-market-order-form-panel', + ORDER_BOOK_PANEL: 'perps-pro-market-order-book-panel', + POSITIONS_PANEL: 'perps-pro-market-positions-panel', +}; + // ======================================== // PERPS MARKET HEADER SELECTORS // ======================================== diff --git a/app/components/UI/Perps/routes/index.tsx b/app/components/UI/Perps/routes/index.tsx index 7a1b6160b114..64f49013bf86 100644 --- a/app/components/UI/Perps/routes/index.tsx +++ b/app/components/UI/Perps/routes/index.tsx @@ -19,7 +19,7 @@ import { PerpsConnectionProvider } from '../providers/PerpsConnectionProvider'; import { PerpsGlobalErrorGate } from '../components/PerpsGlobalErrorGate'; import { PerpsStreamProvider } from '../providers/PerpsStreamManager'; import PerpsHomeView from '../Views/PerpsHomeView/PerpsHomeView'; -import PerpsMarketDetailsView from '../Views/PerpsMarketDetailsView'; +import PerpsMarketDetailsRouter from '../Views/PerpsMarketDetailsRouter'; import PerpsMarketListView from '../Views/PerpsMarketListView'; import PerpsRedirect from '../Views/PerpsRedirect'; import PerpsOrderRedirect from '../Views/PerpsOrderRedirect'; @@ -300,7 +300,7 @@ const PerpsScreenStack = () => { Date: Mon, 20 Jul 2026 20:35:26 -0500 Subject: [PATCH 2/7] chore: add files --- .../PerpsMarketDetailsRouter.test.tsx | 48 ++++++++++++++ .../PerpsMarketDetailsRouter.tsx | 20 ++++++ .../Views/PerpsMarketDetailsRouter/index.ts | 2 + .../usePerpsProModeEnabled.test.ts | 10 +++ .../usePerpsProModeEnabled.ts | 13 ++++ .../PerpsProMarketView.styles.ts | 16 +++++ .../PerpsProMarketView.test.tsx | 59 +++++++++++++++++ .../PerpsProMarketView/PerpsProMarketView.tsx | 63 +++++++++++++++++++ .../PerpsProMarketView.types.ts | 26 ++++++++ .../components/PerpsProChartPanel.tsx | 27 ++++++++ .../components/PerpsProMarketHeader.tsx | 40 ++++++++++++ .../components/PerpsProMarketLayout.tsx | 58 +++++++++++++++++ .../components/PerpsProOrderBookPanel.tsx | 28 +++++++++ .../components/PerpsProOrderFormPanel.tsx | 27 ++++++++ .../components/PerpsProPositionsPanel.tsx | 27 ++++++++ .../components/PerpsProStatsBar.tsx | 33 ++++++++++ .../Perps/Views/PerpsProMarketView/index.ts | 2 + 17 files changed, 499 insertions(+) create mode 100644 app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx create mode 100644 app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.tsx create mode 100644 app/components/UI/Perps/Views/PerpsMarketDetailsRouter/index.ts create mode 100644 app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.test.ts create mode 100644 app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.styles.ts create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProPositionsPanel.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProStatsBar.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/index.ts diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx new file mode 100644 index 000000000000..c06a23e90b2f --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import PerpsMarketDetailsRouter from './PerpsMarketDetailsRouter'; +import { usePerpsProModeEnabled } from './usePerpsProModeEnabled'; + +jest.mock('./usePerpsProModeEnabled'); + +jest.mock('../PerpsProMarketView', () => { + const { View } = jest.requireActual('react-native'); + return { + __esModule: true, + default: () => , + }; +}); + +jest.mock('../PerpsMarketDetailsView', () => { + const { View } = jest.requireActual('react-native'); + return { + __esModule: true, + default: () => , + }; +}); + +const mockUsePerpsProModeEnabled = jest.mocked(usePerpsProModeEnabled); + +describe('PerpsMarketDetailsRouter', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders PerpsProMarketView when Pro mode is enabled', () => { + mockUsePerpsProModeEnabled.mockReturnValue(true); + + const { getByTestId, queryByTestId } = render(); + + expect(getByTestId('mock-pro-market-view')).toBeTruthy(); + expect(queryByTestId('mock-lite-market-details-view')).toBeNull(); + }); + + it('renders PerpsMarketDetailsView when Pro mode is disabled', () => { + mockUsePerpsProModeEnabled.mockReturnValue(false); + + const { getByTestId, queryByTestId } = render(); + + expect(getByTestId('mock-lite-market-details-view')).toBeTruthy(); + expect(queryByTestId('mock-pro-market-view')).toBeNull(); + }); +}); diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.tsx new file mode 100644 index 000000000000..999a1e21ddd3 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import PerpsMarketDetailsView from '../PerpsMarketDetailsView'; +import PerpsProMarketView from '../PerpsProMarketView'; +import { usePerpsProModeEnabled } from './usePerpsProModeEnabled'; + +/** + * Route component registered for `Routes.PERPS.MARKET_DETAILS`. + * + * Renders the Pro-mode market layout (`PerpsProMarketView`) when Pro mode is + * enabled, otherwise the lite `PerpsMarketDetailsView`. The route name and + * navigation params are identical for both modes; only the rendered component + * differs, so market-list navigation is unaffected. + */ +const PerpsMarketDetailsRouter: React.FC = () => { + const isProModeEnabled = usePerpsProModeEnabled(); + + return isProModeEnabled ? : ; +}; + +export default PerpsMarketDetailsRouter; diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/index.ts b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/index.ts new file mode 100644 index 000000000000..83afde2a77d1 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/index.ts @@ -0,0 +1,2 @@ +export { default } from './PerpsMarketDetailsRouter'; +export { usePerpsProModeEnabled } from './usePerpsProModeEnabled'; diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.test.ts b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.test.ts new file mode 100644 index 000000000000..3a884177527d --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.test.ts @@ -0,0 +1,10 @@ +import { renderHook } from '@testing-library/react-native'; +import { usePerpsProModeEnabled } from './usePerpsProModeEnabled'; + +describe('usePerpsProModeEnabled', () => { + it('returns false by default while Pro mode is not yet reachable (TAT-3551)', () => { + const { result } = renderHook(() => usePerpsProModeEnabled()); + + expect(result.current).toBe(false); + }); +}); diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts new file mode 100644 index 000000000000..19518991126d --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts @@ -0,0 +1,13 @@ +/** + * Temporary gate for the Perps Pro-mode layout. + * + * TODO(TAT-3551): replace this placeholder with the real mobile selector + * wrapper around `selectPerpsMode` from `@metamask/perps-controller` (via + * `app/components/UI/Perps/selectors/perpsController`) once that package is + * bumped to expose the `mode` field. Until then this always returns `false`, + * so every user keeps the lite `PerpsMarketDetailsView` and the Pro layout is + * not user-reachable. + * + * @returns Whether the Perps Pro-mode layout is enabled for the current user. + */ +export const usePerpsProModeEnabled = (): boolean => false; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.styles.ts b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.styles.ts new file mode 100644 index 000000000000..32143248b45c --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.styles.ts @@ -0,0 +1,16 @@ +import { StyleSheet } from 'react-native'; +import type { Theme } from '../../../../../util/theme/models'; + +export const createStyles = ({ theme }: { theme: Theme }) => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background.default, + }, + scrollView: { + flex: 1, + }, + scrollContent: { + paddingBottom: 16, + }, + }); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx new file mode 100644 index 000000000000..bbe12d39e0d1 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PerpsProMarketView from './'; +import renderWithProvider from '../../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../../util/test/initial-root-state'; +import { PerpsProMarketViewSelectorsIDs } from '../../Perps.testIds'; + +const mockRouteParams: { market?: { symbol: string } } = { + market: { symbol: 'BTC' }, +}; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useRoute: () => ({ params: mockRouteParams }), + }; +}); + +const renderView = () => + renderWithProvider(, { + state: { engine: { backgroundState } }, + }); + +describe('PerpsProMarketView', () => { + it('renders the container and scroll view', () => { + const { getByTestId } = renderView(); + + expect(getByTestId(PerpsProMarketViewSelectorsIDs.CONTAINER)).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.SCROLL_VIEW), + ).toBeTruthy(); + }); + + it('renders every layout panel placeholder', () => { + const { getByTestId } = renderView(); + + expect(getByTestId(PerpsProMarketViewSelectorsIDs.HEADER)).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.CHART_PANEL), + ).toBeTruthy(); + expect(getByTestId(PerpsProMarketViewSelectorsIDs.STATS_BAR)).toBeTruthy(); + expect(getByTestId(PerpsProMarketViewSelectorsIDs.LAYOUT)).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.ORDER_FORM_PANEL), + ).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.ORDER_BOOK_PANEL), + ).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.POSITIONS_PANEL), + ).toBeTruthy(); + }); + + it('shows the asset symbol from route params in the header', () => { + const { getByText } = renderView(); + + expect(getByText('BTC')).toBeTruthy(); + }); +}); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx new file mode 100644 index 000000000000..e3984687670e --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx @@ -0,0 +1,63 @@ +import { SectionDivider } from '@metamask/design-system-react-native'; +import type { PerpsMarketData } from '@metamask/perps-controller'; +import { useRoute, type RouteProp } from '@react-navigation/native'; +import React from 'react'; +import { ScrollView } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useStyles } from '../../../../../component-library/hooks'; +import { PerpsProMarketViewSelectorsIDs } from '../../Perps.testIds'; +import PerpsProChartPanel from './components/PerpsProChartPanel'; +import PerpsProMarketHeader from './components/PerpsProMarketHeader'; +import PerpsProMarketLayout from './components/PerpsProMarketLayout'; +import PerpsProOrderBookPanel from './components/PerpsProOrderBookPanel'; +import PerpsProOrderFormPanel from './components/PerpsProOrderFormPanel'; +import PerpsProPositionsPanel from './components/PerpsProPositionsPanel'; +import PerpsProStatsBar from './components/PerpsProStatsBar'; +import { createStyles } from './PerpsProMarketView.styles'; +import { DEFAULT_PRO_LAYOUT_CONFIG } from './PerpsProMarketView.types'; + +interface PerpsProMarketRouteParams { + market?: PerpsMarketData; +} + +/** + * Pro-mode replacement for `PerpsMarketDetailsView`. + * + * Scaffold only: lays out the full Pro trading screen (header, chart, stats + * bar, two-column order form / order book, and positions/orders section) as + * placeholder containers matching Figma node 10041:12979, so later tickets + * (TAT-3555/TAT-3556/TAT-3551) can drop real panels into existing slots. + */ +const PerpsProMarketView = () => { + const { styles } = useStyles(createStyles, {}); + const route = + useRoute>(); + const symbol = route.params?.market?.symbol; + + return ( + + + + + + } + orderBook={} + /> + + + + + ); +}; + +export default PerpsProMarketView; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts new file mode 100644 index 000000000000..309d047a0ab3 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts @@ -0,0 +1,26 @@ +/** + * Which column a Pro-mode panel occupies in the two-column trading area. + */ +export type PerpsPanelPosition = 'left' | 'right'; + +/** + * Placement config for the Pro-mode two-column trading area. + * + * Currently a static default (order form left, order book right). Once the + * `@metamask/perps-controller` package is bumped, this will be fed by + * `proLayoutPreferences.orderFormPosition` / `orderBookPosition`, enabling a + * future container-position / rearrangeable-tiles feature without changing the + * panel components themselves. + */ +export interface PerpsProLayoutConfig { + orderFormPosition: PerpsPanelPosition; + orderBookPosition: PerpsPanelPosition; +} + +/** + * Default two-column placement matching Figma node 10041:12979. + */ +export const DEFAULT_PRO_LAYOUT_CONFIG: PerpsProLayoutConfig = { + orderFormPosition: 'left', + orderBookPosition: 'right', +}; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx new file mode 100644 index 000000000000..4fce1f844d03 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx @@ -0,0 +1,27 @@ +import { Box } from '@metamask/design-system-react-native'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +const styles = StyleSheet.create({ + chart: { + height: 344, + }, +}); + +/** + * Pro-mode chart area placeholder. + * + * Scaffold only: empty container sized to the Figma chart height. Real chart + * content is added by TAT-3556. + */ +const PerpsProChartPanel = () => ( + + + +); + +export default PerpsProChartPanel; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx new file mode 100644 index 000000000000..567352430704 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx @@ -0,0 +1,40 @@ +import { + Box, + BoxAlignItems, + BoxFlexDirection, + BoxJustifyContent, + Text, + TextVariant, +} from '@metamask/design-system-react-native'; +import React from 'react'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +interface PerpsProMarketHeaderProps { + symbol?: string; +} + +/** + * Pro-mode market header (nav row + price/24h row). + * + * Scaffold only: renders the asset symbol for orientation. The Pro tag, + * compare/star/settings icons and price/change values are placeholder + * containers, filled in by later Pro-mode tickets. + */ +const PerpsProMarketHeader = ({ symbol }: PerpsProMarketHeaderProps) => ( + + + {symbol ?? ''} + + + + +); + +export default PerpsProMarketHeader; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx new file mode 100644 index 000000000000..8ccd4e52ca08 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx @@ -0,0 +1,58 @@ +import { Box, BoxFlexDirection } from '@metamask/design-system-react-native'; +import React, { type ReactNode } from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; +import type { PerpsProLayoutConfig } from '../PerpsProMarketView.types'; + +interface PerpsProMarketLayoutProps { + orderForm: ReactNode; + orderBook: ReactNode; + config: PerpsProLayoutConfig; +} + +const styles = StyleSheet.create({ + dividerColumn: { + width: 24, + }, + dividerLine: { + width: 1, + }, + rightColumn: { + width: 132, + }, +}); + +/** + * Two-column trading area for the Pro-mode market screen. + * + * Places the order form and order book panels into the left/right columns + * based on `config`, with a vertical divider between them. Making placement + * data-driven keeps the panels position-agnostic so a future + * container-position feature (fed by `proLayoutPreferences`) only needs to + * change the config, not the panels. + */ +const PerpsProMarketLayout = ({ + orderForm, + orderBook, + config, +}: PerpsProMarketLayoutProps) => { + const orderFormIsLeft = config.orderFormPosition === 'left'; + const leftPanel = orderFormIsLeft ? orderForm : orderBook; + const rightPanel = orderFormIsLeft ? orderBook : orderForm; + + return ( + + {leftPanel} + + + + {rightPanel} + + ); +}; + +export default PerpsProMarketLayout; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx new file mode 100644 index 000000000000..91e82f4b0ff3 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx @@ -0,0 +1,28 @@ +import { Box } from '@metamask/design-system-react-native'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +const styles = StyleSheet.create({ + body: { + minHeight: 480, + }, +}); + +/** + * Pro-mode order book column placeholder (right column). + * + * Scaffold only: empty container sized to the Figma order-book column, sized to + * fill the fixed-width column it is placed in. Real order book content + * (including the buy/sell ratio bar) is added by TAT-3555. + */ +const PerpsProOrderBookPanel = () => ( + + + +); + +export default PerpsProOrderBookPanel; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx new file mode 100644 index 000000000000..304d2e8ebf75 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx @@ -0,0 +1,27 @@ +import { Box } from '@metamask/design-system-react-native'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +const styles = StyleSheet.create({ + body: { + minHeight: 480, + }, +}); + +/** + * Pro-mode order form column placeholder (left column). + * + * Scaffold only: empty container sized to the Figma order-form column. Real + * form pieces/hooks are wired in by TAT-3551. + */ +const PerpsProOrderFormPanel = () => ( + + + +); + +export default PerpsProOrderFormPanel; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProPositionsPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProPositionsPanel.tsx new file mode 100644 index 000000000000..178564da4dff --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProPositionsPanel.tsx @@ -0,0 +1,27 @@ +import { Box } from '@metamask/design-system-react-native'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +const styles = StyleSheet.create({ + body: { + minHeight: 200, + }, +}); + +/** + * Pro-mode positions/orders section placeholder. + * + * Scaffold only: empty container matching the Figma tabs/filter/summary/list + * area. No ticket currently scopes its content. + */ +const PerpsProPositionsPanel = () => ( + + + +); + +export default PerpsProPositionsPanel; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProStatsBar.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProStatsBar.tsx new file mode 100644 index 000000000000..f4173d5b5844 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProStatsBar.tsx @@ -0,0 +1,33 @@ +import { + Box, + BoxAlignItems, + BoxFlexDirection, +} from '@metamask/design-system-react-native'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +const styles = StyleSheet.create({ + row: { + height: 36, + }, +}); + +/** + * Pro-mode stats bar placeholder (funding, 24h vol, open interest, prices). + * + * Scaffold only: empty row with the Figma top/bottom borders. Real stat values + * are added by a later Pro-mode ticket. + */ +const PerpsProStatsBar = () => ( + + + +); + +export default PerpsProStatsBar; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/index.ts b/app/components/UI/Perps/Views/PerpsProMarketView/index.ts new file mode 100644 index 000000000000..4b4447bef501 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/index.ts @@ -0,0 +1,2 @@ +export { default } from './PerpsProMarketView'; +export * from './PerpsProMarketView.types'; From 4b77e3a5bda71b6f649ab4742d0f83f5411500d0 Mon Sep 17 00:00:00 2001 From: geositta Date: Mon, 20 Jul 2026 22:49:19 -0500 Subject: [PATCH 3/7] fix: followups to scaffolding --- app/components/UI/Perps/Perps.testIds.ts | 6 ++ .../PerpsMarketDetailsRouter.test.tsx | 10 ++- .../PerpsProMarketView.test.tsx | 87 +++++++++++++++---- .../PerpsProMarketView/PerpsProMarketView.tsx | 20 ++--- .../PerpsProMarketView.types.ts | 17 ++++ .../components/PerpsProChartPanel.tsx | 8 +- .../components/PerpsProMarketHeader.tsx | 36 +++++--- .../components/PerpsProMarketLayout.tsx | 40 +++++++-- .../components/PerpsProOrderBookPanel.tsx | 16 +--- .../components/PerpsProOrderFormPanel.tsx | 15 +--- 10 files changed, 179 insertions(+), 76 deletions(-) diff --git a/app/components/UI/Perps/Perps.testIds.ts b/app/components/UI/Perps/Perps.testIds.ts index af4b781685bf..bcaa98ef79e4 100644 --- a/app/components/UI/Perps/Perps.testIds.ts +++ b/app/components/UI/Perps/Perps.testIds.ts @@ -464,9 +464,15 @@ export const PerpsProMarketViewSelectorsIDs = { CONTAINER: 'perps-pro-market-view', SCROLL_VIEW: 'perps-pro-market-scroll-view', HEADER: 'perps-pro-market-header', + HEADER_SYMBOL: 'perps-pro-market-header-symbol', + MARKET_SUMMARY: 'perps-pro-market-summary', CHART_PANEL: 'perps-pro-market-chart-panel', + CHART_CONTENT: 'perps-pro-market-chart-content', STATS_BAR: 'perps-pro-market-stats-bar', LAYOUT: 'perps-pro-market-layout', + LEFT_COLUMN: 'perps-pro-market-left-column', + RIGHT_COLUMN: 'perps-pro-market-right-column', + VERTICAL_DIVIDER: 'perps-pro-market-vertical-divider', ORDER_FORM_PANEL: 'perps-pro-market-order-form-panel', ORDER_BOOK_PANEL: 'perps-pro-market-order-book-panel', POSITIONS_PANEL: 'perps-pro-market-positions-panel', diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx index c06a23e90b2f..df42ba6c9a4a 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/PerpsMarketDetailsRouter.test.tsx @@ -33,8 +33,10 @@ describe('PerpsMarketDetailsRouter', () => { const { getByTestId, queryByTestId } = render(); - expect(getByTestId('mock-pro-market-view')).toBeTruthy(); - expect(queryByTestId('mock-lite-market-details-view')).toBeNull(); + expect(getByTestId('mock-pro-market-view')).toBeOnTheScreen(); + expect( + queryByTestId('mock-lite-market-details-view'), + ).not.toBeOnTheScreen(); }); it('renders PerpsMarketDetailsView when Pro mode is disabled', () => { @@ -42,7 +44,7 @@ describe('PerpsMarketDetailsRouter', () => { const { getByTestId, queryByTestId } = render(); - expect(getByTestId('mock-lite-market-details-view')).toBeTruthy(); - expect(queryByTestId('mock-pro-market-view')).toBeNull(); + expect(getByTestId('mock-lite-market-details-view')).toBeOnTheScreen(); + expect(queryByTestId('mock-pro-market-view')).not.toBeOnTheScreen(); }); }); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx index bbe12d39e0d1..0c91b101a535 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx @@ -1,10 +1,11 @@ import React from 'react'; +import { within } from '@testing-library/react-native'; import PerpsProMarketView from './'; import renderWithProvider from '../../../../../util/test/renderWithProvider'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { PerpsProMarketViewSelectorsIDs } from '../../Perps.testIds'; -const mockRouteParams: { market?: { symbol: string } } = { +const mockRouteParams = { market: { symbol: 'BTC' }, }; @@ -22,38 +23,92 @@ const renderView = () => }); describe('PerpsProMarketView', () => { - it('renders the container and scroll view', () => { + beforeEach(() => { + mockRouteParams.market.symbol = 'BTC'; + }); + + it('renders the screen inside every safe-area edge', () => { const { getByTestId } = renderView(); - expect(getByTestId(PerpsProMarketViewSelectorsIDs.CONTAINER)).toBeTruthy(); - expect( - getByTestId(PerpsProMarketViewSelectorsIDs.SCROLL_VIEW), - ).toBeTruthy(); + expect(getByTestId(PerpsProMarketViewSelectorsIDs.CONTAINER)).toHaveProp( + 'edges', + ['top', 'bottom', 'left', 'right'], + ); }); - it('renders every layout panel placeholder', () => { + it('renders every top-level scaffold slot', () => { const { getByTestId } = renderView(); - expect(getByTestId(PerpsProMarketViewSelectorsIDs.HEADER)).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.HEADER), + ).toBeOnTheScreen(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.MARKET_SUMMARY), + ).toBeOnTheScreen(); expect( getByTestId(PerpsProMarketViewSelectorsIDs.CHART_PANEL), - ).toBeTruthy(); - expect(getByTestId(PerpsProMarketViewSelectorsIDs.STATS_BAR)).toBeTruthy(); - expect(getByTestId(PerpsProMarketViewSelectorsIDs.LAYOUT)).toBeTruthy(); + ).toBeOnTheScreen(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.STATS_BAR), + ).toBeOnTheScreen(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.LAYOUT), + ).toBeOnTheScreen(); expect( getByTestId(PerpsProMarketViewSelectorsIDs.ORDER_FORM_PANEL), - ).toBeTruthy(); + ).toBeOnTheScreen(); expect( getByTestId(PerpsProMarketViewSelectorsIDs.ORDER_BOOK_PANEL), - ).toBeTruthy(); + ).toBeOnTheScreen(); expect( getByTestId(PerpsProMarketViewSelectorsIDs.POSITIONS_PANEL), - ).toBeTruthy(); + ).toBeOnTheScreen(); + }); + + it('keeps the header fixed while the market summary scrolls', () => { + const { getByTestId } = renderView(); + + const scrollView = getByTestId(PerpsProMarketViewSelectorsIDs.SCROLL_VIEW); + + expect( + within(scrollView).queryByTestId(PerpsProMarketViewSelectorsIDs.HEADER), + ).not.toBeOnTheScreen(); + expect( + within(scrollView).getByTestId( + PerpsProMarketViewSelectorsIDs.MARKET_SUMMARY, + ), + ).toBeOnTheScreen(); }); it('shows the asset symbol from route params in the header', () => { - const { getByText } = renderView(); + const { getByTestId } = renderView(); + + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.HEADER_SYMBOL), + ).toHaveTextContent(/^BTC$/); + }); + + it('removes the HIP-3 dex prefix from the header symbol', () => { + mockRouteParams.market.symbol = 'xyz:TSLA'; + + const { getByTestId } = renderView(); - expect(getByText('BTC')).toBeTruthy(); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.HEADER_SYMBOL), + ).toHaveTextContent(/^TSLA$/); + }); + + it('uses the Figma shell heights', () => { + const { getByTestId } = renderView(); + + expect(getByTestId(PerpsProMarketViewSelectorsIDs.HEADER)).toHaveStyle({ + height: 64, + }); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.MARKET_SUMMARY), + ).toHaveStyle({ height: 76 }); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.CHART_CONTENT), + ).toHaveStyle({ height: 344 }); }); }); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx index e3984687670e..8ca70f56f0d2 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx @@ -1,14 +1,16 @@ import { SectionDivider } from '@metamask/design-system-react-native'; -import type { PerpsMarketData } from '@metamask/perps-controller'; +import { getPerpsDisplaySymbol } from '@metamask/perps-controller'; import { useRoute, type RouteProp } from '@react-navigation/native'; import React from 'react'; import { ScrollView } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useStyles } from '../../../../../component-library/hooks'; import { PerpsProMarketViewSelectorsIDs } from '../../Perps.testIds'; +import type { PerpsStackParamList } from '../../types/navigation'; import PerpsProChartPanel from './components/PerpsProChartPanel'; import PerpsProMarketHeader from './components/PerpsProMarketHeader'; import PerpsProMarketLayout from './components/PerpsProMarketLayout'; +import PerpsProMarketSummary from './components/PerpsProMarketSummary'; import PerpsProOrderBookPanel from './components/PerpsProOrderBookPanel'; import PerpsProOrderFormPanel from './components/PerpsProOrderFormPanel'; import PerpsProPositionsPanel from './components/PerpsProPositionsPanel'; @@ -16,28 +18,24 @@ import PerpsProStatsBar from './components/PerpsProStatsBar'; import { createStyles } from './PerpsProMarketView.styles'; import { DEFAULT_PRO_LAYOUT_CONFIG } from './PerpsProMarketView.types'; -interface PerpsProMarketRouteParams { - market?: PerpsMarketData; -} - /** * Pro-mode replacement for `PerpsMarketDetailsView`. * * Scaffold only: lays out the full Pro trading screen (header, chart, stats * bar, two-column order form / order book, and positions/orders section) as - * placeholder containers matching Figma node 10041:12979, so later tickets - * (TAT-3555/TAT-3556/TAT-3551) can drop real panels into existing slots. + * placeholder containers matching Figma node 10041:12979. Each panel can be + * populated by its owning capability without changing the top-level layout. */ const PerpsProMarketView = () => { const { styles } = useStyles(createStyles, {}); const route = - useRoute>(); - const symbol = route.params?.market?.symbol; + useRoute>(); + const symbol = getPerpsDisplaySymbol(route.params.market.symbol); return ( @@ -45,7 +43,9 @@ const PerpsProMarketView = () => { style={styles.scrollView} contentContainerStyle={styles.scrollContent} testID={PerpsProMarketViewSelectorsIDs.SCROLL_VIEW} + showsVerticalScrollIndicator={false} > + + config.orderFormPosition !== config.orderBookPosition + ? config + : DEFAULT_PRO_LAYOUT_CONFIG; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx index 4fce1f844d03..9a3dd291bc52 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProChartPanel.tsx @@ -13,14 +13,18 @@ const styles = StyleSheet.create({ * Pro-mode chart area placeholder. * * Scaffold only: empty container sized to the Figma chart height. Real chart - * content is added by TAT-3556. + * content is added by the chart capability. */ const PerpsProChartPanel = () => ( - + ); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx index 567352430704..e7bc20dd6ff1 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketHeader.tsx @@ -7,33 +7,41 @@ import { TextVariant, } from '@metamask/design-system-react-native'; import React from 'react'; +import { StyleSheet } from 'react-native'; import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; interface PerpsProMarketHeaderProps { - symbol?: string; + symbol: string; } +const styles = StyleSheet.create({ + container: { + height: 64, + }, +}); + /** - * Pro-mode market header (nav row + price/24h row). + * Fixed Pro-mode market header. * - * Scaffold only: renders the asset symbol for orientation. The Pro tag, - * compare/star/settings icons and price/change values are placeholder - * containers, filled in by later Pro-mode tickets. + * Scaffold only: renders the normalized asset symbol for orientation. Header + * actions remain placeholders until their owning capability is implemented. */ const PerpsProMarketHeader = ({ symbol }: PerpsProMarketHeaderProps) => ( - - {symbol ?? ''} - - - + {symbol} + + ); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx index 8ccd4e52ca08..c41abd6e26cb 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx @@ -2,7 +2,10 @@ import { Box, BoxFlexDirection } from '@metamask/design-system-react-native'; import React, { type ReactNode } from 'react'; import { StyleSheet } from 'react-native'; import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; -import type { PerpsProLayoutConfig } from '../PerpsProMarketView.types'; +import { + resolvePerpsProLayoutConfig, + type PerpsProLayoutConfig, +} from '../PerpsProMarketView.types'; interface PerpsProMarketLayoutProps { orderForm: ReactNode; @@ -10,15 +13,22 @@ interface PerpsProMarketLayoutProps { config: PerpsProLayoutConfig; } +const PRO_TRADING_AREA_MIN_HEIGHT = 682; +const PRO_DIVIDER_COLUMN_WIDTH = 24; +const PRO_ORDER_BOOK_COLUMN_WIDTH = 132; + const styles = StyleSheet.create({ + container: { + minHeight: PRO_TRADING_AREA_MIN_HEIGHT, + }, dividerColumn: { - width: 24, + width: PRO_DIVIDER_COLUMN_WIDTH, }, dividerLine: { width: 1, }, rightColumn: { - width: 132, + width: PRO_ORDER_BOOK_COLUMN_WIDTH, }, }); @@ -36,7 +46,8 @@ const PerpsProMarketLayout = ({ orderBook, config, }: PerpsProMarketLayoutProps) => { - const orderFormIsLeft = config.orderFormPosition === 'left'; + const resolvedConfig = resolvePerpsProLayoutConfig(config); + const orderFormIsLeft = resolvedConfig.orderFormPosition === 'left'; const leftPanel = orderFormIsLeft ? orderForm : orderBook; const rightPanel = orderFormIsLeft ? orderBook : orderForm; @@ -45,12 +56,27 @@ const PerpsProMarketLayout = ({ testID={PerpsProMarketViewSelectorsIDs.LAYOUT} flexDirection={BoxFlexDirection.Row} twClassName="px-4" + style={styles.container} > - {leftPanel} - + + {leftPanel} + + - {rightPanel} + + {rightPanel} + ); }; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx index 91e82f4b0ff3..92e59f4caedd 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderBookPanel.tsx @@ -1,27 +1,19 @@ import { Box } from '@metamask/design-system-react-native'; import React from 'react'; -import { StyleSheet } from 'react-native'; import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; -const styles = StyleSheet.create({ - body: { - minHeight: 480, - }, -}); - /** * Pro-mode order book column placeholder (right column). * - * Scaffold only: empty container sized to the Figma order-book column, sized to - * fill the fixed-width column it is placed in. Real order book content - * (including the buy/sell ratio bar) is added by TAT-3555. + * Scaffold only: empty container sized to fill the fixed-width column. The + * inline order-book capability populates this slot. */ const PerpsProOrderBookPanel = () => ( - + ); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx index 304d2e8ebf75..4c4bc3f98c06 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProOrderFormPanel.tsx @@ -1,26 +1,19 @@ import { Box } from '@metamask/design-system-react-native'; import React from 'react'; -import { StyleSheet } from 'react-native'; import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; -const styles = StyleSheet.create({ - body: { - minHeight: 480, - }, -}); - /** * Pro-mode order form column placeholder (left column). * - * Scaffold only: empty container sized to the Figma order-form column. Real - * form pieces/hooks are wired in by TAT-3551. + * Scaffold only: empty container sized to the Figma order-form column. The + * inline order-form capability populates this slot. */ const PerpsProOrderFormPanel = () => ( - + ); From 26e5b6a4b741ae702a7e74e58c36fe645d4b1d5a Mon Sep 17 00:00:00 2001 From: geositta Date: Mon, 20 Jul 2026 22:50:58 -0500 Subject: [PATCH 4/7] fix: add layout test file and summary component --- .../components/PerpsProMarketLayout.test.tsx | 92 +++++++++++++++++++ .../components/PerpsProMarketSummary.tsx | 38 ++++++++ 2 files changed, 130 insertions(+) create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx create mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketSummary.tsx diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx new file mode 100644 index 000000000000..2a164cee2f2d --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import { View } from 'react-native'; +import { render, within } from '@testing-library/react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; +import type { PerpsProLayoutConfig } from '../PerpsProMarketView.types'; +import PerpsProMarketLayout from './PerpsProMarketLayout'; + +const renderLayout = (config: PerpsProLayoutConfig) => + render( + } + orderBook={} + />, + ); + +describe('PerpsProMarketLayout', () => { + it.each([ + { + name: 'default opposing positions', + config: { + orderFormPosition: 'left', + orderBookPosition: 'right', + }, + expectedLeftPanel: 'mock-order-form', + expectedRightPanel: 'mock-order-book', + }, + { + name: 'reversed opposing positions', + config: { + orderFormPosition: 'right', + orderBookPosition: 'left', + }, + expectedLeftPanel: 'mock-order-book', + expectedRightPanel: 'mock-order-form', + }, + { + name: 'conflicting left positions', + config: { + orderFormPosition: 'left', + orderBookPosition: 'left', + }, + expectedLeftPanel: 'mock-order-form', + expectedRightPanel: 'mock-order-book', + }, + { + name: 'conflicting right positions', + config: { + orderFormPosition: 'right', + orderBookPosition: 'right', + }, + expectedLeftPanel: 'mock-order-form', + expectedRightPanel: 'mock-order-book', + }, + ] as const)( + 'places panels for $name', + ({ config, expectedLeftPanel, expectedRightPanel }) => { + const { getByTestId } = renderLayout(config); + + const leftColumn = getByTestId( + PerpsProMarketViewSelectorsIDs.LEFT_COLUMN, + ); + const rightColumn = getByTestId( + PerpsProMarketViewSelectorsIDs.RIGHT_COLUMN, + ); + + expect( + within(leftColumn).getByTestId(expectedLeftPanel), + ).toBeOnTheScreen(); + expect( + within(rightColumn).getByTestId(expectedRightPanel), + ).toBeOnTheScreen(); + }, + ); + + it('uses the Figma trading-area dimensions', () => { + const { getByTestId } = renderLayout({ + orderFormPosition: 'left', + orderBookPosition: 'right', + }); + + expect(getByTestId(PerpsProMarketViewSelectorsIDs.LAYOUT)).toHaveStyle({ + minHeight: 682, + }); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.VERTICAL_DIVIDER), + ).toHaveStyle({ width: 24 }); + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.RIGHT_COLUMN), + ).toHaveStyle({ width: 132 }); + }); +}); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketSummary.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketSummary.tsx new file mode 100644 index 000000000000..0befe273cbd9 --- /dev/null +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketSummary.tsx @@ -0,0 +1,38 @@ +import { + Box, + BoxAlignItems, + BoxFlexDirection, + BoxJustifyContent, +} from '@metamask/design-system-react-native'; +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; + +const styles = StyleSheet.create({ + container: { + height: 76, + }, +}); + +/** + * Scroll-contained market price and 24-hour change placeholder. + * + * The fixed app header remains outside the scroll view. Price content and the + * compact chart action are populated by the panel implementation that owns + * live market data. + */ +const PerpsProMarketSummary = () => ( + + + + +); + +export default PerpsProMarketSummary; From f8e9b4229e0a3b3c130befd07aa676b21f87c120 Mon Sep 17 00:00:00 2001 From: geositta Date: Mon, 20 Jul 2026 23:26:19 -0500 Subject: [PATCH 5/7] fix: orderbook width --- .../components/PerpsProMarketLayout.test.tsx | 21 +++++++++++++++---- .../components/PerpsProMarketLayout.tsx | 15 ++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx index 2a164cee2f2d..928548d9de67 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx @@ -24,6 +24,8 @@ describe('PerpsProMarketLayout', () => { }, expectedLeftPanel: 'mock-order-form', expectedRightPanel: 'mock-order-book', + expectedLeftColumnStyle: { flex: 1 }, + expectedRightColumnStyle: { width: 132 }, }, { name: 'reversed opposing positions', @@ -33,6 +35,8 @@ describe('PerpsProMarketLayout', () => { }, expectedLeftPanel: 'mock-order-book', expectedRightPanel: 'mock-order-form', + expectedLeftColumnStyle: { width: 132 }, + expectedRightColumnStyle: { flex: 1 }, }, { name: 'conflicting left positions', @@ -42,6 +46,8 @@ describe('PerpsProMarketLayout', () => { }, expectedLeftPanel: 'mock-order-form', expectedRightPanel: 'mock-order-book', + expectedLeftColumnStyle: { flex: 1 }, + expectedRightColumnStyle: { width: 132 }, }, { name: 'conflicting right positions', @@ -51,10 +57,18 @@ describe('PerpsProMarketLayout', () => { }, expectedLeftPanel: 'mock-order-form', expectedRightPanel: 'mock-order-book', + expectedLeftColumnStyle: { flex: 1 }, + expectedRightColumnStyle: { width: 132 }, }, ] as const)( 'places panels for $name', - ({ config, expectedLeftPanel, expectedRightPanel }) => { + ({ + config, + expectedLeftPanel, + expectedRightPanel, + expectedLeftColumnStyle, + expectedRightColumnStyle, + }) => { const { getByTestId } = renderLayout(config); const leftColumn = getByTestId( @@ -70,6 +84,8 @@ describe('PerpsProMarketLayout', () => { expect( within(rightColumn).getByTestId(expectedRightPanel), ).toBeOnTheScreen(); + expect(leftColumn).toHaveStyle(expectedLeftColumnStyle); + expect(rightColumn).toHaveStyle(expectedRightColumnStyle); }, ); @@ -85,8 +101,5 @@ describe('PerpsProMarketLayout', () => { expect( getByTestId(PerpsProMarketViewSelectorsIDs.VERTICAL_DIVIDER), ).toHaveStyle({ width: 24 }); - expect( - getByTestId(PerpsProMarketViewSelectorsIDs.RIGHT_COLUMN), - ).toHaveStyle({ width: 132 }); }); }); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx index c41abd6e26cb..fc3766f43a5d 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx @@ -27,7 +27,10 @@ const styles = StyleSheet.create({ dividerLine: { width: 1, }, - rightColumn: { + orderFormColumn: { + flex: 1, + }, + orderBookColumn: { width: PRO_ORDER_BOOK_COLUMN_WIDTH, }, }); @@ -50,6 +53,12 @@ const PerpsProMarketLayout = ({ const orderFormIsLeft = resolvedConfig.orderFormPosition === 'left'; const leftPanel = orderFormIsLeft ? orderForm : orderBook; const rightPanel = orderFormIsLeft ? orderBook : orderForm; + const leftColumnStyle = orderFormIsLeft + ? styles.orderFormColumn + : styles.orderBookColumn; + const rightColumnStyle = orderFormIsLeft + ? styles.orderBookColumn + : styles.orderFormColumn; return ( {leftPanel} @@ -73,7 +82,7 @@ const PerpsProMarketLayout = ({ {rightPanel} From 1ebc35b56670789346f0ad9ad7a2bad3b26092a2 Mon Sep 17 00:00:00 2001 From: geositta Date: Tue, 21 Jul 2026 16:39:47 -0500 Subject: [PATCH 6/7] fix: rm layout config, and use fixed layout --- .../usePerpsProModeEnabled.ts | 11 +-- .../PerpsProMarketView/PerpsProMarketView.tsx | 2 - .../PerpsProMarketView.types.ts | 43 --------- .../components/PerpsProMarketLayout.test.tsx | 93 +++---------------- .../components/PerpsProMarketLayout.tsx | 78 ++++++---------- .../Perps/Views/PerpsProMarketView/index.ts | 1 - 6 files changed, 49 insertions(+), 179 deletions(-) delete mode 100644 app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts diff --git a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts index 19518991126d..1b1686cf15eb 100644 --- a/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts +++ b/app/components/UI/Perps/Views/PerpsMarketDetailsRouter/usePerpsProModeEnabled.ts @@ -1,12 +1,11 @@ /** * Temporary gate for the Perps Pro-mode layout. * - * TODO(TAT-3551): replace this placeholder with the real mobile selector - * wrapper around `selectPerpsMode` from `@metamask/perps-controller` (via - * `app/components/UI/Perps/selectors/perpsController`) once that package is - * bumped to expose the `mode` field. Until then this always returns `false`, - * so every user keeps the lite `PerpsMarketDetailsView` and the Pro layout is - * not user-reachable. + * TODO(TAT-3551): replace this placeholder once the mobile feature-flag and + * mode selectors land. The Pro view should render only when the feature flag + * is enabled and the persisted controller mode is `PerpsMode.Pro`. Until then + * this always returns `false`, so every user keeps the lite + * `PerpsMarketDetailsView` and the Pro layout is not user-reachable. * * @returns Whether the Perps Pro-mode layout is enabled for the current user. */ diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx index 8ca70f56f0d2..6ce6ef8fbe08 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx @@ -16,7 +16,6 @@ import PerpsProOrderFormPanel from './components/PerpsProOrderFormPanel'; import PerpsProPositionsPanel from './components/PerpsProPositionsPanel'; import PerpsProStatsBar from './components/PerpsProStatsBar'; import { createStyles } from './PerpsProMarketView.styles'; -import { DEFAULT_PRO_LAYOUT_CONFIG } from './PerpsProMarketView.types'; /** * Pro-mode replacement for `PerpsMarketDetailsView`. @@ -49,7 +48,6 @@ const PerpsProMarketView = () => { } orderBook={} /> diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts deleted file mode 100644 index da27c23e7a97..000000000000 --- a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.types.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Which column a Pro-mode panel occupies in the two-column trading area. - */ -export type PerpsPanelPosition = 'left' | 'right'; - -/** - * Placement config for the Pro-mode two-column trading area. - * - * Currently a static default (order form left, order book right). Once the - * `@metamask/perps-controller` package is bumped, this will be fed by - * `proLayoutPreferences.orderFormPosition` / `orderBookPosition`, enabling a - * future container-position / rearrangeable-tiles feature without changing the - * panel components themselves. - */ -export interface PerpsProLayoutConfig { - orderFormPosition: PerpsPanelPosition; - orderBookPosition: PerpsPanelPosition; -} - -/** - * Default two-column placement matching Figma node 10041:12979. - */ -export const DEFAULT_PRO_LAYOUT_CONFIG: PerpsProLayoutConfig = { - orderFormPosition: 'left', - orderBookPosition: 'right', -}; - -/** - * Resolves persisted panel positions into a renderable two-column layout. - * - * Both Core fields are retained for preference parity. During rehydration or - * migration they may temporarily conflict; in that case the Figma default is - * used so both panels remain visible. - * - * @param config - Persisted order-form and order-book positions. - * @returns Opposing positions safe to render. - */ -export const resolvePerpsProLayoutConfig = ( - config: PerpsProLayoutConfig, -): PerpsProLayoutConfig => - config.orderFormPosition !== config.orderBookPosition - ? config - : DEFAULT_PRO_LAYOUT_CONFIG; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx index 928548d9de67..3931694d9211 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.test.tsx @@ -2,98 +2,35 @@ import React from 'react'; import { View } from 'react-native'; import { render, within } from '@testing-library/react-native'; import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; -import type { PerpsProLayoutConfig } from '../PerpsProMarketView.types'; import PerpsProMarketLayout from './PerpsProMarketLayout'; -const renderLayout = (config: PerpsProLayoutConfig) => +const renderLayout = () => render( } orderBook={} />, ); describe('PerpsProMarketLayout', () => { - it.each([ - { - name: 'default opposing positions', - config: { - orderFormPosition: 'left', - orderBookPosition: 'right', - }, - expectedLeftPanel: 'mock-order-form', - expectedRightPanel: 'mock-order-book', - expectedLeftColumnStyle: { flex: 1 }, - expectedRightColumnStyle: { width: 132 }, - }, - { - name: 'reversed opposing positions', - config: { - orderFormPosition: 'right', - orderBookPosition: 'left', - }, - expectedLeftPanel: 'mock-order-book', - expectedRightPanel: 'mock-order-form', - expectedLeftColumnStyle: { width: 132 }, - expectedRightColumnStyle: { flex: 1 }, - }, - { - name: 'conflicting left positions', - config: { - orderFormPosition: 'left', - orderBookPosition: 'left', - }, - expectedLeftPanel: 'mock-order-form', - expectedRightPanel: 'mock-order-book', - expectedLeftColumnStyle: { flex: 1 }, - expectedRightColumnStyle: { width: 132 }, - }, - { - name: 'conflicting right positions', - config: { - orderFormPosition: 'right', - orderBookPosition: 'right', - }, - expectedLeftPanel: 'mock-order-form', - expectedRightPanel: 'mock-order-book', - expectedLeftColumnStyle: { flex: 1 }, - expectedRightColumnStyle: { width: 132 }, - }, - ] as const)( - 'places panels for $name', - ({ - config, - expectedLeftPanel, - expectedRightPanel, - expectedLeftColumnStyle, - expectedRightColumnStyle, - }) => { - const { getByTestId } = renderLayout(config); + it('places the order form left and the order book right', () => { + const { getByTestId } = renderLayout(); - const leftColumn = getByTestId( - PerpsProMarketViewSelectorsIDs.LEFT_COLUMN, - ); - const rightColumn = getByTestId( - PerpsProMarketViewSelectorsIDs.RIGHT_COLUMN, - ); + const leftColumn = getByTestId(PerpsProMarketViewSelectorsIDs.LEFT_COLUMN); + const rightColumn = getByTestId( + PerpsProMarketViewSelectorsIDs.RIGHT_COLUMN, + ); - expect( - within(leftColumn).getByTestId(expectedLeftPanel), - ).toBeOnTheScreen(); - expect( - within(rightColumn).getByTestId(expectedRightPanel), - ).toBeOnTheScreen(); - expect(leftColumn).toHaveStyle(expectedLeftColumnStyle); - expect(rightColumn).toHaveStyle(expectedRightColumnStyle); - }, - ); + expect(within(leftColumn).getByTestId('mock-order-form')).toBeOnTheScreen(); + expect( + within(rightColumn).getByTestId('mock-order-book'), + ).toBeOnTheScreen(); + expect(leftColumn).toHaveStyle({ flex: 1 }); + expect(rightColumn).toHaveStyle({ width: 132 }); + }); it('uses the Figma trading-area dimensions', () => { - const { getByTestId } = renderLayout({ - orderFormPosition: 'left', - orderBookPosition: 'right', - }); + const { getByTestId } = renderLayout(); expect(getByTestId(PerpsProMarketViewSelectorsIDs.LAYOUT)).toHaveStyle({ minHeight: 682, diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx index fc3766f43a5d..73fb3ad60960 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/components/PerpsProMarketLayout.tsx @@ -2,15 +2,10 @@ import { Box, BoxFlexDirection } from '@metamask/design-system-react-native'; import React, { type ReactNode } from 'react'; import { StyleSheet } from 'react-native'; import { PerpsProMarketViewSelectorsIDs } from '../../../Perps.testIds'; -import { - resolvePerpsProLayoutConfig, - type PerpsProLayoutConfig, -} from '../PerpsProMarketView.types'; interface PerpsProMarketLayoutProps { orderForm: ReactNode; orderBook: ReactNode; - config: PerpsProLayoutConfig; } const PRO_TRADING_AREA_MIN_HEIGHT = 682; @@ -38,56 +33,41 @@ const styles = StyleSheet.create({ /** * Two-column trading area for the Pro-mode market screen. * - * Places the order form and order book panels into the left/right columns - * based on `config`, with a vertical divider between them. Making placement - * data-driven keeps the panels position-agnostic so a future - * container-position feature (fed by `proLayoutPreferences`) only needs to - * change the config, not the panels. + * Matches the current Figma layout: the order form fills the left column and + * the order book occupies the fixed-width right column. Configurable panel + * positioning is deferred until the rearrangeable-layout feature consumes the + * controller preferences. */ const PerpsProMarketLayout = ({ orderForm, orderBook, - config, -}: PerpsProMarketLayoutProps) => { - const resolvedConfig = resolvePerpsProLayoutConfig(config); - const orderFormIsLeft = resolvedConfig.orderFormPosition === 'left'; - const leftPanel = orderFormIsLeft ? orderForm : orderBook; - const rightPanel = orderFormIsLeft ? orderBook : orderForm; - const leftColumnStyle = orderFormIsLeft - ? styles.orderFormColumn - : styles.orderBookColumn; - const rightColumnStyle = orderFormIsLeft - ? styles.orderBookColumn - : styles.orderFormColumn; - - return ( +}: PerpsProMarketLayoutProps) => ( + + + {orderForm} + + + + - - {leftPanel} - - - - - - {rightPanel} - + {orderBook} - ); -}; + +); export default PerpsProMarketLayout; diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/index.ts b/app/components/UI/Perps/Views/PerpsProMarketView/index.ts index 4b4447bef501..487b5c21bcb9 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/index.ts +++ b/app/components/UI/Perps/Views/PerpsProMarketView/index.ts @@ -1,2 +1 @@ export { default } from './PerpsProMarketView'; -export * from './PerpsProMarketView.types'; From f511aed5f66e289ca6312ea8b848cd015f6a8151 Mon Sep 17 00:00:00 2001 From: geositta Date: Wed, 22 Jul 2026 08:35:08 -0500 Subject: [PATCH 7/7] feat(perps): add route validation fix --- app/components/UI/Perps/Perps.testIds.ts | 1 + .../PerpsProMarketView.test.tsx | 33 +++++++++++++++++-- .../PerpsProMarketView/PerpsProMarketView.tsx | 31 +++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/app/components/UI/Perps/Perps.testIds.ts b/app/components/UI/Perps/Perps.testIds.ts index bcaa98ef79e4..e850e672edd3 100644 --- a/app/components/UI/Perps/Perps.testIds.ts +++ b/app/components/UI/Perps/Perps.testIds.ts @@ -462,6 +462,7 @@ export const PerpsMarketDetailsViewSelectorsIDs = { export const PerpsProMarketViewSelectorsIDs = { CONTAINER: 'perps-pro-market-view', + ERROR: 'perps-pro-market-error', SCROLL_VIEW: 'perps-pro-market-scroll-view', HEADER: 'perps-pro-market-header', HEADER_SYMBOL: 'perps-pro-market-header-symbol', diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx index 0c91b101a535..930a1217ffa3 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.test.tsx @@ -5,7 +5,11 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider'; import { backgroundState } from '../../../../../util/test/initial-root-state'; import { PerpsProMarketViewSelectorsIDs } from '../../Perps.testIds'; -const mockRouteParams = { +interface MockRouteParams { + market?: { symbol: string }; +} + +let mockRouteParams: MockRouteParams | undefined = { market: { symbol: 'BTC' }, }; @@ -24,9 +28,32 @@ const renderView = () => describe('PerpsProMarketView', () => { beforeEach(() => { - mockRouteParams.market.symbol = 'BTC'; + mockRouteParams = { market: { symbol: 'BTC' } }; }); + it.each([ + ['params', undefined], + ['market', {}], + ['symbol', { market: { symbol: '' } }], + ] as const)( + 'renders the error state when route %s are invalid', + (_missingField, params) => { + mockRouteParams = params; + + const { getByTestId, queryByTestId, getByText } = renderView(); + + expect( + getByTestId(PerpsProMarketViewSelectorsIDs.ERROR), + ).toBeOnTheScreen(); + expect( + getByText('Market data not found. Please go back and try again.'), + ).toBeOnTheScreen(); + expect( + queryByTestId(PerpsProMarketViewSelectorsIDs.CONTAINER), + ).not.toBeOnTheScreen(); + }, + ); + it('renders the screen inside every safe-area edge', () => { const { getByTestId } = renderView(); @@ -89,7 +116,7 @@ describe('PerpsProMarketView', () => { }); it('removes the HIP-3 dex prefix from the header symbol', () => { - mockRouteParams.market.symbol = 'xyz:TSLA'; + mockRouteParams = { market: { symbol: 'xyz:TSLA' } }; const { getByTestId } = renderView(); diff --git a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx index 6ce6ef8fbe08..37c12e40c83e 100644 --- a/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx +++ b/app/components/UI/Perps/Views/PerpsProMarketView/PerpsProMarketView.tsx @@ -1,9 +1,16 @@ -import { SectionDivider } from '@metamask/design-system-react-native'; +import { + Box, + SectionDivider, + Text, + TextColor, + TextVariant, +} from '@metamask/design-system-react-native'; import { getPerpsDisplaySymbol } from '@metamask/perps-controller'; import { useRoute, type RouteProp } from '@react-navigation/native'; import React from 'react'; import { ScrollView } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { strings } from '../../../../../../locales/i18n'; import { useStyles } from '../../../../../component-library/hooks'; import { PerpsProMarketViewSelectorsIDs } from '../../Perps.testIds'; import type { PerpsStackParamList } from '../../types/navigation'; @@ -29,7 +36,27 @@ const PerpsProMarketView = () => { const { styles } = useStyles(createStyles, {}); const route = useRoute>(); - const symbol = getPerpsDisplaySymbol(route.params.market.symbol); + const market = route.params?.market; + + if (!market?.symbol) { + return ( + + + + {strings('perps.market.details.error_message')} + + + + ); + } + + const symbol = getPerpsDisplaySymbol(market.symbol); return (