From b418caea38c63f18f7fa8ac8cb1f0ed3069a39c8 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Fri, 8 May 2026 11:12:24 +0100 Subject: [PATCH 1/8] chore: adds several improvements to the UI --- .../components/AssetRow.test.tsx | 169 ++++++++++++++ .../components/AssetRow.tsx | 76 ++++++- .../components/PageIndicator.test.tsx | 27 +++ .../components/PageIndicator.tsx | 2 +- .../components/PerpsRow.test.tsx | 92 +++++++- .../components/PerpsRow.tsx | 31 ++- .../components/TokenRow.test.tsx | 91 +++++++- .../components/TokenRow.tsx | 59 ++++- .../WhatsHappeningExpandedCard.test.tsx | 61 ++++- .../components/WhatsHappeningExpandedCard.tsx | 52 ++++- .../useWhatsHappeningAssetPrices.test.ts | 213 ++++++++++++++++++ .../hooks/useWhatsHappeningAssetPrices.ts | 145 ++++++++++++ .../utils/formatAssetPrice.test.ts | 106 +++++++++ .../utils/formatAssetPrice.ts | 74 ++++++ ios/MetaMask.xcodeproj/project.pbxproj | 8 +- 15 files changed, 1156 insertions(+), 50 deletions(-) create mode 100644 app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx create mode 100644 app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.test.ts create mode 100644 app/components/Views/WhatsHappeningDetailView/hooks/useWhatsHappeningAssetPrices.ts create mode 100644 app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.test.ts create mode 100644 app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.ts diff --git a/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx b/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx new file mode 100644 index 00000000000..a0534be7b67 --- /dev/null +++ b/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx @@ -0,0 +1,169 @@ +import React from 'react'; +import { screen, fireEvent } from '@testing-library/react-native'; +import { TextColor } from '@metamask/design-system-react-native'; +import type { RelatedAsset } from '@metamask/ai-controllers'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import AssetRow from './AssetRow'; + +jest.mock('../utils/getRelatedAssetImageSource', () => ({ + getRelatedAssetImageSource: jest.fn(() => undefined), +})); + +jest.mock( + '../../../UI/Tokens/components/TokenListSecurityBadge/TokenListSecurityBadge', + () => 'TokenListSecurityBadge', +); + +const btcAsset: RelatedAsset = { + sourceAssetId: 'bitcoin', + symbol: 'BTC', + name: 'Bitcoin', + caip19: ['eip155:1/slip44:0'], +}; + +const symbolOnlyAsset: RelatedAsset = { + sourceAssetId: 'unknown', + symbol: 'UNK', + name: '', + caip19: [], +}; + +describe('AssetRow', () => { + const onAction = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('displays asset.name when name is present', () => { + renderWithProvider( + , + ); + expect(screen.getByText('Bitcoin')).toBeOnTheScreen(); + }); + + it('falls back to asset.symbol when name is empty', () => { + renderWithProvider( + , + ); + expect(screen.getByText('UNK')).toBeOnTheScreen(); + }); + + it('renders the action button with the provided label', () => { + renderWithProvider( + , + ); + expect(screen.getByText('Trade')).toBeOnTheScreen(); + }); + + it('calls onAction when the button is pressed', () => { + renderWithProvider( + , + ); + fireEvent.press(screen.getByText('Buy')); + expect(onAction).toHaveBeenCalledTimes(1); + }); + + it('renders the TokenListSecurityBadge when caipAssetId is provided', () => { + renderWithProvider( + , + ); + expect(screen.getByTestId !== undefined).toBeTruthy(); + // The mocked component renders as 'TokenListSecurityBadge' native element + const badge = screen.UNSAFE_getByType( + 'TokenListSecurityBadge' as unknown as React.ComponentType, + ); + expect(badge).toBeTruthy(); + expect(badge.props.caipAssetId).toBe('eip155:1/slip44:0'); + }); + + it('does not render the security badge when caipAssetId is not provided', () => { + renderWithProvider( + , + ); + expect( + screen.UNSAFE_queryByType( + 'TokenListSecurityBadge' as unknown as React.ComponentType, + ), + ).toBeNull(); + }); + + it('renders the price secondary line when secondaryLine is provided', () => { + renderWithProvider( + , + ); + expect(screen.getByText('$95,000.00')).toBeOnTheScreen(); + expect(screen.getByText('+2.50%')).toBeOnTheScreen(); + }); + + it('renders just the price without change text when changeText is undefined', () => { + renderWithProvider( + , + ); + expect(screen.getByText('$95,000.00')).toBeOnTheScreen(); + expect(screen.queryByText('%')).toBeNull(); + }); + + it('does not render secondary line when secondaryLine is not provided', () => { + renderWithProvider( + , + ); + expect(screen.queryByText('$')).toBeNull(); + }); +}); diff --git a/app/components/Views/WhatsHappeningDetailView/components/AssetRow.tsx b/app/components/Views/WhatsHappeningDetailView/components/AssetRow.tsx index 381404af394..a3e25cd0040 100644 --- a/app/components/Views/WhatsHappeningDetailView/components/AssetRow.tsx +++ b/app/components/Views/WhatsHappeningDetailView/components/AssetRow.tsx @@ -15,25 +15,38 @@ import { TextVariant, } from '@metamask/design-system-react-native'; import type { RelatedAsset } from '@metamask/ai-controllers'; +import type { CaipAssetType } from '@metamask/utils'; import { getRelatedAssetImageSource } from '../utils/getRelatedAssetImageSource'; +import TokenListSecurityBadge from '../../../UI/Tokens/components/TokenListSecurityBadge/TokenListSecurityBadge'; + +export interface AssetRowSecondaryLine { + priceText: string; + changeText: string | undefined; + changeColor: TextColor; +} interface AssetRowProps { asset: RelatedAsset; actionLabel: string; accessibilityLabel: string; onAction: () => void; + /** When provided, renders the security badge inline next to the asset name. */ + caipAssetId?: CaipAssetType; + /** When provided, renders price + 24h change below the asset name. */ + secondaryLine?: AssetRowSecondaryLine; } /** - * Shared layout for a single asset row (logo + symbol + action button). - * Used by TokenRow (Buy/Trade) and PerpsRow (Trade); each wrapper supplies its - * own hook logic and passes the resolved label and handler here. + * Shared layout for a single asset row (logo + name + optional badge + optional + * price/change + action button). Used by TokenRow (Buy/Trade) and PerpsRow (Trade). */ const AssetRow: React.FC = ({ asset, actionLabel, accessibilityLabel, onAction, + caipAssetId, + secondaryLine, }) => { const rawImageSource = getRelatedAssetImageSource(asset); const imageSource = Array.isArray(rawImageSource) @@ -59,13 +72,56 @@ const AssetRow: React.FC = ({ alignItems={BoxAlignItems.Center} justifyContent={BoxJustifyContent.Between} > - - {asset.symbol} - + {/* Left: name + optional badge + optional price/change */} + + + + {asset.name || asset.symbol} + + {caipAssetId && ( + + )} + + + {secondaryLine && ( + + + {secondaryLine.priceText} + + {secondaryLine.changeText ? ( + <> + + {' \u2022 '} + + + {secondaryLine.changeText} + + + ) : null} + + )} + + {onAction && actionLabel && accessibilityLabel ? ( + + ) : null} ); diff --git a/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.test.tsx b/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.test.tsx index 84835ba9856..a54027aa345 100644 --- a/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.test.tsx +++ b/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.test.tsx @@ -143,7 +143,7 @@ describe('PerpsRow', () => { }); }); - it('does not navigate when hlPerpsMarket is empty', () => { + it('does not render Trade when hlPerpsMarket is empty', () => { const assetNoPerps: RelatedAsset = { ...perpsOnlyAsset, hlPerpsMarket: [], @@ -156,8 +156,9 @@ describe('PerpsRow', () => { perpsPriceBySymbol={emptyPriceMap} />, ); - fireEvent.press(screen.getByText('Trade')); + expect(screen.queryByText('Trade')).toBeNull(); expect(mockNavigate).not.toHaveBeenCalled(); + expect(mockCreateEventBuilder).not.toHaveBeenCalled(); }); it('tracks Whats Happening Interaction on Trade press', () => { @@ -189,23 +190,6 @@ describe('PerpsRow', () => { ); }); - it('does not track Interaction when hlPerpsMarket is empty', () => { - const assetNoPerps: RelatedAsset = { - ...perpsOnlyAsset, - hlPerpsMarket: [], - }; - renderWithProvider( - , - ); - fireEvent.press(screen.getByText('Trade')); - expect(mockCreateEventBuilder).not.toHaveBeenCalled(); - }); - it('displays price and 24h change from perpsPriceBySymbol', () => { const priceMap: Record = { 'xyz:TSLA': { price: 172.5, percentChange24h: 3.45 }, diff --git a/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx b/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx index 111e1a16bb6..daca5c743f1 100644 --- a/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx +++ b/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx @@ -26,7 +26,8 @@ interface PerpsRowProps { /** * A single row for a perps asset in the expanded What's Happening card. * Shows logo, name, optional verified badge (for assets that also have caip19), - * live price/change, and a Trade button. + * live price/change when `hlPerpsMarket` is set, and a Trade button only when a + * perps market symbol exists. */ const PerpsRow: React.FC = ({ asset, @@ -70,14 +71,17 @@ const PerpsRow: React.FC = ({ }, [asset.hlPerpsMarket, perpsPriceBySymbol]); const handleTradeWithTracking = useCallback(() => { - if (!asset.hlPerpsMarket?.[0]) return; + const perpsMarket = asset.hlPerpsMarket?.[0]; + if (!perpsMarket) { + return; + } trackEvent( createEventBuilder(MetaMetricsEvents.WHATS_HAPPENING_INTERACTION) .addProperties({ ...getWhatsHappeningEventProps(item, cardIndex), interaction_type: WhatsHappeningInteractionType.TradePressed, asset_symbol: asset.symbol, - perps_market: asset.hlPerpsMarket?.[0], + perps_market: perpsMarket, }) .build(), ); @@ -92,12 +96,18 @@ const PerpsRow: React.FC = ({ createEventBuilder, ]); + const perpsMarketSymbol = asset.hlPerpsMarket?.[0]; + return ( From d3959056d54507e527e72801d349ee4947ebb4a0 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Fri, 8 May 2026 15:58:37 +0100 Subject: [PATCH 7/8] chore: reset file --- ios/MetaMask.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/MetaMask.xcodeproj/project.pbxproj b/ios/MetaMask.xcodeproj/project.pbxproj index f29b89d59c1..feaae11162e 100644 --- a/ios/MetaMask.xcodeproj/project.pbxproj +++ b/ios/MetaMask.xcodeproj/project.pbxproj @@ -88,13 +88,13 @@ E4B580762E33A001008165E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B580752E33A001008165E1 /* AppDelegate.swift */; }; E4B580772E33A001008165E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B580752E33A001008165E1 /* AppDelegate.swift */; }; E4B580782E33A001008165E1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4B580752E33A001008165E1 /* AppDelegate.swift */; }; + F0B2A3E101000001000000A1 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; + F0B2A3E101000001000000A2 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; + F0B2A3E101000001000000A3 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; E83DB5522BBDF2AA00536063 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */; }; E83DB5532BBDF2AE00536063 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */; }; E83DB5542BBDF2AF00536063 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */; }; ED2E8FE6D71BE9319F3B27D3 /* libPods-MetaMask.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D2632307C64595BE1B8ABEAF /* libPods-MetaMask.a */; }; - F0B2A3E101000001000000A1 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; - F0B2A3E101000001000000A2 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; - F0B2A3E101000001000000A3 /* BrazeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = F0B2A3E101000001000000A0 /* BrazeHelper.mm */; }; F23972D16903249A8EC120BD /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EB256CB3A1A7A1D942A95F6 /* ExpoModulesProvider.swift */; }; F961A37228105CF9007442B5 /* LinkPresentation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F961A36A28105CF9007442B5 /* LinkPresentation.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; /* End PBXBuildFile section */ @@ -242,11 +242,11 @@ D3350113F0764105B1E60002 /* MMSans-Bold.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "MMSans-Bold.otf"; path = "../app/fonts/MMSans-Bold.otf"; sourceTree = ""; }; E4B580712E32F462008165E1 /* Expo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; E4B580752E33A001008165E1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaMask/AppDelegate.swift; sourceTree = ""; }; + F0B2A3E101000001000000A0 /* BrazeHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MetaMask/BrazeHelper.mm; sourceTree = ""; }; E7EEA32C976A46B991D55FD4 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-MetaMask-QA/ExpoModulesProvider.swift"; sourceTree = ""; }; E83DB5392BBDB14700536063 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = MetaMask/PrivacyInfo.xcprivacy; sourceTree = SOURCE_ROOT; }; E9629905BA1940ADA4189921 /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = ""; }; EBC2B6371CD846D28B9FAADF /* FontAwesome5_Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome5_Regular.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf"; sourceTree = ""; }; - F0B2A3E101000001000000A0 /* BrazeHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MetaMask/BrazeHelper.mm; sourceTree = ""; }; F10E7EBF946A4F6D8E229143 /* MMPoly-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "MMPoly-Regular.otf"; path = "../app/fonts/MMPoly-Regular.otf"; sourceTree = ""; }; F1CCBB0591B4D16C1710A05D /* Pods-MetaMask-Flask.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MetaMask-Flask.release.xcconfig"; path = "Target Support Files/Pods-MetaMask-Flask/Pods-MetaMask-Flask.release.xcconfig"; sourceTree = ""; }; F3C919D8F42C47389FF643E7 /* MMSans-Regular.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "MMSans-Regular.otf"; path = "../app/fonts/MMSans-Regular.otf"; sourceTree = ""; }; From 36e18219fed2f95eda0e3b10fc9bd8abcdfb0d40 Mon Sep 17 00:00:00 2001 From: Antonio Regadas Date: Fri, 8 May 2026 16:53:51 +0100 Subject: [PATCH 8/8] chore: update based on feedback review --- .../components/AssetRow.test.tsx | 40 -------- .../components/AssetRow.tsx | 28 ++---- .../components/PerpsRow.test.tsx | 96 ++----------------- .../components/PerpsRow.tsx | 28 ------ .../utils/formatAssetPrice.test.ts | 29 +----- .../utils/formatAssetPrice.ts | 27 +----- 6 files changed, 20 insertions(+), 228 deletions(-) diff --git a/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx b/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx index e958808d3df..e845a6807b7 100644 --- a/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx +++ b/app/components/Views/WhatsHappeningDetailView/components/AssetRow.test.tsx @@ -9,11 +9,6 @@ jest.mock('../utils/getRelatedAssetImageSource', () => ({ getRelatedAssetImageSource: jest.fn(() => undefined), })); -jest.mock( - '../../../UI/Tokens/components/TokenListSecurityBadge/TokenListSecurityBadge', - () => 'TokenListSecurityBadge', -); - const btcAsset: RelatedAsset = { sourceAssetId: 'bitcoin', symbol: 'BTC', @@ -90,41 +85,6 @@ describe('AssetRow', () => { expect(onAction).toHaveBeenCalledTimes(1); }); - it('renders the TokenListSecurityBadge when caipAssetId is provided', () => { - renderWithProvider( - , - ); - expect(screen.getByTestId !== undefined).toBeTruthy(); - // The mocked component renders as 'TokenListSecurityBadge' native element - const badge = screen.UNSAFE_getByType( - 'TokenListSecurityBadge' as unknown as React.ComponentType, - ); - expect(badge).toBeTruthy(); - expect(badge.props.caipAssetId).toBe('eip155:1/slip44:0'); - }); - - it('does not render the security badge when caipAssetId is not provided', () => { - renderWithProvider( - , - ); - expect( - screen.UNSAFE_queryByType( - 'TokenListSecurityBadge' as unknown as React.ComponentType, - ), - ).toBeNull(); - }); - it('renders the price secondary line when secondaryLine is provided', () => { renderWithProvider( void; - /** When provided, renders the security badge inline next to the asset name. */ - caipAssetId?: CaipAssetType; /** When provided, renders price + 24h change below the asset name. */ secondaryLine?: AssetRowSecondaryLine; } @@ -45,7 +41,6 @@ const AssetRow: React.FC = ({ actionLabel, accessibilityLabel, onAction, - caipAssetId, secondaryLine, }) => { const rawImageSource = getRelatedAssetImageSource(asset); @@ -74,23 +69,14 @@ const AssetRow: React.FC = ({ > {/* Left: name + optional badge + optional price/change */} - - - {asset.name || asset.symbol} - - {caipAssetId && ( - - )} - + {asset.name || asset.symbol} + {secondaryLine && ( ({ }), })); -jest.mock( - '../../../UI/Tokens/components/TokenListSecurityBadge/TokenListSecurityBadge', - () => 'TokenListSecurityBadge', -); - -jest.mock( - '../../../../selectors/featureFlagController/tokenListSecurityBadges', - () => ({ - selectTokenListSecurityBadgesEnabled: jest.fn(() => true), - }), -); - const perpsOnlyAsset: RelatedAsset = { sourceAssetId: 'tsla', symbol: 'TSLA', @@ -56,15 +44,6 @@ const perpsOnlyAsset: RelatedAsset = { hlPerpsMarket: ['xyz:TSLA'], }; -/** Asset that has both a perps market AND a caip19 id — eligible for badge */ -const dualAsset: RelatedAsset = { - sourceAssetId: 'bitcoin', - symbol: 'BTC', - name: 'Bitcoin', - caip19: ['eip155:1/slip44:0'], - hlPerpsMarket: ['BTC'], -}; - const mockItem: WhatsHappeningItem = { id: 'trend-3', title: 'TSLA earnings', @@ -125,10 +104,14 @@ describe('PerpsRow', () => { }); }); - it('uses first hlPerpsMarket entry as the market symbol', () => { + it('uses first hlPerpsMarket entry as the market symbol when multiple are present', () => { + const multiMarketAsset: RelatedAsset = { + ...perpsOnlyAsset, + hlPerpsMarket: ['FIRST-MARKET', 'SECOND-MARKET'], + }; renderWithProvider( { expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, { screen: Routes.PERPS.MARKET_DETAILS, params: expect.objectContaining({ - market: { symbol: 'BTC', name: 'Bitcoin' }, + market: { symbol: 'FIRST-MARKET', name: 'Tesla' }, }), }); }); @@ -217,69 +200,4 @@ describe('PerpsRow', () => { ); expect(screen.queryByText('$')).toBeNull(); }); - - describe('verified badge gating', () => { - it('renders TokenListSecurityBadge when asset has caip19 and feature flags are enabled', () => { - renderWithProvider( - , - { - state: { - settings: { basicFunctionalityEnabled: true }, - }, - }, - ); - const badge = screen.UNSAFE_getByType( - 'TokenListSecurityBadge' as unknown as React.ComponentType, - ); - expect(badge).toBeTruthy(); - expect(badge.props.caipAssetId).toBe('eip155:1/slip44:0'); - }); - - it('does not render TokenListSecurityBadge when basicFunctionalityEnabled is false', () => { - renderWithProvider( - , - { - state: { - settings: { basicFunctionalityEnabled: false }, - }, - }, - ); - expect( - screen.UNSAFE_queryByType( - 'TokenListSecurityBadge' as unknown as React.ComponentType, - ), - ).toBeNull(); - }); - - it('does not render TokenListSecurityBadge for a perps-only asset (no caip19)', () => { - renderWithProvider( - , - { - state: { - settings: { basicFunctionalityEnabled: true }, - }, - }, - ); - expect( - screen.UNSAFE_queryByType( - 'TokenListSecurityBadge' as unknown as React.ComponentType, - ), - ).toBeNull(); - }); - }); }); diff --git a/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx b/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx index daca5c743f1..8f72324d5e7 100644 --- a/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx +++ b/app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx @@ -1,6 +1,4 @@ import React, { useCallback, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import type { CaipAssetType } from '@metamask/utils'; import type { RelatedAsset } from '@metamask/ai-controllers'; import { strings } from '../../../../../locales/i18n'; import { MetaMetricsEvents } from '../../../../core/Analytics'; @@ -9,11 +7,9 @@ import { WhatsHappeningInteractionType } from '../../Homepage/Sections/WhatsHapp import { getWhatsHappeningEventProps } from '../../Homepage/Sections/WhatsHappening/eventProperties'; import type { WhatsHappeningItem } from '../../Homepage/Sections/WhatsHappening/types'; import { formatAssetPrice } from '../utils/formatAssetPrice'; -import { selectTokenListSecurityBadgesEnabled } from '../../../../selectors/featureFlagController/tokenListSecurityBadges'; import type { PerpsPriceEntry } from '../hooks/useWhatsHappeningAssetPrices'; import AssetRow from './AssetRow'; import useTradeNavigation from '../hooks/useTradeNavigation'; -import type { RootState } from '../../../../reducers'; interface PerpsRowProps { asset: RelatedAsset; @@ -37,29 +33,6 @@ const PerpsRow: React.FC = ({ }) => { const { handleTrade } = useTradeNavigation(asset); const { trackEvent, createEventBuilder } = useAnalytics(); - const isTokenListSecurityBadgesEnabled = useSelector( - selectTokenListSecurityBadgesEnabled, - ); - const basicFunctionalityEnabled = useSelector( - (state: RootState) => state.settings.basicFunctionalityEnabled, - ); - - // Show verified badge for assets that also have a caip19 identifier - const caipAssetId = useMemo(() => { - const firstCaip = asset.caip19?.[0]; - if ( - !firstCaip || - !basicFunctionalityEnabled || - !isTokenListSecurityBadgesEnabled - ) { - return undefined; - } - return firstCaip as CaipAssetType; - }, [ - asset.caip19, - basicFunctionalityEnabled, - isTokenListSecurityBadgesEnabled, - ]); // Perps prices are always quoted in USD const secondaryLine = useMemo(() => { @@ -108,7 +81,6 @@ const PerpsRow: React.FC = ({ : undefined } onAction={perpsMarketSymbol ? handleTradeWithTracking : undefined} - caipAssetId={caipAssetId} secondaryLine={secondaryLine} /> ); diff --git a/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.test.ts b/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.test.ts index 8259436da86..a078f1f29ef 100644 --- a/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.test.ts +++ b/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.test.ts @@ -1,32 +1,5 @@ import { TextColor } from '@metamask/design-system-react-native'; -import { - formatPrice, - formatPercentageChange, - formatAssetPrice, -} from './formatAssetPrice'; - -describe('formatPrice', () => { - it('formats a USD price with 2 decimal places', () => { - expect(formatPrice(1.0, 'USD')).toBe('$1.00'); - }); - - it('formats a large USD price with comma separators', () => { - const result = formatPrice(95000, 'USD'); - expect(result).toBe('$95,000.00'); - }); - - it('handles fractional cents', () => { - const result = formatPrice(0.9998, 'USD'); - expect(result).toBe('$1.00'); - }); - - it('falls back to plain format for invalid currency codes', () => { - const result = formatPrice(100, 'INVALID_CURRENCY_XYZ'); - // Should not throw; returns a fallback string - expect(result).toBeTruthy(); - expect(result).toContain('100'); - }); -}); +import { formatPercentageChange, formatAssetPrice } from './formatAssetPrice'; describe('formatPercentageChange', () => { it('returns dash text and alternative color when change is undefined', () => { diff --git a/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.ts b/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.ts index 980839ae11c..65f1d9fd5eb 100644 --- a/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.ts +++ b/app/components/Views/WhatsHappeningDetailView/utils/formatAssetPrice.ts @@ -1,4 +1,5 @@ import { TextColor } from '@metamask/design-system-react-native'; +import { formatPerpsFiat } from '@metamask/perps-controller'; export interface FormattedAssetPrice { priceText: string; @@ -6,27 +7,6 @@ export interface FormattedAssetPrice { changeColor: TextColor; } -/** - * Formats a fiat price for display. Mirrors the logic used in `PopularTokenRow` - * and `TokenListItem` — fixed 2 decimal places with currency symbol. - */ -export function formatPrice( - price: number, - currency: string | undefined, -): string { - const safeCurrency = currency?.toUpperCase() || 'USD'; - try { - return new Intl.NumberFormat(undefined, { - style: 'currency', - currency: safeCurrency, - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(price); - } catch { - return `${safeCurrency} ${price.toFixed(2)}`; - } -} - /** * Returns the text and color for a 24h percentage change value. * Positive → success green, negative → error red, zero/undefined → muted. @@ -64,7 +44,10 @@ export function formatAssetPrice( ): FormattedAssetPrice { const priceText = price !== undefined && price !== null && Number.isFinite(price) - ? formatPrice(price, currency) + ? formatPerpsFiat(price, { + currency: currency ?? 'USD', + stripTrailingZeros: false, + }) : '—'; const { text: changeText, color: changeColor } =