Skip to content

Commit 8d9bcda

Browse files
authored
chore: What's Happening UI/UX polish (#29782)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Four small UI/UX fixes for the What's Happening feature: 1. Home carousel card date visibility 2. Detail-view header size 3. Expanded card height consistency 4. in Tokens row the Buy button switched to Trade button, and now users should always be directed to Perps trade. When a token asset has an hlPerpsMarket entry the row now shows a Trade button navigating to Perps market details; otherwise it falls back to the existing Buy/Ramp flow. <img height="790" alt="Simulator Screenshot - iPhone 17 Pro - 2026-05-06 at 12 23 11" src="https://github.com/user-attachments/assets/d3a91506-e9d0-4182-a9ea-47732834268a" /> <img height="790" alt="Simulator Screenshot - iPhone 17 Pro - 2026-05-06 at 12 23 02" src="https://github.com/user-attachments/assets/2c40b688-b91b-4da5-9e10-d69bfc8b6844" /> <img height="790" alt="Simulator Screenshot - iPhone 17 Pro - 2026-05-06 at 12 01 23" src="https://github.com/user-attachments/assets/9b7371c3-4306-4794-be88-2671d0e01836" /> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it changes navigation behavior for token action buttons (routing some assets to Perps) and alters carousel/card layout rendering based on runtime measurement, which could affect visibility/scroll positioning across devices. > > **Overview** > Polishes What’s Happening UI by slightly increasing home carousel card heights, reducing card title lines to preserve footer/date visibility, and resizing the “View more” card to match. > > Updates the detail view header to a custom, smaller layout and makes the expanded-card carousel use a fixed measured `cardHeight`, gating card rendering and initial scroll positioning until layout is known; tests now simulate `onLayout` to validate rendering. > > Changes related-asset actions so token rows show **Trade** (navigating to Perps market details) when `hlPerpsMarket` is present, via new `useTradeNavigation`; `PerpsRow` is simplified to reuse the same hook, and `AssetRow` migrates to the design-system `Button` component. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7edb172. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent b3c014c commit 8d9bcda

11 files changed

Lines changed: 287 additions & 141 deletions

File tree

app/components/Views/Homepage/Sections/WhatsHappening/WhatsHappeningSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ const WhatsHappeningSection = forwardRef<
166166
))}
167167
<ViewMoreCard
168168
onPress={handleViewAll}
169-
twClassName="w-[180px] h-[248px]"
169+
twClassName="w-[180px] h-[254px]"
170170
textVariant={TextVariant.BodyLg}
171171
/>
172172
</>

app/components/Views/Homepage/Sections/WhatsHappening/components/WhatsHappeningCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const WhatsHappeningCard: React.FC<WhatsHappeningCardProps> = ({
3838
onPress={handlePress}
3939
activeOpacity={0.7}
4040
style={tw.style(
41-
'w-[280px] h-[248px] rounded-2xl bg-background-muted overflow-hidden p-4 justify-between gap-3',
41+
'w-[280px] h-[254px] rounded-2xl bg-background-muted overflow-hidden p-4 justify-between gap-3',
4242
)}
4343
>
4444
<Box gap={3}>
@@ -83,7 +83,7 @@ const WhatsHappeningCard: React.FC<WhatsHappeningCardProps> = ({
8383
variant={TextVariant.BodyMd}
8484
fontWeight={FontWeight.Medium}
8585
color={TextColor.TextDefault}
86-
numberOfLines={3}
86+
numberOfLines={2}
8787
>
8888
{item.title}
8989
</Text>

app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.test.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,12 @@ describe('WhatsHappeningDetailView', () => {
113113
refresh: mockRefresh,
114114
});
115115
renderWithProvider(<WhatsHappeningDetailView />);
116-
expect(
117-
screen.getByTestId('whats-happening-detail-carousel'),
118-
).toBeOnTheScreen();
116+
const carousel = screen.getByTestId('whats-happening-detail-carousel');
117+
expect(carousel).toBeOnTheScreen();
118+
// Simulate the carousel measuring its height so cards become visible
119+
fireEvent(carousel, 'layout', {
120+
nativeEvent: { layout: { height: 600, width: 375, x: 0, y: 0 } },
121+
});
119122
expect(screen.getByText(mockItem.title)).toBeOnTheScreen();
120123
});
121124

app/components/Views/WhatsHappeningDetailView/WhatsHappeningDetailView.tsx

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useCallback, useEffect, useRef, useState } from 'react';
22
import {
33
Dimensions,
4+
LayoutChangeEvent,
45
NativeScrollEvent,
56
NativeSyntheticEvent,
67
SafeAreaView,
@@ -10,10 +11,14 @@ import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
1011
import { useTailwind } from '@metamask/design-system-twrnc-preset';
1112
import {
1213
Box,
14+
BoxAlignItems,
15+
BoxFlexDirection,
1316
ButtonIcon,
1417
ButtonIconSize,
15-
HeaderBase,
18+
FontWeight,
1619
IconName,
20+
Text,
21+
TextVariant,
1722
} from '@metamask/design-system-react-native';
1823
import { strings } from '../../../../locales/i18n';
1924
import { useWhatsHappening } from '../Homepage/Sections/WhatsHappening/hooks';
@@ -45,22 +50,33 @@ const WhatsHappeningDetailView = () => {
4550
const route =
4651
useRoute<RouteProp<{ params: WhatsHappeningDetailParams }, 'params'>>();
4752

48-
const { initialIndex = 0 } = route.params;
53+
const initialIndex = route.params?.initialIndex ?? 0;
4954

5055
const { items, isLoading, error, refresh } =
5156
useWhatsHappening(MAX_ITEMS_DISPLAYED);
5257

5358
const [currentIndex, setCurrentIndex] = useState(initialIndex);
59+
const [cardHeight, setCardHeight] = useState(0);
5460
const scrollViewRef = useRef<ScrollView>(null);
5561

62+
const handleCarouselLayout = useCallback((e: LayoutChangeEvent) => {
63+
const { height } = e.nativeEvent.layout;
64+
if (height > 0) setCardHeight(height);
65+
}, []);
66+
5667
useEffect(() => {
57-
if (initialIndex > 0 && scrollViewRef.current && !isLoading) {
68+
if (
69+
initialIndex > 0 &&
70+
cardHeight > 0 &&
71+
scrollViewRef.current &&
72+
!isLoading
73+
) {
5874
scrollViewRef.current.scrollTo({
5975
x: initialIndex * SNAP_INTERVAL,
6076
animated: false,
6177
});
6278
}
63-
}, [initialIndex, isLoading]);
79+
}, [initialIndex, isLoading, cardHeight]);
6480

6581
const handleBackPress = useCallback(() => {
6682
navigation.goBack();
@@ -79,20 +95,24 @@ const WhatsHappeningDetailView = () => {
7995

8096
return (
8197
<SafeAreaView style={tw`flex-1 bg-default`}>
82-
<HeaderBase
83-
startAccessory={
84-
<ButtonIcon
85-
size={ButtonIconSize.Lg}
86-
onPress={handleBackPress}
87-
iconName={IconName.ArrowLeft}
88-
testID="whats-happening-detail-back-button"
89-
/>
90-
}
91-
style={tw`p-4`}
92-
twClassName="h-auto"
98+
<Box
99+
flexDirection={BoxFlexDirection.Row}
100+
alignItems={BoxAlignItems.Center}
101+
twClassName="px-2 py-2"
93102
>
94-
{strings('homepage.sections.whats_happening')}
95-
</HeaderBase>
103+
<ButtonIcon
104+
iconName={IconName.ArrowLeft}
105+
size={ButtonIconSize.Md}
106+
onPress={handleBackPress}
107+
testID="whats-happening-detail-back-button"
108+
/>
109+
<Box twClassName="flex-1 items-center">
110+
<Text variant={TextVariant.HeadingSm} fontWeight={FontWeight.Bold}>
111+
{strings('homepage.sections.whats_happening')}
112+
</Text>
113+
</Box>
114+
<Box twClassName="w-10" />
115+
</Box>
96116

97117
<Box twClassName="flex-1">
98118
{isLoading ? (
@@ -125,17 +145,20 @@ const WhatsHappeningDetailView = () => {
125145
snapToInterval={SNAP_INTERVAL}
126146
snapToAlignment="start"
127147
style={tw`flex-1`}
128-
contentContainerStyle={tw.style(`px-4 gap-3 items-stretch`)}
148+
contentContainerStyle={tw.style('px-4 gap-3')}
149+
onLayout={handleCarouselLayout}
129150
onMomentumScrollEnd={handleScrollEnd}
130151
testID="whats-happening-detail-carousel"
131152
>
132-
{items.map((item) => (
133-
<WhatsHappeningExpandedCard
134-
key={item.id}
135-
item={item}
136-
cardWidth={CARD_WIDTH}
137-
/>
138-
))}
153+
{cardHeight > 0 &&
154+
items.map((item) => (
155+
<WhatsHappeningExpandedCard
156+
key={item.id}
157+
item={item}
158+
cardWidth={CARD_WIDTH}
159+
cardHeight={cardHeight}
160+
/>
161+
))}
139162
</ScrollView>
140163

141164
<PageIndicator count={items.length} activeIndex={currentIndex} />

app/components/Views/WhatsHappeningDetailView/components/AssetRow.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {
66
BoxAlignItems,
77
BoxFlexDirection,
88
BoxJustifyContent,
9-
ButtonBase,
10-
ButtonBaseSize,
9+
Button,
10+
ButtonSize,
11+
ButtonVariant,
1112
FontWeight,
1213
Text,
1314
TextColor,
@@ -25,7 +26,7 @@ interface AssetRowProps {
2526

2627
/**
2728
* Shared layout for a single asset row (logo + symbol + action button).
28-
* Used by TokenRow (Buy) and PerpsRow (Trade); each wrapper supplies its
29+
* Used by TokenRow (Buy/Trade) and PerpsRow (Trade); each wrapper supplies its
2930
* own hook logic and passes the resolved label and handler here.
3031
*/
3132
const AssetRow: React.FC<AssetRowProps> = ({
@@ -66,14 +67,14 @@ const AssetRow: React.FC<AssetRowProps> = ({
6667
{asset.symbol}
6768
</Text>
6869

69-
<ButtonBase
70-
size={ButtonBaseSize.Md}
71-
twClassName="bg-background-default rounded-2xl px-4"
70+
<Button
71+
variant={ButtonVariant.Primary}
72+
size={ButtonSize.Md}
7273
onPress={onAction}
7374
accessibilityLabel={accessibilityLabel}
7475
>
7576
{actionLabel}
76-
</ButtonBase>
77+
</Button>
7778
</Box>
7879
</Box>
7980
);

app/components/Views/WhatsHappeningDetailView/components/PerpsRow.tsx

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import React, { useCallback } from 'react';
2-
import { useNavigation, NavigationProp } from '@react-navigation/native';
3-
import { PERPS_EVENT_VALUE } from '@metamask/perps-controller';
1+
import React from 'react';
42
import type { RelatedAsset } from '@metamask/ai-controllers';
5-
import type { PerpsNavigationParamList } from '../../../UI/Perps/types/navigation';
6-
import Routes from '../../../../constants/navigation/Routes';
73
import { strings } from '../../../../../locales/i18n';
84
import AssetRow from './AssetRow';
5+
import useTradeNavigation from '../hooks/useTradeNavigation';
96

107
interface PerpsRowProps {
118
asset: RelatedAsset;
@@ -18,19 +15,7 @@ interface PerpsRowProps {
1815
* be called per-asset (hooks cannot be called inside a loop).
1916
*/
2017
const PerpsRow: React.FC<PerpsRowProps> = ({ asset }) => {
21-
const navigation = useNavigation<NavigationProp<PerpsNavigationParamList>>();
22-
const hlPerpsMarket = asset.hlPerpsMarket?.[0];
23-
24-
const handleTrade = useCallback(() => {
25-
if (!hlPerpsMarket) return;
26-
navigation.navigate(Routes.PERPS.ROOT, {
27-
screen: Routes.PERPS.MARKET_DETAILS,
28-
params: {
29-
market: { symbol: hlPerpsMarket, name: asset.name },
30-
source: PERPS_EVENT_VALUE.SOURCE.HOME_SECTION,
31-
},
32-
});
33-
}, [navigation, hlPerpsMarket, asset.name]);
18+
const { handleTrade } = useTradeNavigation(asset);
3419

3520
return (
3621
<AssetRow

app/components/Views/WhatsHappeningDetailView/components/TokenRow.test.tsx

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@ import { screen, fireEvent } from '@testing-library/react-native';
33
import renderWithProvider from '../../../../util/test/renderWithProvider';
44
import TokenRow from './TokenRow';
55
import type { RelatedAsset } from '@metamask/ai-controllers';
6+
import Routes from '../../../../constants/navigation/Routes';
67

78
const mockGoToBuy = jest.fn();
9+
const mockNavigate = jest.fn();
810

911
jest.mock('../../../UI/Ramp/hooks/useRampNavigation', () => ({
1012
useRampNavigation: () => ({ goToBuy: mockGoToBuy }),
1113
}));
1214

15+
jest.mock('@react-navigation/native', () => {
16+
const actual = jest.requireActual('@react-navigation/native');
17+
return {
18+
...actual,
19+
useNavigation: () => ({ navigate: mockNavigate }),
20+
};
21+
});
22+
1323
jest.mock('../utils/getRelatedAssetImageSource', () => ({
1424
getRelatedAssetImageSource: jest.fn(() => undefined),
1525
}));
@@ -21,40 +31,61 @@ const btcAsset: RelatedAsset = {
2131
caip19: ['eip155:1/slip44:0'],
2232
};
2333

24-
const perpsOnlyAsset: RelatedAsset = {
25-
sourceAssetId: 'tsla',
26-
symbol: 'TSLA',
27-
name: 'Tesla',
28-
caip19: [],
29-
hlPerpsMarket: ['xyz:TSLA'],
34+
const dualAsset: RelatedAsset = {
35+
sourceAssetId: 'eth',
36+
symbol: 'ETH',
37+
name: 'Ethereum',
38+
caip19: ['eip155:1/slip44:60'],
39+
hlPerpsMarket: ['ETH'],
3040
};
3141

3242
describe('TokenRow', () => {
3343
beforeEach(() => {
3444
jest.clearAllMocks();
3545
});
3646

37-
it('renders the asset symbol', () => {
38-
renderWithProvider(<TokenRow asset={btcAsset} />);
39-
expect(screen.getByText('BTC')).toBeOnTheScreen();
40-
});
47+
describe('when asset has only caip19 (no hlPerpsMarket)', () => {
48+
it('renders the asset symbol', () => {
49+
renderWithProvider(<TokenRow asset={btcAsset} />);
50+
expect(screen.getByText('BTC')).toBeOnTheScreen();
51+
});
4152

42-
it('renders the Buy button', () => {
43-
renderWithProvider(<TokenRow asset={btcAsset} />);
44-
expect(screen.getByText('Buy')).toBeOnTheScreen();
45-
});
53+
it('renders the Buy button', () => {
54+
renderWithProvider(<TokenRow asset={btcAsset} />);
55+
expect(screen.getByText('Buy')).toBeOnTheScreen();
56+
});
4657

47-
it('calls goToBuy with the first caip19 identifier on Buy press', () => {
48-
renderWithProvider(<TokenRow asset={btcAsset} />);
49-
fireEvent.press(screen.getByText('Buy'));
50-
expect(mockGoToBuy).toHaveBeenCalledWith({
51-
assetId: 'eip155:1/slip44:0',
58+
it('calls goToBuy with the first caip19 identifier on Buy press', () => {
59+
renderWithProvider(<TokenRow asset={btcAsset} />);
60+
fireEvent.press(screen.getByText('Buy'));
61+
expect(mockGoToBuy).toHaveBeenCalledWith({
62+
assetId: 'eip155:1/slip44:0',
63+
});
5264
});
5365
});
5466

55-
it('calls goToBuy with assetId undefined when caip19 is empty', () => {
56-
renderWithProvider(<TokenRow asset={perpsOnlyAsset} />);
57-
fireEvent.press(screen.getByText('Buy'));
58-
expect(mockGoToBuy).toHaveBeenCalledWith({ assetId: undefined });
67+
describe('when asset has hlPerpsMarket (dual asset)', () => {
68+
it('renders the Trade button instead of Buy', () => {
69+
renderWithProvider(<TokenRow asset={dualAsset} />);
70+
expect(screen.getByText('Trade')).toBeOnTheScreen();
71+
expect(screen.queryByText('Buy')).toBeNull();
72+
});
73+
74+
it('navigates to Perps market details on Trade press', () => {
75+
renderWithProvider(<TokenRow asset={dualAsset} />);
76+
fireEvent.press(screen.getByText('Trade'));
77+
expect(mockNavigate).toHaveBeenCalledWith(Routes.PERPS.ROOT, {
78+
screen: Routes.PERPS.MARKET_DETAILS,
79+
params: expect.objectContaining({
80+
market: { symbol: 'ETH', name: 'Ethereum' },
81+
}),
82+
});
83+
});
84+
85+
it('does not call goToBuy when Trade is pressed', () => {
86+
renderWithProvider(<TokenRow asset={dualAsset} />);
87+
fireEvent.press(screen.getByText('Trade'));
88+
expect(mockGoToBuy).not.toHaveBeenCalled();
89+
});
5990
});
6091
});

app/components/Views/WhatsHappeningDetailView/components/TokenRow.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,39 @@ import type { RelatedAsset } from '@metamask/ai-controllers';
33
import { strings } from '../../../../../locales/i18n';
44
import { useRampNavigation } from '../../../UI/Ramp/hooks/useRampNavigation';
55
import AssetRow from './AssetRow';
6+
import useTradeNavigation from '../hooks/useTradeNavigation';
67

78
interface TokenRowProps {
89
asset: RelatedAsset;
910
}
1011

1112
/**
1213
* A single row in the Tokens section of the expanded What's Happening card.
13-
* Displays the token logo, symbol, and a Buy button that navigates to the
14+
* Shows a Trade button (navigating to Perps) when the asset has an
15+
* `hlPerpsMarket` entry; otherwise falls back to a Buy button that opens the
1416
* Ramp buy flow. Extracted as its own component so hooks can be called
1517
* per-asset (hooks cannot be called inside a loop).
1618
*/
1719
const TokenRow: React.FC<TokenRowProps> = ({ asset }) => {
1820
const { goToBuy } = useRampNavigation();
21+
const { handleTrade, canTrade } = useTradeNavigation(asset);
1922

2023
const handleBuy = useCallback(() => {
2124
const assetId = asset.caip19?.[0];
2225
goToBuy({ assetId });
2326
}, [goToBuy, asset.caip19]);
2427

28+
if (canTrade) {
29+
return (
30+
<AssetRow
31+
asset={asset}
32+
actionLabel={strings('bottom_nav.trade')}
33+
accessibilityLabel={`${strings('bottom_nav.trade')} ${asset.symbol}`}
34+
onAction={handleTrade}
35+
/>
36+
);
37+
}
38+
2539
return (
2640
<AssetRow
2741
asset={asset}

0 commit comments

Comments
 (0)