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
92 changes: 55 additions & 37 deletions app/components/UI/Perps/hooks/usePerpsBalanceTokenFilter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react-native';
import { renderHook, waitFor } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import { TransactionType } from '@metamask/transaction-controller';
import { usePerpsBalanceTokenFilter } from './usePerpsBalanceTokenFilter';
Expand All @@ -10,8 +10,12 @@ import {
isHighlightedItemOutsideAssetList,
} from '../../../Views/confirmations/types/token';
import { usePerpsTrading } from './usePerpsTrading';
import { useConfirmNavigation } from '../../../Views/confirmations/hooks/useConfirmNavigation';
import { usePerpsPaymentToken } from './usePerpsPaymentToken';
import Routes from '../../../../constants/navigation/Routes';
import { useNavigation } from '@react-navigation/native';
import useApprovalRequest from '../../../Views/confirmations/hooks/useApprovalRequest';
import { selectPerpsAccountState } from '../selectors/perpsController';
import { selectPerpsPayWithAnyTokenAllowlistAssets } from '../selectors/featureFlags';

jest.mock('../../../../../locales/i18n', () => ({
strings: jest.fn((key: string) => key),
Expand All @@ -22,8 +26,12 @@ jest.mock(
);
jest.mock('./useIsPerpsBalanceSelected');
jest.mock('./usePerpsTrading');
jest.mock('../../../Views/confirmations/hooks/useConfirmNavigation', () => ({
useConfirmNavigation: jest.fn(),
jest.mock('../../../Views/confirmations/hooks/useApprovalRequest', () => ({
__esModule: true,
default: jest.fn(),
}));
jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));
jest.mock('./usePerpsPaymentToken');
jest.mock('./usePerpsNetworkManagement', () => ({
Expand Down Expand Up @@ -58,17 +66,21 @@ const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
const mockUsePerpsTrading = usePerpsTrading as jest.MockedFunction<
typeof usePerpsTrading
>;
const mockUseConfirmNavigation = useConfirmNavigation as jest.MockedFunction<
typeof useConfirmNavigation
>;
const mockUsePerpsPaymentToken = usePerpsPaymentToken as jest.MockedFunction<
typeof usePerpsPaymentToken
>;
const mockUseNavigation = useNavigation as jest.MockedFunction<
typeof useNavigation
>;
const mockUseApprovalRequest = useApprovalRequest as jest.MockedFunction<
typeof useApprovalRequest
>;

describe('usePerpsBalanceTokenFilter', () => {
const chainId = '0xa4b1';
const mockDepositWithConfirmation = jest.fn().mockResolvedValue(undefined);
const mockNavigateToConfirmation = jest.fn();
const mockNavigate = jest.fn();
const mockOnReject = jest.fn();
const mockOnPerpsPaymentTokenChange = jest.fn();

beforeEach(() => {
Expand All @@ -77,10 +89,10 @@ describe('usePerpsBalanceTokenFilter', () => {
mockUseIsPerpsBalanceSelected.mockReturnValue(false);
mockUseSelector.mockImplementation(
(selector: (state: unknown) => unknown) => {
if (selector.name === 'selectPerpsAccountState') {
if (selector === selectPerpsAccountState) {
return { availableBalance: '1500.00' };
}
if (selector.name === 'selectPerpsPayWithAnyTokenAllowlistAssets') {
if (selector === selectPerpsPayWithAnyTokenAllowlistAssets) {
return [];
}
return undefined;
Expand All @@ -89,9 +101,12 @@ describe('usePerpsBalanceTokenFilter', () => {
mockUsePerpsTrading.mockReturnValue({
depositWithConfirmation: mockDepositWithConfirmation,
} as unknown as ReturnType<typeof usePerpsTrading>);
mockUseConfirmNavigation.mockReturnValue({
navigateToConfirmation: mockNavigateToConfirmation,
} as unknown as ReturnType<typeof useConfirmNavigation>);
mockUseNavigation.mockReturnValue({
navigate: mockNavigate,
} as unknown as ReturnType<typeof useNavigation>);
mockUseApprovalRequest.mockReturnValue({
onReject: mockOnReject,
} as unknown as ReturnType<typeof useApprovalRequest>);
mockUsePerpsPaymentToken.mockReturnValue({
onPaymentTokenChange: mockOnPerpsPaymentTokenChange,
} as unknown as ReturnType<typeof usePerpsPaymentToken>);
Expand Down Expand Up @@ -200,9 +215,8 @@ describe('usePerpsBalanceTokenFilter', () => {
it('uses zero balance when perps account is null', () => {
mockUseSelector.mockImplementation(
(selector: (state: unknown) => unknown) => {
if (selector.name === 'selectPerpsAccountState') return null;
if (selector.name === 'selectPerpsPayWithAnyTokenAllowlistAssets')
return [];
if (selector === selectPerpsAccountState) return null;
if (selector === selectPerpsPayWithAnyTokenAllowlistAssets) return [];
return undefined;
},
);
Expand All @@ -222,10 +236,9 @@ describe('usePerpsBalanceTokenFilter', () => {
it('clears isSelected on other tokens when perps balance is selected', () => {
mockUseSelector.mockImplementation(
(selector: (state: unknown) => unknown) => {
if (selector.name === 'selectPerpsAccountState')
if (selector === selectPerpsAccountState)
return { availableBalance: '1500.00' };
if (selector.name === 'selectPerpsPayWithAnyTokenAllowlistAssets')
return [];
if (selector === selectPerpsPayWithAnyTokenAllowlistAssets) return [];
return undefined;
},
);
Expand Down Expand Up @@ -255,10 +268,9 @@ describe('usePerpsBalanceTokenFilter', () => {
it('keeps token isSelected when perps balance is not selected', () => {
mockUseSelector.mockImplementation(
(selector: (state: unknown) => unknown) => {
if (selector.name === 'selectPerpsAccountState')
if (selector === selectPerpsAccountState)
return { availableBalance: '1500.00' };
if (selector.name === 'selectPerpsPayWithAnyTokenAllowlistAssets')
return [];
if (selector === selectPerpsPayWithAnyTokenAllowlistAssets) return [];
return undefined;
},
);
Expand All @@ -280,14 +292,15 @@ describe('usePerpsBalanceTokenFilter', () => {

it('filters to only allowlisted tokens when allowlist is set', () => {
const allowlistKey = `${chainId}.0xusdc`.toLowerCase();
let callIndex = 0;
mockUseSelector.mockImplementation(() => {
callIndex += 1;
// Hook calls selectPerpsAccountState first, then selectPerpsPayWithAnyTokenAllowlistAssets
if (callIndex === 1) return { availableBalance: '100.00' };
if (callIndex === 2) return [allowlistKey];
return [];
});
mockUseSelector.mockImplementation(
(selector: (state: unknown) => unknown) => {
if (selector === selectPerpsAccountState)
return { availableBalance: '100.00' };
if (selector === selectPerpsPayWithAnyTokenAllowlistAssets)
return [allowlistKey];
return [];
},
);
const inputTokens: AssetType[] = [
{
address: '0xusdc',
Expand All @@ -314,7 +327,7 @@ describe('usePerpsBalanceTokenFilter', () => {
expect((output[1] as AssetType).symbol).toBe('USDC');
});

it('calls navigateToConfirmation and depositWithConfirmation when Add funds is pressed', async () => {
it('calls onReject, depositWithConfirmation and navigation.navigate when Add funds is pressed', async () => {
mockUseSelector.mockReturnValue({
availableBalance: '500.00',
});
Expand All @@ -336,12 +349,17 @@ describe('usePerpsBalanceTokenFilter', () => {
expect(isHighlightedItemOutsideAssetList(highlightedAction)).toBe(true);
if (isHighlightedItemOutsideAssetList(highlightedAction)) {
highlightedAction.actions?.[0]?.onPress();
// handlePerpsDepositPress is async (ensureArbitrumNetworkExists().then(...))
await Promise.resolve();
expect(mockNavigateToConfirmation).toHaveBeenCalledWith({
stack: expect.any(String),
});
expect(mockDepositWithConfirmation).toHaveBeenCalledTimes(1);
await waitFor(
() => {
expect(mockOnReject).toHaveBeenCalledTimes(1);
expect(mockDepositWithConfirmation).toHaveBeenCalledTimes(1);
expect(mockNavigate).toHaveBeenCalledWith(
Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS,
{ showPerpsHeader: true },
);
},
{ timeout: 2000 },
);
Comment thread
cursor[bot] marked this conversation as resolved.
}
});

Expand Down
28 changes: 20 additions & 8 deletions app/components/UI/Perps/hooks/usePerpsBalanceTokenFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import { useIsPerpsBalanceSelected } from './useIsPerpsBalanceSelected';
import { usePerpsPaymentToken } from './usePerpsPaymentToken';
import Routes from '../../../../constants/navigation/Routes';
import { usePerpsTrading } from './usePerpsTrading';
import { useNavigation } from '@react-navigation/native';
import useApprovalRequest from '../../../Views/confirmations/hooks/useApprovalRequest';
import { usePerpsNetworkManagement } from './usePerpsNetworkManagement';
import { useConfirmNavigation } from '../../../Views/confirmations/hooks/useConfirmNavigation';

/** URI for the perps balance token icon, shared with PerpsPayRow and pay-with modal. */
const resolvedPerpsIcon = Image.resolveAssetSource(perpsPayTokenIcon);
Expand All @@ -46,27 +47,38 @@ export function usePerpsBalanceTokenFilter(): (
const formatFiat = useFiatFormatter({ currency: 'usd' });

const { depositWithConfirmation } = usePerpsTrading();
const { ensureArbitrumNetworkExists } = usePerpsNetworkManagement();
const { navigateToConfirmation } = useConfirmNavigation();

const isPerpsDepositAndOrder = hasTransactionType(transactionMeta, [
TransactionType.perpsDepositAndOrder,
]);

const { onReject: handleReject } = useApprovalRequest();
const { ensureArbitrumNetworkExists } = usePerpsNetworkManagement();

const navigation = useNavigation();

const handlePerpsDepositPress = useCallback(() => {
ensureArbitrumNetworkExists()
.then(() => {
navigateToConfirmation({ stack: Routes.PERPS.ROOT });
handleReject();
return depositWithConfirmation();
})
.catch((_err) => {
.then(() => {
navigation.navigate(
Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS,
{
showPerpsHeader: true,
},
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded showPerpsHeader bypasses shared config constant

Low Severity

The showPerpsHeader value is hardcoded to true, but every other perps navigation path (PerpsOrderRedirect.tsx, usePerpsNavigation.ts) uses CONFIRMATION_HEADER_CONFIG.ShowPerpsHeaderForDepositAndTrade from the shared perpsConfig constants. Hardcoding the value here means this code path won't respect future changes to that configuration constant, creating an inconsistency across perps deposit/order flows.

Fix in Cursor Fix in Web

})
.catch(() => {
// Deposit flow handles errors (e.g. user rejection or missing network).
// ensureArbitrumNetworkExists errors are logged inside the hook itself.
});
}, [
ensureArbitrumNetworkExists,
navigateToConfirmation,
navigation,
depositWithConfirmation,
handleReject,
ensureArbitrumNetworkExists,
]);

const { onPaymentTokenChange: onPerpsPaymentTokenChange } =
Expand Down
Loading