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 @@ -8,7 +8,7 @@ import { useStyles } from '../../../../../component-library/hooks';
import { PredictPosition as PredictPositionType } from '../../types';
import { formatPercentage, formatPrice } from '../../utils/format';
import styleSheet from './PredictPosition.styles';
import { getPredictPositionSelector } from '../../../../../../e2e/selectors/Predict/Predict.selectors';
import { PredictPositionSelectorsIDs } from '../../../../../../e2e/selectors/Predict/Predict.selectors';

interface PredictPositionProps {
position: PredictPositionType;
Expand All @@ -32,7 +32,7 @@ const PredictPosition: React.FC<PredictPositionProps> = ({

return (
<TouchableOpacity
testID={getPredictPositionSelector.currentPositionCard}
testID={PredictPositionSelectorsIDs.CURRENT_POSITION_CARD}
style={styles.positionContainer}
onPress={() => onPress?.(position)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useStyles } from '../../../../../component-library/hooks';
import { PredictPosition as PredictPositionType } from '../../types';
import { formatPrice } from '../../utils/format';
import styleSheet from './PredictPositionResolved.styles';
import { getPredictPositionSelector } from '../../../../../../e2e/selectors/Predict/Predict.selectors';
import { PredictPositionSelectorsIDs } from '../../../../../../e2e/selectors/Predict/Predict.selectors';

dayjs.extend(relativeTime);

Expand Down Expand Up @@ -46,7 +46,7 @@ const PredictPositionResolved: React.FC<PredictPositionResolvedProps> = ({

return (
<TouchableOpacity
testID={getPredictPositionSelector.resolvedPositionCard(position.id)}
testID={PredictPositionSelectorsIDs.RESOLVED_POSITION_CARD}
style={styles.positionContainer}
onPress={() => onPress?.(position)}
>
Expand Down
190 changes: 190 additions & 0 deletions app/components/UI/Tokens/util/sortAssetsWithPriority.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import type { Asset } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import type { CaipChainId, Hex } from '@metamask/utils';
import {
compareFiatBalanceWithPriority,
sortAssetsWithPriority,
} from './sortAssetsWithPriority';

function createMockAsset({
name,
fiatBalance,
isNative = true,
chainId = CHAIN_IDS.MAINNET,
}: {
name: string;
fiatBalance?: number;
isNative?: boolean;
chainId?: Hex | CaipChainId;
}): Asset {
return {
name,
fiat: fiatBalance === undefined ? undefined : { balance: fiatBalance },
isNative,
chainId,
} as unknown as Asset;
}

describe('sortAssetsWithPriority', () => {
function extractAssetNames(assets: Asset[]): string[] {
return assets.map((asset) => asset.name);
}

it('sorts by name when key is "name"', () => {
const assets = [
createMockAsset({ name: 'Asset B', fiatBalance: 400 }),
createMockAsset({ name: 'Asset A', fiatBalance: 600 }),
createMockAsset({ name: 'Asset Z', fiatBalance: 500 }),
];

const sortedAssets = sortAssetsWithPriority(assets, {
key: 'name',
order: 'asc',
sortCallback: 'alphaNumeric',
});

expect(extractAssetNames(sortedAssets)).toStrictEqual([
'Asset A',
'Asset B',
'Asset Z',
]);
});

it('sorts by fiat balance when key is "tokenFiatAmount"', () => {
const assets = [
createMockAsset({ name: 'Asset B', fiatBalance: 400 }),
createMockAsset({ name: 'Asset A', fiatBalance: 600 }),
createMockAsset({ name: 'Asset Z', fiatBalance: 500 }),
];

const sortedAssets = sortAssetsWithPriority(assets, {
key: 'tokenFiatAmount',
order: 'dsc',
sortCallback: 'stringNumeric',
});

expect(extractAssetNames(sortedAssets)).toStrictEqual([
'Asset A',
'Asset Z',
'Asset B',
]);
});
});

describe('compareFiatBalanceWithPriority', () => {
describe('fiat balance comparison', () => {
it('compares second value above first if the second asset has a fiat balance and the first asset does not', () => {
const assetA = createMockAsset({
name: 'Asset A',
fiatBalance: undefined,
});
const assetB = createMockAsset({ name: 'Asset B', fiatBalance: 400 });

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeGreaterThan(0);
});

it('compares second value above the first value if both assets have fiat balances and the second value is greater', () => {
const assetA = createMockAsset({ name: 'Asset A', fiatBalance: 300 });
const assetB = createMockAsset({ name: 'Asset B', fiatBalance: 400 });

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeGreaterThan(0);
});
});

describe('non-native asset comparison', () => {
it('returns -1 if the first asset is native and the second asset is not', () => {
const assetA = createMockAsset({ name: 'Asset A', isNative: true });
const assetB = createMockAsset({ name: 'Asset B', isNative: false });

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeLessThan(0);
});

it('returns 1 if the second asset is native and the first asset is not', () => {
const assetA = createMockAsset({ name: 'Asset A', isNative: false });
const assetB = createMockAsset({ name: 'Asset B', isNative: true });

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeGreaterThan(0);
});

it('compares name values if neither asset is native', () => {
const assetA = createMockAsset({ name: 'Asset A', isNative: false });
const assetB = createMockAsset({ name: 'Asset B', isNative: false });

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeLessThan(0);
});
});

describe('native asset comparison', () => {
it('compares name values if neither asset is in the defaultNativeAssetOrder', () => {
const assetA = createMockAsset({
name: 'Asset A',
chainId: '0xeeeeeeeeeeeee1',
});
const assetB = createMockAsset({
name: 'Asset B',
chainId: '0xeeeeeeeeeeeee2',
});

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeLessThan(0);
});

it('compares second value above first if the second asset is in the defaultNativeAssetOrder and the first asset is not', () => {
const assetA = createMockAsset({
name: 'Asset A',
chainId: '0xeeeeeeeeeeeee1',
});
const assetB = createMockAsset({
name: 'Asset B',
chainId: CHAIN_IDS.BASE,
});

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeGreaterThan(0);
});

it('compares second value above first if both assets are in the defaultNativeAssetOrder and the second value has higher priority', () => {
const assetA = createMockAsset({
name: 'Asset A',
chainId: CHAIN_IDS.ARBITRUM,
});
const assetB = createMockAsset({
name: 'Asset B',
chainId: CHAIN_IDS.LINEA_MAINNET,
});

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeGreaterThan(0);
});

it('compares second value above first if both assets are in the defaultNativeAssetOrder and the second value has higher priority (both values have balances of zero)', () => {
const assetA = createMockAsset({
name: 'Asset A',
chainId: CHAIN_IDS.ARBITRUM,
fiatBalance: 0,
});
const assetB = createMockAsset({
name: 'Asset B',
chainId: CHAIN_IDS.LINEA_MAINNET,
fiatBalance: 0,
});

const result = compareFiatBalanceWithPriority(assetA, assetB);

expect(result).toBeGreaterThan(0);
});
});
});
82 changes: 82 additions & 0 deletions app/components/UI/Tokens/util/sortAssetsWithPriority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Asset } from '@metamask/assets-controllers';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import type { CaipChainId, Hex } from '@metamask/utils';
import { BtcScope, SolScope, TrxScope } from '@metamask/keyring-api';
import { sortAssets, type SortCriteria } from './sortAssets';

// These are the only two options for sorting assets
// {"key": "name", "order": "asc", "sortCallback": "alphaNumeric"}
// {"key": "tokenFiatAmount", "order": "dsc", "sortCallback": "stringNumeric"}
export function sortAssetsWithPriority(
array: Asset[],
criteria: SortCriteria,
): Asset[] {
if (criteria.key === 'name') {
return sortAssets(array, {
key: 'name',
order: 'asc',
sortCallback: 'alphaNumeric',
});
}

return [...array].sort(compareFiatBalanceWithPriority);
}

// Higher priority assets are last in the array to facilitate sorting
const defaultNativeAssetOrder: (Hex | CaipChainId)[] = [
CHAIN_IDS.POLYGON,
CHAIN_IDS.OPTIMISM,
CHAIN_IDS.BSC,
CHAIN_IDS.ARBITRUM,
CHAIN_IDS.BASE,
TrxScope.Mainnet,
BtcScope.Mainnet,
SolScope.Mainnet,
CHAIN_IDS.LINEA_MAINNET,
CHAIN_IDS.MAINNET,
];

/**
* Compares assets by fiat balance with priority sorting.
*
* @param a - The first asset to compare.
* @param b - The second asset to compare.
* @returns A negative number if the first asset should appear before the second, a positive number if the first asset should appear after the second, or 0 if they are equal.
*/
export function compareFiatBalanceWithPriority(a: Asset, b: Asset) {
// If one of the fiat balances is greater than the other, return the comparison
const fiatBalanceComparison = (b.fiat?.balance ?? 0) - (a.fiat?.balance ?? 0);

// Only return comparison if it is not zero
if (fiatBalanceComparison) {
return fiatBalanceComparison;
}

// With equal fiat balances
// Always return native assets before token assets
// Apply the priority defined in defaultNativeAssetOrder if both are native assets
// If both assets are tokens or none is in the defaultNativeAssetOrder, compare by name
if (a.isNative && !b.isNative) {
return -1;
}

if (b.isNative && !a.isNative) {
return 1;
}

if (!a.isNative && !b.isNative) {
return a.name.localeCompare(b.name);
}

const nativeAssetOrderA = defaultNativeAssetOrder.indexOf(a.chainId);
const nativeAssetOrderB = defaultNativeAssetOrder.indexOf(b.chainId);

const nativeAssetOrderComparison = nativeAssetOrderB - nativeAssetOrderA;

if (nativeAssetOrderComparison) {
return nativeAssetOrderComparison;
}

// If neither asset is in the defaultNativeAssetOrder, compare by name
return a.name.localeCompare(b.name);
}
5 changes: 5 additions & 0 deletions app/components/Views/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,11 @@ const Login: React.FC<LoginProps> = ({ saveOnboardingEvent }) => {
};

const handleUseOtherMethod = () => {
if (isComingFromOauthOnboarding) {
track(MetaMetricsEvents.USE_DIFFERENT_LOGIN_METHOD_CLICKED, {
account_type: 'social',
});
}
navigation.goBack();
OAuthService.resetOauthState();
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
upgradeAccountConfirmation,
} from '../../../../../../util/test/confirm-data-helpers';
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
import useBalanceChanges from '../../../../../UI/SimulationDetails/useBalanceChanges';
// eslint-disable-next-line import/no-namespace
import * as EditNonceHook from '../../../../../../components/hooks/useEditNonce';
import { useConfirmActions } from '../../../hooks/useConfirmActions';
Expand All @@ -36,6 +37,7 @@ jest.mock('../../../hooks/useAutomaticGasFeeTokenSelect');
jest.mock('../../../hooks/alerts/useInsufficientBalanceAlert', () => ({
useInsufficientBalanceAlert: jest.fn().mockReturnValue([]),
}));
jest.mock('../../../../../UI/SimulationDetails/useBalanceChanges');

jest.mock('../../../hooks/7702/use7702TransactionType', () => ({
use7702TransactionType: jest
Expand Down Expand Up @@ -111,6 +113,7 @@ describe('ContractInteraction', () => {
const mockUseConfirmationMetricEvents = jest.mocked(
useConfirmationMetricEvents,
);
const mockUseBalanceChanges = jest.mocked(useBalanceChanges);

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -122,6 +125,10 @@ describe('ContractInteraction', () => {
mockUseConfirmationMetricEvents.mockReturnValue({
trackPageViewedEvent: mockTrackPageViewedEvent,
} as unknown as ReturnType<typeof useConfirmationMetricEvents>);
mockUseBalanceChanges.mockReturnValue({
pending: false,
value: [],
});

const mockTxId = '7e62bcb1-a4e9-11ef-9b51-ddf21c91a998';
jest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
formatPercentage,
formatPrice,
} from '../../../../../UI/Predict/utils/format';
import { PredictClaimConfirmationSelectorsIDs } from '../../../../../../../e2e/selectors/Predict/Predict.selectors';
import styleSheet from './predict-claim-amount.styles';

export function PredictClaimAmount() {
Expand All @@ -35,7 +36,10 @@ export function PredictClaimAmount() {
})} (${formatPercentage((winningsPnl / winningsFiat) * 100)})`;

return (
<Box style={styles.container}>
<Box
style={styles.container}
testID={PredictClaimConfirmationSelectorsIDs.CLAIM_AMOUNT_CONTAINER}
>
<Text variant={TextVariant.HeadingLG} color={TextColor.Alternative}>
{strings('confirm.predict_claim.summary')}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Text, {
} from '../../../../../../component-library/components/Texts/Text';
import { useStyles } from '../../../../../../component-library/hooks';
import { Box } from '../../../../../UI/Box/Box';
import { PredictClaimConfirmationSelectorsIDs } from '../../../../../../../e2e/selectors/Predict/Predict.selectors';
import styleSheet from './predict-claim-footer.styles';
import { selectPredictWonPositions } from '../../../../../UI/Predict/selectors/predictController';

Expand Down Expand Up @@ -52,6 +53,7 @@ export function PredictClaimFooter({ onPress }: PredictClaimFooterProps) {
label={strings('confirm.predict_claim.button_label')}
onPress={onPress}
isInverse
testID={PredictClaimConfirmationSelectorsIDs.CLAIM_CONFIRM_BUTTON}
/>
<Text
variant={TextVariant.BodyXS}
Expand Down
Loading
Loading