Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
28be891
refactor(perps): migrate tpsl bottomsheet to fullscreen view
abretonc7s Oct 17, 2025
1758a1b
Merge branch 'main' into fix/perps/modalbottomsheetrefactor
abretonc7s Oct 17, 2025
c8d1d0a
fix: unit tests
abretonc7s Oct 17, 2025
9a915c0
Merge remote-tracking branch 'origin/main' into fix/perps/modalbottom…
abretonc7s Oct 17, 2025
4a7737e
fix: pr comments
abretonc7s Oct 17, 2025
9966afe
Merge branch 'main' into fix/perps/modalbottomsheetrefactor
abretonc7s Oct 18, 2025
e8ed0f2
feat(perps): new TPSL view design
abretonc7s Oct 18, 2025
54f73c4
fix: unit tests
abretonc7s Oct 18, 2025
c8a44a2
fix: unit tests
abretonc7s Oct 19, 2025
fdde0ef
coverage
abretonc7s Oct 19, 2025
fe9894c
fix: unit tests
abretonc7s Oct 19, 2025
3f7273e
fix: pr comments
abretonc7s Oct 20, 2025
b929f0c
fix: sonarcloud
abretonc7s Oct 20, 2025
75a2e10
feat: coverage
abretonc7s Oct 20, 2025
b7f2c32
cleanup
abretonc7s Oct 20, 2025
6621ff8
bugbot
abretonc7s Oct 20, 2025
7c6658a
Merge branch 'main' into fix/perps/modalbottomsheetrefactor
abretonc7s Oct 20, 2025
f19998d
Merge branch 'fix/perps/modalbottomsheetrefactor' into feat/perps/new…
abretonc7s Oct 20, 2025
134cdd7
Merge remote-tracking branch 'origin/main' into feat/perps/newtpsl
abretonc7s Oct 21, 2025
b9ba42f
Merge remote-tracking branch 'origin/main' into feat/perps/newtpsl
abretonc7s Oct 21, 2025
a9f8de0
fix: styling and order computation
abretonc7s Oct 21, 2025
cb0adbc
fix: sonarcloud
abretonc7s Oct 21, 2025
d62a8ed
cleanup
abretonc7s Oct 21, 2025
9cfee99
cleanup
abretonc7s Oct 21, 2025
5b5e5a7
fix: bug bot
abretonc7s Oct 21, 2025
b9983fa
cleanup short expected profit
abretonc7s Oct 21, 2025
c9678b8
cleanup
abretonc7s Oct 21, 2025
13894c4
merge main into branch
abretonc7s Oct 21, 2025
4ac0c9d
Merge branch 'main' into feat/perps/newtpsl
gambinish Oct 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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) =>
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ jest.mock('../../../../../util/networks', () => ({
getNetworkImageSource: jest.fn(() => ({ uri: 'network-icon' })),
}));

// Consolidated mock for all bottom sheet components
// Consolidated mock for bottom sheet components (not PerpsTPSLView - it's now a navigation screen)
const createBottomSheetMock = (testId: string) => {
const MockReact = jest.requireActual('react');
return {
Expand All @@ -438,9 +438,6 @@ const createBottomSheetMock = (testId: string) => {
};
};

jest.mock('../../components/PerpsTPSLBottomSheet', () =>
createBottomSheetMock('tpsl-bottom-sheet'),
);
jest.mock('../../components/PerpsLeverageBottomSheet', () =>
createBottomSheetMock('leverage-bottom-sheet'),
);
Expand Down Expand Up @@ -1775,11 +1772,16 @@ describe('PerpsOrderView', () => {
expect(mockShowToast).toHaveBeenCalledTimes(1);
expect(mockShowToast).toHaveBeenCalledWith(mockLimitPriceRequiredToast);

// Verify that the TP/SL bottom sheet was NOT opened
expect(screen.queryByTestId('tpsl-bottom-sheet')).toBeNull();
// Verify that navigation to TP/SL screen was NOT triggered
expect(mockNavigate).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
asset: expect.anything(),
}),
);
});

it('opens TP/SL bottom sheet on limit order with limit price', async () => {
it('navigates to TP/SL screen on limit order with limit price', async () => {
// Set up route params
(useRoute as jest.Mock).mockReturnValue({
params: {
Expand Down Expand Up @@ -1863,9 +1865,18 @@ describe('PerpsOrderView', () => {
);
fireEvent.press(tpSlButton);

// Verify that TP/SL bottom sheet IS shown
// Verify that navigation to TP/SL screen was triggered
await waitFor(() => {
expect(screen.getByTestId('tpsl-bottom-sheet')).toBeDefined();
expect(mockNavigate).toHaveBeenCalledWith(
expect.stringContaining('TPSL'),
expect.objectContaining({
asset: 'ETH',
direction: 'long',
leverage: 3,
orderType: 'limit',
limitPrice: '3100',
}),
);
});
});
});
Expand Down
74 changes: 44 additions & 30 deletions app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -138,9 +140,19 @@ interface OrderRouteParams {
const PerpsOrderViewContentBase: React.FC = () => {
const navigation = useNavigation<NavigationProp<PerpsNavigationParamList>>();
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<PerpsTooltipContentKey | null>(null);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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: async (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 = () => {
Expand Down Expand Up @@ -804,7 +842,7 @@ const PerpsOrderViewContentBase: React.FC = () => {
);

return (
<SafeAreaView style={styles.container}>
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
{/* Header */}
<PerpsOrderHeader
asset={orderForm.asset}
Expand Down Expand Up @@ -1164,7 +1202,7 @@ const PerpsOrderViewContentBase: React.FC = () => {
)}
{/* Fixed Place Order Button - Hide when keypad is active */}
{!isInputFocused && (
<View style={styles.fixedBottomContainer}>
<View style={fixedBottomContainerStyle}>
{filteredErrors.length > 0 && (
<View style={styles.validationContainer}>
{filteredErrors.map((error) => (
Expand Down Expand Up @@ -1200,30 +1238,6 @@ const PerpsOrderViewContentBase: React.FC = () => {
</ButtonSemantic>
</View>
)}
{/* TP/SL Bottom Sheet */}
<PerpsTPSLBottomSheet
isVisible={isTPSLVisible}
onClose={() => 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 */}
<PerpsLeverageBottomSheet
isVisible={isLeverageVisible}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
type NavigationProp,
type ParamListBase,
} from '@react-navigation/native';
import React, { useMemo, useState } from 'react';
import React, { useMemo } from 'react';
import { ScrollView, View } from 'react-native';
import { strings } from '../../../../../../locales/i18n';
import ButtonIcon, {
Expand All @@ -19,9 +19,7 @@ import Text, {
} from '../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../component-library/hooks';
import PerpsPositionCard from '../../components/PerpsPositionCard';
import PerpsTPSLBottomSheet from '../../components/PerpsTPSLBottomSheet';
import type { Position } from '../../controllers/types';
import { usePerpsLivePositions, usePerpsTPSLUpdate } from '../../hooks';
import { usePerpsLivePositions } from '../../hooks';
import { usePerpsLiveAccount } from '../../hooks/stream';
import {
formatPnl,
Expand All @@ -40,26 +38,13 @@ const PerpsPositionsView: React.FC = () => {

const { account } = usePerpsLiveAccount();

const [selectedPosition, setSelectedPosition] = useState<Position | null>(
null,
);
const [isTPSLVisible, setIsTPSLVisible] = useState(false);

// Get real-time positions via WebSocket
const { positions, isInitialLoading } = usePerpsLivePositions({
throttleMs: 1000, // Update every second
});

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;
Expand Down Expand Up @@ -216,32 +201,6 @@ const PerpsPositionsView: React.FC = () => {
</View>
{renderContent()}
</ScrollView>

{/* TP/SL Bottom Sheet - Rendered outside ScrollView to fix layering issue */}
{isTPSLVisible && selectedPosition && (
<PerpsTPSLBottomSheet
isVisible
onClose={() => {
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
/>
)}
</SafeAreaView>
);
};
Expand Down
Loading
Loading