From 28be89188b4265bdce6e2b9328668de4931ba519 Mon Sep 17 00:00:00 2001 From: Arthur Breton Date: Fri, 17 Oct 2025 19:40:32 +0800 Subject: [PATCH 01/20] refactor(perps): migrate tpsl bottomsheet to fullscreen view --- .../PerpsOrderView/PerpsOrderView.styles.ts | 3 +- .../PerpsOrderView/PerpsOrderView.test.tsx | 2 +- .../Views/PerpsOrderView/PerpsOrderView.tsx | 74 ++++--- .../PerpsPositionsView/PerpsPositionsView.tsx | 45 +--- .../PerpsTPSLView/PerpsTPSLView.styles.ts} | 26 ++- .../PerpsTPSLView/PerpsTPSLView.test.tsx} | 132 ++++++----- .../PerpsTPSLView/PerpsTPSLView.tsx} | 205 +++++++++--------- .../PerpsPositionCard.test.tsx | 8 +- .../PerpsPositionCard/PerpsPositionCard.tsx | 58 ++--- .../components/PerpsTPSLBottomSheet/index.ts | 1 - .../UI/Perps/hooks/usePerpsTPSLUpdate.ts | 4 +- app/components/UI/Perps/routes/index.tsx | 11 + app/components/UI/Perps/types/navigation.ts | 14 ++ app/constants/navigation/Routes.ts | 1 + e2e/selectors/Perps/Perps.selectors.ts | 6 +- 15 files changed, 288 insertions(+), 302 deletions(-) rename app/components/UI/Perps/{components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.styles.ts => Views/PerpsTPSLView/PerpsTPSLView.styles.ts} (90%) rename app/components/UI/Perps/{components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx => Views/PerpsTPSLView/PerpsTPSLView.test.tsx} (92%) rename app/components/UI/Perps/{components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx => Views/PerpsTPSLView/PerpsTPSLView.tsx} (86%) delete mode 100644 app/components/UI/Perps/components/PerpsTPSLBottomSheet/index.ts diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.styles.ts b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.styles.ts index fc3dfa81d6e7..982b1c70be63 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.styles.ts +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.styles.ts @@ -1,4 +1,4 @@ -import { StyleSheet, Platform } from 'react-native'; +import { StyleSheet } from 'react-native'; import type { Colors } from '../../../../../util/theme/models'; const createStyles = (colors: Colors) => @@ -25,7 +25,6 @@ const createStyles = (colors: Colors) => borderTopColor: colors.border.muted, paddingHorizontal: 16, paddingTop: 16, - paddingBottom: Platform.OS === 'ios' ? 32 : 16, }, sliderSection: { paddingHorizontal: 32, diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx index 8f81b739a4d8..5a5191a8c65d 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx @@ -438,7 +438,7 @@ const createBottomSheetMock = (testId: string) => { }; }; -jest.mock('../../components/PerpsTPSLBottomSheet', () => +jest.mock('../PerpsTPSLView/PerpsTPSLView', () => createBottomSheetMock('tpsl-bottom-sheet'), ); jest.mock('../../components/PerpsLeverageBottomSheet', () => diff --git a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx index e04ff54e199a..37dee92fe802 100644 --- a/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx +++ b/app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx @@ -12,7 +12,10 @@ import React, { useState, } from 'react'; import { ScrollView, TouchableOpacity, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { + SafeAreaView, + useSafeAreaInsets, +} from 'react-native-safe-area-context'; import { PerpsOrderViewSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors'; import { ButtonSize as ButtonSizeRNDesignSystem } from '@metamask/design-system-react-native'; @@ -51,7 +54,6 @@ import PerpsLimitPriceBottomSheet from '../../components/PerpsLimitPriceBottomSh import PerpsOrderHeader from '../../components/PerpsOrderHeader'; import PerpsOrderTypeBottomSheet from '../../components/PerpsOrderTypeBottomSheet'; import PerpsSlider from '../../components/PerpsSlider'; -import PerpsTPSLBottomSheet from '../../components/PerpsTPSLBottomSheet'; import RewardsAnimations, { RewardAnimationState, } from '../../../Rewards/components/RewardPointsAnimation'; @@ -138,9 +140,19 @@ interface OrderRouteParams { const PerpsOrderViewContentBase: React.FC = () => { const navigation = useNavigation>(); const { colors } = useTheme(); + const insets = useSafeAreaInsets(); const styles = createStyles(colors); + // Dynamic bottom padding for fixed container: safe area inset + 16px visual padding + const fixedBottomContainerStyle = useMemo( + () => ({ + ...styles.fixedBottomContainer, + paddingBottom: insets.bottom + 16, + }), + [styles.fixedBottomContainer, insets.bottom], + ); + const [selectedTooltip, setSelectedTooltip] = useState(null); @@ -241,7 +253,6 @@ const PerpsOrderViewContentBase: React.FC = () => { orderTypeRef.current = orderForm.type; }, [orderForm.type]); - const [isTPSLVisible, setIsTPSLVisible] = useState(false); const [isLeverageVisible, setIsLeverageVisible] = useState(false); const [isLimitPriceVisible, setIsLimitPriceVisible] = useState(false); const [isOrderTypeVisible, setIsOrderTypeVisible] = useState(false); @@ -552,12 +563,39 @@ const PerpsOrderViewContentBase: React.FC = () => { return; } - setIsTPSLVisible(true); + navigation.navigate(Routes.PERPS.TPSL, { + asset: orderForm.asset, + currentPrice: assetData.price, + direction: orderForm.direction, + leverage: orderForm.leverage, + orderType: orderForm.type, + limitPrice: orderForm.limitPrice, + initialTakeProfitPrice: orderForm.takeProfitPrice, + initialStopLossPrice: orderForm.stopLossPrice, + onConfirm: (takeProfitPrice?: string, stopLossPrice?: string) => { + // Use the same clearing approach as the "Off" button + // If values are undefined or empty, ensure they're cleared properly + const tpToSet = takeProfitPrice || undefined; + const slToSet = stopLossPrice || undefined; + + setTakeProfitPrice(tpToSet); + setStopLossPrice(slToSet); + }, + }); }, [ PerpsToastOptions.formValidation.orderForm.limitPriceRequired, orderForm.limitPrice, orderForm.type, + orderForm.asset, + orderForm.direction, + orderForm.leverage, + orderForm.takeProfitPrice, + orderForm.stopLossPrice, + assetData.price, showToast, + navigation, + setTakeProfitPrice, + setStopLossPrice, ]); const handleAmountPress = () => { @@ -804,7 +842,7 @@ const PerpsOrderViewContentBase: React.FC = () => { ); return ( - + {/* Header */} { )} {/* Fixed Place Order Button - Hide when keypad is active */} {!isInputFocused && ( - + {filteredErrors.length > 0 && ( {filteredErrors.map((error) => ( @@ -1200,30 +1238,6 @@ const PerpsOrderViewContentBase: React.FC = () => { )} - {/* TP/SL Bottom Sheet */} - setIsTPSLVisible(false)} - onConfirm={(takeProfitPrice, stopLossPrice) => { - // Use the same clearing approach as the "Off" button - // If values are undefined or empty, ensure they're cleared properly - const tpToSet = takeProfitPrice || undefined; - const slToSet = stopLossPrice || undefined; - - setTakeProfitPrice(tpToSet); - setStopLossPrice(slToSet); - setIsTPSLVisible(false); - }} - asset={orderForm.asset} - currentPrice={assetData.price} - direction={orderForm.direction} - leverage={orderForm.leverage} - marginRequired={marginRequired} - initialTakeProfitPrice={orderForm.takeProfitPrice} - initialStopLossPrice={orderForm.stopLossPrice} - orderType={orderForm.type} - limitPrice={orderForm.limitPrice} - /> {/* Leverage Selector */} { const { account } = usePerpsLiveAccount(); - const [selectedPosition, setSelectedPosition] = useState( - null, - ); - const [isTPSLVisible, setIsTPSLVisible] = useState(false); - // Get real-time positions via WebSocket const { positions, isInitialLoading } = usePerpsLivePositions({ throttleMs: 1000, // Update every second @@ -52,14 +45,6 @@ const PerpsPositionsView: React.FC = () => { const error = null; - const { handleUpdateTPSL, isUpdating } = usePerpsTPSLUpdate({ - onSuccess: () => { - // Positions update automatically via WebSocket - setIsTPSLVisible(false); - setSelectedPosition(null); - }, - }); - // Memoize position count text to avoid recalculating on every render const positionCountText = useMemo(() => { const positionCount = positions.length; @@ -216,32 +201,6 @@ const PerpsPositionsView: React.FC = () => { {renderContent()} - - {/* TP/SL Bottom Sheet - Rendered outside ScrollView to fix layering issue */} - {isTPSLVisible && selectedPosition && ( - { - setIsTPSLVisible(false); - setSelectedPosition(null); - }} - onConfirm={async (takeProfitPrice, stopLossPrice) => { - await handleUpdateTPSL( - selectedPosition, - takeProfitPrice, - stopLossPrice, - ); - setIsTPSLVisible(false); - setSelectedPosition(null); - }} - asset={selectedPosition.coin} - position={selectedPosition} - initialTakeProfitPrice={selectedPosition.takeProfitPrice} - initialStopLossPrice={selectedPosition.stopLossPrice} - isUpdating={isUpdating} - orderType="market" // Default to market for existing positions - /> - )} ); }; diff --git a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.styles.ts b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts similarity index 90% rename from app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.styles.ts rename to app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts index 3acb0ce2c0ac..d49e0371f3d1 100644 --- a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.styles.ts +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.styles.ts @@ -5,14 +5,24 @@ export const createStyles = (colors: Theme['colors']) => StyleSheet.create({ bottomSheet: {}, container: { - paddingHorizontal: 16, - paddingBottom: 16, + flex: 1, + backgroundColor: colors.background.default, + }, + scrollView: { + flex: 1, }, header: { - paddingBottom: 8, + flexDirection: 'row', + alignItems: 'center', + gap: 16, + paddingHorizontal: 16, + paddingVertical: 16, + borderBottomWidth: 1, + borderBottomColor: colors.border.muted, }, footer: { - paddingBottom: 8, + paddingHorizontal: 16, + paddingBottom: 16, }, priceInfoContainer: { marginTop: 16, @@ -136,7 +146,10 @@ export const createStyles = (colors: Theme['colors']) => marginTop: 12, }, content: { + flexGrow: 1, paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 24, }, description: { marginBottom: 16, @@ -174,7 +187,7 @@ export const createStyles = (colors: Theme['colors']) => borderRadius: 8, }, keypadContainer: { - paddingHorizontal: 8, + paddingHorizontal: 16, paddingTop: 8, backgroundColor: colors.background.default, }, @@ -182,7 +195,8 @@ export const createStyles = (colors: Theme['colors']) => flex: 1, }, keypadFooter: { - paddingHorizontal: 8, + paddingHorizontal: 16, + paddingBottom: 16, width: '100%', }, doneButton: { diff --git a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx similarity index 92% rename from app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx rename to app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx index 3035cdc73642..c9bdd7312d8e 100644 --- a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.test.tsx +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react-native'; -import PerpsTPSLBottomSheet from './PerpsTPSLBottomSheet'; +import PerpsTPSLView from './PerpsTPSLView'; import type { Position } from '../../controllers/types'; // Mock dependencies - only what's absolutely necessary @@ -419,7 +419,7 @@ jest.doMock('react-native', () => ({ })); // Mock styles -jest.mock('./PerpsTPSLBottomSheet.styles', () => ({ +jest.mock('./PerpsTPSLView.styles', () => ({ createStyles: () => ({ container: { padding: 16 }, priceDisplay: { backgroundColor: '#f0f0f0' }, @@ -445,7 +445,7 @@ jest.mock('./PerpsTPSLBottomSheet.styles', () => ({ }), })); -describe('PerpsTPSLBottomSheet', () => { +describe('PerpsTPSLView', () => { const mockTheme = { colors: { background: { alternative: '#f0f0f0' }, @@ -500,7 +500,7 @@ describe('PerpsTPSLBottomSheet', () => { describe('Component Rendering', () => { it('renders when visible', () => { // Act - render(); + render(); // Assert expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); @@ -510,7 +510,7 @@ describe('PerpsTPSLBottomSheet', () => { it('returns null when not visible', () => { // Act - render(); + render(); // Assert expect(screen.queryByText('perps.tpsl.title')).toBeNull(); @@ -520,7 +520,7 @@ describe('PerpsTPSLBottomSheet', () => { it('displays current price for new orders', () => { // Act - render(); + render(); // Assert expect(screen.getByText('perps.tpsl.current_price')).toBeOnTheScreen(); @@ -530,7 +530,7 @@ describe('PerpsTPSLBottomSheet', () => { it('displays entry price when editing existing position', () => { // Act render( - { it('renders percentage buttons with correct RoE values', () => { // Act - render(); + render(); // Assert - Take Profit buttons (RoE percentages) expect(screen.getByText('+10%')).toBeOnTheScreen(); @@ -562,9 +562,7 @@ describe('PerpsTPSLBottomSheet', () => { it('renders without crashing when position is provided', () => { // Arrange & Act - render( - , - ); + render(); // Assert - Component should render successfully with position expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); @@ -572,7 +570,7 @@ describe('PerpsTPSLBottomSheet', () => { it('renders without crashing when leverage prop is provided', () => { // Arrange & Act - render(); + render(); // Assert - Component should render successfully with leverage prop expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); @@ -580,9 +578,7 @@ describe('PerpsTPSLBottomSheet', () => { it('renders without crashing when margin is provided', () => { // Arrange & Act - render( - , - ); + render(); // Assert - Component should render successfully with margin prop expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); @@ -617,7 +613,7 @@ describe('PerpsTPSLBottomSheet', () => { }; // Act - render(); + render(); // Assert const takeProfitInputs = screen.getAllByDisplayValue('$3300.00'); @@ -636,7 +632,7 @@ describe('PerpsTPSLBottomSheet', () => { }; // Act - render(); + render(); // Assert - The hook should have been called with the component // The initial prices are passed as props, so the component should render @@ -660,7 +656,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -684,7 +680,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', )[0]; @@ -707,7 +703,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', )[0]; @@ -731,7 +727,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.profit_roe_placeholder', @@ -755,7 +751,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const stopLossPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -779,7 +775,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const stopLossPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.loss_roe_placeholder', @@ -805,7 +801,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const tenPercentButton = screen.getByText('+10%'); // Act @@ -826,7 +822,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const fivePercentButton = screen.getByText('-5%'); // Act @@ -849,7 +845,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Get the take profit off button - there are two "Off" buttons, get all and find the first one const offButtons = screen.getAllByText('perps.tpsl.off'); @@ -873,7 +869,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Get the stop loss off button - there are two "Off" buttons, get all and find the second one const offButtons = screen.getAllByText('perps.tpsl.off'); @@ -901,7 +897,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Component should display the form state values const takeProfitPriceInput = screen.getAllByPlaceholderText( @@ -930,7 +926,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Component should display the form state values const stopLossPriceInput = screen.getAllByPlaceholderText( @@ -957,7 +953,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Component can handle validation errors // (Note: The component might not display error messages directly, @@ -976,7 +972,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Component renders correctly even with validation errors const confirmButton = screen.getByText('perps.tpsl.done'); @@ -998,7 +994,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Stop loss liquidation error should be displayed expect( @@ -1018,7 +1014,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Stop loss liquidation error should be displayed expect( @@ -1039,7 +1035,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Stop loss error should be displayed (takes precedence in current implementation) expect( @@ -1063,7 +1059,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Regular stop loss error should be displayed expect( @@ -1084,7 +1080,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); // Assert - Stop loss liquidation error should be displayed expect( @@ -1105,7 +1101,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1134,9 +1130,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render( - , - ); + render(); const confirmButton = screen.getByText('perps.tpsl.done'); @@ -1150,9 +1144,7 @@ describe('PerpsTPSLBottomSheet', () => { it('calls onConfirm with undefined for empty values', () => { // Arrange const mockOnConfirm = jest.fn(); - render( - , - ); + render(); const confirmButton = screen.getByText('perps.tpsl.done'); @@ -1166,7 +1158,7 @@ describe('PerpsTPSLBottomSheet', () => { it('does not call onClose immediately after confirm', () => { // Arrange const mockOnClose = jest.fn(); - render(); + render(); const confirmButton = screen.getByText('perps.tpsl.done'); @@ -1186,7 +1178,7 @@ describe('PerpsTPSLBottomSheet', () => { direction: 'short' as const, }; - render(); + render(); // Assert - Should display short-specific labels expect( @@ -1197,14 +1189,14 @@ describe('PerpsTPSLBottomSheet', () => { it('renders RoE percentage buttons for both directions', () => { // Assert - RoE buttons should always be present regardless of direction - render(); + render(); expect(screen.getByText('+10%')).toBeOnTheScreen(); expect(screen.getByText('-5%')).toBeOnTheScreen(); }); it('renders RoE percentage buttons for short direction', () => { // Assert - RoE buttons should always be present regardless of direction - render(); + render(); expect(screen.getByText('+10%')).toBeOnTheScreen(); expect(screen.getByText('-5%')).toBeOnTheScreen(); }); @@ -1220,7 +1212,7 @@ describe('PerpsTPSLBottomSheet', () => { // Act & Assert - Should not crash expect(() => - render(), + render(), ).not.toThrow(); }); @@ -1233,7 +1225,7 @@ describe('PerpsTPSLBottomSheet', () => { // Act & Assert - Should not crash expect(() => - render(), + render(), ).not.toThrow(); }); @@ -1245,7 +1237,7 @@ describe('PerpsTPSLBottomSheet', () => { currentPrice: 3200, // Live price should be displayed }; - render(); + render(); // Assert - Should display current price (live price) expect(screen.getByText('$3200.00')).toBeOnTheScreen(); @@ -1260,7 +1252,7 @@ describe('PerpsTPSLBottomSheet', () => { currentPrice: undefined, // No current price provided - should fall back to entry price }; - render(); + render(); // Assert - Should display position entry price as fallback expect(screen.getAllByText('$2800.00')).toHaveLength(2); @@ -1272,14 +1264,14 @@ describe('PerpsTPSLBottomSheet', () => { describe('Component Memoization', () => { it('renders correctly with different props', () => { // Arrange - const { rerender } = render(); + const { rerender } = render(); // Act - Change a prop const newProps = { ...defaultProps, onClose: jest.fn(), // Different function reference }; - rerender(); + rerender(); // Assert - Component should still render correctly expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); @@ -1287,10 +1279,10 @@ describe('PerpsTPSLBottomSheet', () => { it('re-renders when visibility changes', () => { // Arrange - const { rerender } = render(); + const { rerender } = render(); // Act - rerender(); + rerender(); // Assert - Should render null when not visible expect(screen.queryByText('perps.tpsl.title')).toBeNull(); @@ -1313,7 +1305,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1345,7 +1337,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.profit_roe_placeholder', @@ -1373,7 +1365,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const stopLossPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1405,7 +1397,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const stopLossPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.loss_roe_placeholder', @@ -1425,7 +1417,7 @@ describe('PerpsTPSLBottomSheet', () => { it('hides keypad when input loses focus', () => { // Arrange - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1452,7 +1444,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1478,7 +1470,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.profit_roe_placeholder', @@ -1504,7 +1496,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const stopLossPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1530,7 +1522,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const stopLossPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.loss_roe_placeholder', @@ -1547,7 +1539,7 @@ describe('PerpsTPSLBottomSheet', () => { it('dismisses keypad when tapping outside the input area', () => { // Arrange - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1576,7 +1568,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1601,7 +1593,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPriceInput = screen.getAllByPlaceholderText( 'perps.tpsl.trigger_price_placeholder', @@ -1631,7 +1623,7 @@ describe('PerpsTPSLBottomSheet', () => { }, }); - render(); + render(); const takeProfitPercentInput = screen.getAllByPlaceholderText( 'perps.tpsl.profit_roe_placeholder', @@ -1653,7 +1645,7 @@ describe('PerpsTPSLBottomSheet', () => { // Arrange mockPlatform.OS = 'ios'; - render(); + render(); // Assert - Component should render without issues on iOS expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); @@ -1663,7 +1655,7 @@ describe('PerpsTPSLBottomSheet', () => { // Arrange mockPlatform.OS = 'android'; - render(); + render(); // Assert - Component should render without issues on Android expect(screen.getByText('perps.tpsl.title')).toBeOnTheScreen(); diff --git a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx similarity index 86% rename from app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx rename to app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx index 4be91c743926..b4f6b494ead4 100644 --- a/app/components/UI/Perps/components/PerpsTPSLBottomSheet/PerpsTPSLBottomSheet.tsx +++ b/app/components/UI/Perps/Views/PerpsTPSLView/PerpsTPSLView.tsx @@ -1,15 +1,26 @@ -import React, { memo, useCallback, useEffect, useRef, useState } from 'react'; -import { ScrollView, TextInput, TouchableOpacity, View } from 'react-native'; +import React, { memo, useCallback, useRef, useState } from 'react'; +import { + Keyboard, + ScrollView, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { strings } from '../../../../../../locales/i18n'; -import BottomSheet, { - BottomSheetRef, -} from '../../../../../component-library/components/BottomSheets/BottomSheet'; -import BottomSheetFooter from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; -import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; import Button, { ButtonSize, ButtonVariants, + ButtonWidthTypes, } from '../../../../../component-library/components/Buttons/Button'; +import ButtonIcon, { + ButtonIconSizes, +} from '../../../../../component-library/components/Buttons/ButtonIcon'; +import { + IconColor, + IconName, +} from '../../../../../component-library/components/Icons/Icon'; import Text, { TextColor, TextVariant, @@ -22,16 +33,16 @@ import { PerpsEventProperties, PerpsEventValues, } from '../../constants/eventNames'; -import type { Position } from '../../controllers/types'; import { usePerpsLivePrices } from '../../hooks/stream'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; +import type { PerpsNavigationParamList } from '../../types/navigation'; import { - getPerpsTPSLBottomSheetSelector, - PerpsTPSLBottomSheetSelectorsIDs, + getPerpsTPSLViewSelector, + PerpsTPSLViewSelectorsIDs, } from '../../../../../../e2e/selectors/Perps/Perps.selectors'; import { usePerpsTPSLForm } from '../../hooks/usePerpsTPSLForm'; import { usePerpsLiquidationPrice } from '../../hooks/usePerpsLiquidationPrice'; -import { createStyles } from './PerpsTPSLBottomSheet.styles'; +import { createStyles } from './PerpsTPSLView.styles'; import { formatPerpsFiat, PRICE_RANGES_UNIVERSAL, @@ -41,42 +52,27 @@ import { const TAKE_PROFIT_PERCENTAGES = [10, 25, 50, 100]; // +10%, +25%, +50%, +100% RoE const STOP_LOSS_PERCENTAGES = [-5, -10, -25, -50]; // -5%, -10%, -25%, -50% RoE -interface PerpsTPSLBottomSheetProps { - isVisible: boolean; - onClose: () => void; - onConfirm: (takeProfitPrice?: string, stopLossPrice?: string) => void; - // Context data - asset: string; - currentPrice?: number; - direction?: 'long' | 'short'; // For new orders - position?: Position; // For existing positions - initialTakeProfitPrice?: string; - initialStopLossPrice?: string; - isUpdating?: boolean; - leverage?: number; // For new orders - marginRequired?: string; // For new orders - orderType?: 'market' | 'limit'; // Order type for new orders - limitPrice?: string; // Limit price for limit orders -} - -const PerpsTPSLBottomSheet: React.FC = ({ - isVisible, - onClose, - onConfirm, - asset, - currentPrice: initialCurrentPrice, - direction, - position, - initialTakeProfitPrice, - initialStopLossPrice, - isUpdating = false, - leverage: propLeverage, - orderType, - limitPrice, -}) => { +const PerpsTPSLView: React.FC = () => { + const navigation = useNavigation(); + const route = useRoute>(); + + // Extract params from navigation route + const { + asset, + currentPrice: initialCurrentPrice, + direction, + position, + initialTakeProfitPrice, + initialStopLossPrice, + leverage: propLeverage, + orderType, + limitPrice, + onConfirm, + } = route.params; + + const [isUpdating, setIsUpdating] = useState(false); const { colors } = useTheme(); const styles = createStyles(colors); - const bottomSheetRef = useRef(null); const scrollViewRef = useRef(null); // Keypad state management @@ -88,10 +84,10 @@ const PerpsTPSLBottomSheet: React.FC = ({ const stopLossPriceRef = useRef(null); const stopLossPercentageRef = useRef(null); - // Subscribe to real-time price only when visible and we have an asset + // Subscribe to real-time price only when we have an asset // Use 1s debounce for TP/SL bottom sheet const priceData = usePerpsLivePrices({ - symbols: isVisible && asset ? [asset] : [], + symbols: asset ? [asset] : [], throttleMs: 1000, }); const livePrice = priceData[asset]?.price @@ -157,7 +153,7 @@ const PerpsTPSLBottomSheet: React.FC = ({ initialStopLossPrice, leverage: propLeverage, entryPrice: effectiveEntryPrice, - isVisible, + isVisible: true, liquidationPrice: displayLiquidationPrice, orderType, }); @@ -204,7 +200,7 @@ const PerpsTPSLBottomSheet: React.FC = ({ usePerpsEventTracking({ eventName: MetaMetricsEvents.PERPS_SCREEN_VIEWED, - conditions: [isVisible], + conditions: [true], properties: { [PerpsEventProperties.SCREEN_TYPE]: PerpsEventValues.SCREEN_TYPE.TP_SL, [PerpsEventProperties.ASSET]: asset, @@ -215,16 +211,10 @@ const PerpsTPSLBottomSheet: React.FC = ({ }, }); - useEffect(() => { - if (isVisible) { - bottomSheetRef.current?.onOpenBottomSheet(); - } - }, [isVisible]); - - // Handle close without saving - const handleClose = useCallback(() => { - onClose(); - }, [onClose]); + // Handle back button press + const handleBack = useCallback(() => { + navigation.goBack(); + }, [navigation]); // Keypad handlers const handleKeypadChange = useCallback( @@ -316,7 +306,7 @@ const PerpsTPSLBottomSheet: React.FC = ({ setFocusedInput(null); }, [focusedInput]); - const handleConfirm = useCallback(() => { + const handleConfirm = useCallback(async () => { if (focusedInput) { dismissKeypad(); } @@ -330,39 +320,48 @@ const PerpsTPSLBottomSheet: React.FC = ({ ? stopLossPrice.replace(/[$,]/g, '') : undefined; - onConfirm(parseTakeProfitPrice, parseStopLossPrice); - // Don't close immediately - let the parent handle closing after update completes - }, [focusedInput, takeProfitPrice, stopLossPrice, onConfirm, dismissKeypad]); + setIsUpdating(true); + try { + await onConfirm(parseTakeProfitPrice, parseStopLossPrice); + navigation.goBack(); + } finally { + setIsUpdating(false); + } + }, [ + focusedInput, + takeProfitPrice, + stopLossPrice, + onConfirm, + dismissKeypad, + navigation, + ]); const confirmDisabled = !hasChanges || !isValid || isUpdating; const inputsDisabled = isUpdating; - if (!isVisible) return null; - return ( - - - + + {/* Simple header with back button and title */} + + + {strings('perps.tpsl.title')} - - - { - if (focusedInput) { - dismissKeypad(); - } - }} - > + + + {/* Description text */} {!focusedInput && ( = ({ styles.percentageButtonActiveTP, ]} onPress={() => handleTakeProfitPercentageButton(percentage)} - testID={getPerpsTPSLBottomSheetSelector.takeProfitPercentageButton( + testID={getPerpsTPSLViewSelector.takeProfitPercentageButton( percentage, )} disabled={inputsDisabled} @@ -607,7 +606,7 @@ const PerpsTPSLBottomSheet: React.FC = ({ styles.percentageButtonActiveSL, ]} onPress={() => handleStopLossPercentageButton(percentage)} - testID={getPerpsTPSLBottomSheetSelector.stopLossPercentageButton( + testID={getPerpsTPSLViewSelector.stopLossPercentageButton( percentage, )} disabled={inputsDisabled} @@ -710,7 +709,7 @@ const PerpsTPSLBottomSheet: React.FC = ({ )} - + @@ -746,22 +745,22 @@ const PerpsTPSLBottomSheet: React.FC = ({ ) : ( - + +