Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -11,7 +11,13 @@ import React, {
useEffect,
useRef,
} from 'react';
import { SafeAreaView, ScrollView, View, RefreshControl } from 'react-native';
import {
SafeAreaView,
ScrollView,
View,
RefreshControl,
Linking,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { strings } from '../../../../../../locales/i18n';
import Button, {
Expand Down Expand Up @@ -53,9 +59,7 @@ import {
PerpsEventProperties,
PerpsEventValues,
} from '../../constants/eventNames';
import { useSelector } from 'react-redux';
import { selectPerpsProvider } from '../../selectors/perpsController';
import { capitalize } from '../../../../../util/general';

import {
usePerpsAccount,
usePerpsConnection,
Expand Down Expand Up @@ -104,8 +108,6 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
const [activeTabId, setActiveTabId] = useState('position');
const [refreshing, setRefreshing] = useState(false);

const perpsProvider = useSelector(selectPerpsProvider);

const account = usePerpsAccount();

usePerpsConnection();
Expand Down Expand Up @@ -284,6 +286,12 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
});
};

const handleTradingViewPress = useCallback(() => {
Linking.openURL('https://www.tradingview.com/').catch((error) => {
console.error('Failed to open Trading View URL:', error);
});
}, []);

// Determine if any action buttons will be visible
const hasLongShortButtons = useMemo(
() => !isLoadingPosition && !hasZeroBalance,
Expand Down Expand Up @@ -387,9 +395,14 @@ const PerpsMarketDetailsView: React.FC<PerpsMarketDetailsViewProps> = () => {
variant={TextVariant.BodyXS}
color={TextColor.Alternative}
>
{strings('perps.risk_disclaimer', {
provider: capitalize(perpsProvider),
})}
{strings('perps.risk_disclaimer')}{' '}
<Text
variant={TextVariant.BodyXS}
color={TextColor.Alternative}
onPress={handleTradingViewPress}
>
Trading View
</Text>
</Text>
</View>
</ScrollView>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,29 @@ const PerpsMarketListView = ({
};

const filteredMarkets = useMemo(() => {
// First filter out markets with no volume or $0 volume
const marketsWithVolume = markets.filter((market: PerpsMarketData) => {
// Check if volume exists and is not zero
if (
!market.volume ||
market.volume === '$0' ||
market.volume === '$0.00'
) {
return false;
}
// Also filter out fallback display values
if (market.volume === '$---' || market.volume === '---') {
return false;
}
return true;
});

// Then apply search filter if needed
if (!searchQuery.trim()) {
return markets;
return marketsWithVolume;
}
const query = searchQuery.toLowerCase().trim();
return markets.filter(
return marketsWithVolume.filter(
(market: PerpsMarketData) =>
market.symbol.toLowerCase().includes(query) ||
market.name.toLowerCase().includes(query),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ const PerpsOrderViewContentBase: React.FC = () => {
{/* Amount Display */}
<PerpsAmountDisplay
amount={orderForm.amount}
maxAmount={availableBalance}
maxAmount={availableBalance * orderForm.leverage}
showWarning={availableBalance === 0}
onPress={handleAmountPress}
isActive={isInputFocused}
Expand All @@ -663,7 +663,7 @@ const PerpsOrderViewContentBase: React.FC = () => {
value={parseFloat(orderForm.amount || '0')}
onValueChange={(value) => setAmount(Math.floor(value).toString())}
minimumValue={0}
maximumValue={availableBalance}
maximumValue={availableBalance * orderForm.leverage}
step={1}
showPercentageLabels
/>
Expand Down
88 changes: 88 additions & 0 deletions app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useNavigation } from '@react-navigation/native';
import { act, fireEvent, render, screen } from '@testing-library/react-native';
import React from 'react';
import { useSelector } from 'react-redux';
import Routes from '../../../../../constants/navigation/Routes';
import { strings } from '../../../../../../locales/i18n';
import type { Position } from '../../controllers/types';
Expand All @@ -12,6 +13,20 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));

// Mock Redux
jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));

// Mock the multichain selector
jest.mock('../../../../../selectors/multichainAccounts/accounts', () => ({
selectSelectedInternalAccountByScope: jest.fn(() => () => ({
address: '0x1234567890123456789012345678901234567890',
id: 'mock-account-id',
type: 'eip155:eoa',
})),
}));

// Mock PerpsConnectionProvider
jest.mock('../../providers/PerpsConnectionProvider', () => ({
PerpsConnectionProvider: ({ children }: { children: React.ReactNode }) =>
Expand Down Expand Up @@ -126,10 +141,20 @@ describe('PerpsTabView', () => {
jest.clearAllMocks();
(useNavigation as jest.Mock).mockReturnValue(mockNavigation);

// Mock useSelector for the multichain selector
(useSelector as jest.Mock).mockImplementation(() => () => ({
address: '0x1234567890123456789012345678901234567890',
id: 'mock-account-id',
type: 'eip155:eoa',
}));

// Default hook mocks
mockUsePerpsConnection.mockReturnValue({
isConnected: true,
isInitialized: true,
error: null,
connect: jest.fn(),
resetError: jest.fn(),
});

mockUsePerpsLivePositions.mockReturnValue({
Expand Down Expand Up @@ -477,6 +502,69 @@ describe('PerpsTabView', () => {

consoleSpy.mockRestore();
});

it('should render connection error state when connection fails', () => {
mockUsePerpsConnection.mockReturnValue({
isConnected: false,
isInitialized: false,
error: 'CONNECTION_FAILED',
connect: jest.fn(),
resetError: jest.fn(),
});

render(<PerpsTabView />);

// Should show connection failed error
expect(
screen.getByText(strings('perps.errors.connectionFailed.title')),
).toBeOnTheScreen();
expect(
screen.getByText(strings('perps.errors.connectionFailed.description')),
).toBeOnTheScreen();
});

it('should render network error state when network error occurs', () => {
mockUsePerpsConnection.mockReturnValue({
isConnected: false,
isInitialized: false,
error: 'NETWORK_ERROR',
connect: jest.fn(),
resetError: jest.fn(),
});

render(<PerpsTabView />);

// Should show connection failed error (PerpsTabView always uses CONNECTION_FAILED)
expect(
screen.getByText(strings('perps.errors.connectionFailed.title')),
).toBeOnTheScreen();
expect(
screen.getByText(strings('perps.errors.connectionFailed.description')),
).toBeOnTheScreen();
});

it('should call connect when retry button is pressed on error', () => {
const mockConnect = jest.fn();
const mockResetError = jest.fn();

mockUsePerpsConnection.mockReturnValue({
isConnected: false,
isInitialized: false,
error: 'CONNECTION_FAILED',
connect: mockConnect,
resetError: mockResetError,
});

render(<PerpsTabView />);

const retryButton = screen.getByText(
strings('perps.errors.connectionFailed.retry'),
);
fireEvent.press(retryButton);

expect(mockResetError).toHaveBeenCalledTimes(1);
expect(mockConnect).toHaveBeenCalledTimes(1);
});
});

describe('Accessibility', () => {
Expand Down
36 changes: 31 additions & 5 deletions app/components/UI/Perps/Views/PerpsTabView/PerpsTabView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useNavigation, type NavigationProp } from '@react-navigation/native';
import React, { useCallback, useEffect, useRef } from 'react';
import { ScrollView, View } from 'react-native';
import { useSelector } from 'react-redux';
import { strings } from '../../../../../../locales/i18n';
import Button, {
ButtonSize,
Expand All @@ -21,6 +22,9 @@ import Routes from '../../../../../constants/navigation/Routes';
import { MetaMetricsEvents } from '../../../../hooks/useMetrics';
import PerpsPositionCard from '../../components/PerpsPositionCard';
import { PerpsTabControlBar } from '../../components/PerpsTabControlBar';
import PerpsErrorState, {
PerpsErrorType,
} from '../../components/PerpsErrorState';
import {
PerpsEventProperties,
PerpsEventValues,
Expand All @@ -36,15 +40,20 @@ import {
usePerpsPerformance,
usePerpsLivePositions,
} from '../../hooks';
import { selectSelectedInternalAccountByScope } from '../../../../../selectors/multichainAccounts/accounts';
import styleSheet from './PerpsTabView.styles';

interface PerpsTabViewProps {}

const PerpsTabView: React.FC<PerpsTabViewProps> = () => {
const { styles } = useStyles(styleSheet, {});
const navigation = useNavigation<NavigationProp<PerpsNavigationParamList>>();
const selectedEvmAccount = useSelector(selectSelectedInternalAccountByScope)(
'eip155:1',
);
const { getAccountState } = usePerpsTrading();
const { isConnected, isInitialized } = usePerpsConnection();
const { isConnected, isInitialized, error, connect, resetError } =
usePerpsConnection();
const { track } = usePerpsEventTracking();
const cachedAccountState = usePerpsAccount();

Expand All @@ -64,15 +73,15 @@ const PerpsTabView: React.FC<PerpsTabViewProps> = () => {
startMeasure(PerpsMeasurementName.POSITION_DATA_LOADED_PERP_TAB);
}, [startMeasure]);

// Automatically load account state on mount and when network changes
// Automatically load account state on mount and when network or account changes
useEffect(() => {
// Only load account state if we're connected and initialized
if (isConnected && isInitialized) {
// Only load account state if we're connected, initialized, and have an EVM account
if (isConnected && isInitialized && selectedEvmAccount) {
// Fire and forget - errors are already handled in getAccountState
// and stored in the controller's state
getAccountState();
}
}, [getAccountState, isConnected, isInitialized]);
}, [getAccountState, isConnected, isInitialized, selectedEvmAccount]);

// Track homescreen tab viewed - only once when positions and account are loaded
useEffect(() => {
Expand Down Expand Up @@ -123,6 +132,11 @@ const PerpsTabView: React.FC<PerpsTabViewProps> = () => {
});
}, [navigation]);

const handleRetryConnection = useCallback(() => {
resetError();
connect();
}, [connect, resetError]);

const renderPositionsSection = () => {
if (isInitialLoading) {
return (
Expand Down Expand Up @@ -208,6 +222,18 @@ const PerpsTabView: React.FC<PerpsTabViewProps> = () => {
);
};

// Check for connection errors
if (error && !isConnected && selectedEvmAccount) {
return (
<View style={styles.wrapper}>
<PerpsErrorState
errorType={PerpsErrorType.CONNECTION_FAILED}
onRetry={handleRetryConnection}
/>
</View>
);
}

return (
<View style={styles.wrapper}>
{isFirstTimeUser ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import Routes from '../../../../../constants/navigation/Routes';
import PerpsFundingTransactionView from './PerpsFundingTransactionView';
import { PerpsTransactionSelectorsIDs } from '../../../../../../e2e/selectors/Perps/Perps.selectors';
Expand All @@ -23,7 +24,6 @@ const mockTransaction = {
// Mock all dependencies properly
const mockUseNavigation = jest.fn();
const mockUseRoute = jest.fn();
const mockUseSelector = jest.fn();
const mockUsePerpsNetwork = jest.fn();
const mockUsePerpsBlockExplorerUrl = jest.fn();
const mockGetHyperliquidExplorerUrl = jest.fn();
Expand All @@ -37,7 +37,13 @@ jest.mock('@react-navigation/native', () => ({
}));

jest.mock('react-redux', () => ({
useSelector: () => mockUseSelector(),
useSelector: jest.fn(),
}));

jest.mock('../../../../../selectors/multichainAccounts/accounts', () => ({
selectSelectedInternalAccountByScope: jest.fn(() => () => ({
address: '0x1234567890abcdef1234567890abcdef12345678',
})),
}));

jest.mock('../../hooks', () => ({
Expand Down Expand Up @@ -69,9 +75,10 @@ describe('PerpsFundingTransactionView', () => {
),
baseExplorerUrl: 'https://app.hyperliquid.xyz/explorer',
});
mockUseSelector.mockReturnValue({
// Mock useSelector to return a function that returns the account
(useSelector as jest.Mock).mockImplementation(() => () => ({
address: '0x1234567890abcdef1234567890abcdef12345678',
});
}));
mockUseRoute.mockReturnValue({
params: { transaction: mockTransaction },
});
Expand Down Expand Up @@ -263,7 +270,8 @@ describe('PerpsFundingTransactionView', () => {
setOptions: jest.fn(),
});

mockUseSelector.mockReturnValue(null);
// Mock useSelector to return null for no account
(useSelector as jest.Mock).mockImplementationOnce(() => () => null);

const { getByTestId } = render(<PerpsFundingTransactionView />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import Text, {
TextVariant,
} from '../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../component-library/hooks';
import { selectSelectedInternalAccount } from '../../../../../selectors/accountsController';
import { selectSelectedInternalAccountByScope } from '../../../../../selectors/multichainAccounts/accounts';
import ScreenView from '../../../../Base/ScreenView';
import { getPerpsTransactionsDetailsNavbar } from '../../../Navbar';
import { usePerpsBlockExplorerUrl } from '../../hooks';
Expand All @@ -40,7 +40,9 @@ const PerpsFundingTransactionView: React.FC = () => {
const navigation = useNavigation<NavigationProp<PerpsNavigationParamList>>();
const route = useRoute<PerpsFundingTransactionRouteProp>();

const selectedInternalAccount = useSelector(selectSelectedInternalAccount);
const selectedInternalAccount = useSelector(
selectSelectedInternalAccountByScope,
)('eip155:1');
const { getExplorerUrl } = usePerpsBlockExplorerUrl();

// Get transaction from route params
Expand Down
Loading
Loading