Skip to content

Commit 0c80dc9

Browse files
feat: Disable scroll from wallet tabs, enable wallet scroll (#22044)
## **Description** This PR refactors the wallet page scroll architecture to improve performance and user experience. **What is the reason for the change?** Previously, each wallet tab (Tokens, NFTs, DeFi Positions, Predictions) used virtualized lists (FlashList/FlatList) with individual scroll containers. This approach caused: - Performance overhead from multiple virtualized lists - Complex scroll coordination issues - Inconsistent scroll behavior across tabs - Memory overhead from maintaining multiple list states **What is the improvement/solution?** This PR consolidates scrolling by: 1. **Enabling scroll at the wallet page level** - The entire wallet page now scrolls as a single container 2. **Removing scroll from individual tabs** - Each tab (Tokens, NFTs, DeFi, Predictions) now renders content statically without FlashList/FlatList when scroll is disabled at the tab level 3. **Adding skeleton loaders** - New skeleton components provide better loading states (TokenListSkeleton, NftGridSkeleton) 4. **Adding empty state components** - Improved empty state handling with dedicated components (TokensEmptyState) The changes affect: - **Tokens tab**: Replaced FlashList with static rendering when scroll disabled - **NFT Grid**: Removed FlashList, uses ScrollView-compatible layout - **DeFi Positions**: Removed FlatList in favor of direct rendering - **Predictions tab**: Removed FlashList, uses static layout - **Wallet page**: Added ScrollView wrapper for unified scrolling This architecture pairs with the TabsList auto-height capability (in `fix/tab-individual-height`) to allow each tab to display all content with the page-level scroll handling navigation between sections. ## **Changelog** CHANGELOG entry: Improved wallet page scrolling performance by consolidating scroll behavior at the page level ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/jira/software/c/projects/DSYS/boards/1888?selectedIssue=DSYS-250 ## **Manual testing steps** ```gherkin Feature: Wallet page scroll improvements Scenario: user scrolls through wallet tabs Given user is on the Wallet page with tokens When user scrolls down Then the entire page scrolls smoothly And all tabs remain accessible And tab content loads without flickering Scenario: user switches between tabs Given user is on the Wallet page When user taps on different tabs (Tokens, NFTs, DeFi, Predictions) Then each tab displays content without FlashList artifacts And scroll position resets appropriately per tab And tab transitions are smooth Scenario: user views empty states Given user has no tokens/NFTs/positions When user navigates to empty tabs Then appropriate empty state messages display And layout remains stable Scenario: user views loading states Given wallet data is loading When user opens the wallet page Then skeleton loaders display for each tab And transitions to actual content are smooth ``` ## **Screenshots/Recordings** ### **Before** https://github.com/user-attachments/assets/28e93320-e519-4fc4-860f-1e5477d90bb9 https://github.com/user-attachments/assets/912fd548-c613-4fb5-9b5b-8dd864f20d2d ### **After** When homepage redesign feature flag V1 is false - Tokens Tab https://github.com/user-attachments/assets/15809592-6066-48ee-a8b7-2af0a03cdb55 - NFTs Tab https://github.com/user-attachments/assets/8b821113-fcf8-4416-a629-740571d49d59 - Perps Tab https://github.com/user-attachments/assets/b364311c-6162-4f2a-bfbc-476daea5587a - Defi Tab https://github.com/user-attachments/assets/c59acab7-0cfa-4a75-92c5-4eb0bcd7b2a3 - Predictions Tab https://github.com/user-attachments/assets/1f584e55-a0fd-40d9-9942-7ceb5335837f When homepage redesign feature flag V1 is true - Tokens Tab https://github.com/user-attachments/assets/c10f0f2a-aa50-4f38-be42-b65d7f28d521 - NFTs Tab https://github.com/user-attachments/assets/0263d21c-0bbc-4e57-8784-37eeecbdea60 - Perps Tab https://github.com/user-attachments/assets/602db546-455c-462b-ab87-8f74624d0d73 - Defi Tab https://github.com/user-attachments/assets/4a154aae-2245-42dd-bdca-e19a15eea68b - Predictions Tab https://github.com/user-attachments/assets/8fdfeeed-3cee-4ec2-b627-3fd8fe966166 <!-- Add recordings showing the new unified scroll behavior --> ## **Pre-merge author checklist** - [x] 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). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] 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. ## **Pre-merge reviewer checklist** - [ ] 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] > Enables global wallet scrolling and refactors Tokens/NFTs/DeFi/Perps/Predict tabs to render without inner scroll (behind homepageRedesignV1), adding skeleton loaders and empty states. > > - **Wallet**: > - **Global Scroll**: Wraps wallet content in `ConditionalScrollView` (new) to enable page-level scrolling; tabs pass content without inner scroll when `homepageRedesignV1` is enabled. > - **New Components**: > - `components-temp/ConditionalScrollView` (+ types, tests). > - `TokensEmptyState` and `TokenListSkeleton`; `NftGridSkeleton`. > - **Tabs Refactor (flag-driven)**: > - **Tokens**: `TokenList` renders items directly (no `FlashList`) when not full view; adds skeleton and empty state; simplifies `Tokens` (removes progressive loader) and applies `maxItems` (10) for homepage redesign. > - **NFTs**: `NftGrid` renders grid directly or via `FlashList` in full view; adds skeleton, limits to 18 items with "View all"; UI tweaks in `NftGridItem`. > - **DeFi Positions**: Replace `FlatList` with direct render; optional scroll via `ConditionalScrollView` and new testID for scroll view. > - **Perps**: Use `ConditionalScrollView`; loading skeleton layout adapts to flag. > - **Predict**: Replace `FlashList` with direct render; `PredictTabView` uses `ConditionalScrollView`. > - **UI/Styling**: > - Centered `CollectibleMedia` fallback text; minor layout cleanups; token header back button `testID`. > - **Tests & E2E**: > - Extensive unit test updates for new render/flag behavior; add skeleton/empty-state tests; update selectors (e.g., `DEFI_POSITIONS_SCROLL_VIEW`) and e2e token matcher to fetch first instance. > - **Localization**: > - Adds `wallet.tokens_empty_description`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6bedb5f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent e0a8a4b commit 0c80dc9

File tree

49 files changed

+2278
-649
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2278
-649
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react-native';
3+
import { Text, View } from 'react-native';
4+
import ConditionalScrollView from './ConditionalScrollView';
5+
6+
describe('ConditionalScrollView', () => {
7+
const testContent = (
8+
<View>
9+
<Text>Test Content</Text>
10+
</View>
11+
);
12+
13+
describe('when isScrollEnabled is true', () => {
14+
it('wraps children in ScrollView and renders content', () => {
15+
const { getByTestId, getByText } = render(
16+
<ConditionalScrollView
17+
isScrollEnabled
18+
scrollViewProps={{ testID: 'scroll-container' }}
19+
>
20+
{testContent}
21+
</ConditionalScrollView>,
22+
);
23+
24+
expect(getByTestId('scroll-container')).toBeDefined();
25+
expect(getByText('Test Content')).toBeDefined();
26+
});
27+
28+
it('passes scrollViewProps to ScrollView', () => {
29+
const testID = 'test-scroll-view';
30+
const { getByTestId } = render(
31+
<ConditionalScrollView
32+
isScrollEnabled
33+
scrollViewProps={{
34+
testID,
35+
showsVerticalScrollIndicator: false,
36+
bounces: false,
37+
}}
38+
>
39+
{testContent}
40+
</ConditionalScrollView>,
41+
);
42+
43+
const scrollView = getByTestId(testID);
44+
expect(scrollView.props.showsVerticalScrollIndicator).toBe(false);
45+
expect(scrollView.props.bounces).toBe(false);
46+
});
47+
});
48+
49+
describe('when isScrollEnabled is false', () => {
50+
it('renders children without ScrollView wrapper', () => {
51+
const { getByText, queryByTestId } = render(
52+
<ConditionalScrollView
53+
isScrollEnabled={false}
54+
scrollViewProps={{ testID: 'should-not-exist' }}
55+
>
56+
{testContent}
57+
</ConditionalScrollView>,
58+
);
59+
60+
expect(queryByTestId('should-not-exist')).toBeNull();
61+
expect(getByText('Test Content')).toBeDefined();
62+
});
63+
});
64+
65+
describe('dynamic behavior', () => {
66+
it('switches between ScrollView and direct rendering when isScrollEnabled changes', () => {
67+
const result = render(
68+
<ConditionalScrollView
69+
isScrollEnabled
70+
scrollViewProps={{ testID: 'scroll-view' }}
71+
>
72+
{testContent}
73+
</ConditionalScrollView>,
74+
);
75+
76+
expect(result.getByTestId('scroll-view')).toBeDefined();
77+
78+
result.rerender(
79+
<ConditionalScrollView
80+
isScrollEnabled={false}
81+
scrollViewProps={{ testID: 'scroll-view' }}
82+
>
83+
{testContent}
84+
</ConditionalScrollView>,
85+
);
86+
87+
expect(result.queryByTestId('scroll-view')).toBeNull();
88+
89+
result.rerender(
90+
<ConditionalScrollView
91+
isScrollEnabled
92+
scrollViewProps={{ testID: 'scroll-view' }}
93+
>
94+
{testContent}
95+
</ConditionalScrollView>,
96+
);
97+
98+
expect(result.getByTestId('scroll-view')).toBeDefined();
99+
});
100+
});
101+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react';
2+
import { ScrollView } from 'react-native';
3+
import { ConditionalScrollViewProps } from './ConditionalScrollView.types';
4+
5+
/**
6+
* ConditionalScrollView renders either a ScrollView or content directly based on isScrollEnabled prop.
7+
* This is useful for homepage redesign where we want to remove nested scroll views in favor of a global scroll container.
8+
*/
9+
const ConditionalScrollView: React.FC<ConditionalScrollViewProps> = ({
10+
children,
11+
isScrollEnabled,
12+
scrollViewProps,
13+
}) =>
14+
isScrollEnabled ? (
15+
<ScrollView {...scrollViewProps}>{children}</ScrollView>
16+
) : (
17+
<>{children}</>
18+
);
19+
20+
export default ConditionalScrollView;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ScrollViewProps } from 'react-native';
2+
3+
export interface ConditionalScrollViewProps {
4+
/**
5+
* Content to render inside the conditional scroll view
6+
*/
7+
children: React.ReactNode;
8+
/**
9+
* If true, wraps children in ScrollView. If false, renders children directly.
10+
*/
11+
isScrollEnabled: boolean;
12+
/**
13+
* Optional props to pass to ScrollView when isScrollEnabled is true
14+
*/
15+
scrollViewProps?: ScrollViewProps;
16+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './ConditionalScrollView';

app/components/UI/AssetElement/__snapshots__/index.test.tsx.snap

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ exports[`AssetElement should render correctly 1`] = `
88
style={
99
{
1010
"alignItems": "center",
11-
"flex": 1,
1211
"flexDirection": "row",
1312
"height": 64,
1413
}

app/components/UI/AssetElement/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ interface AssetElementProps {
4040
const createStyles = (colors: Colors) =>
4141
StyleSheet.create({
4242
itemWrapper: {
43-
flex: 1,
4443
flexDirection: 'row',
4544
height: 64,
4645
alignItems: 'center',

app/components/UI/AssetOverview/Balance/__snapshots__/index.test.tsx.snap

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ exports[`Balance should render correctly with main and secondary balance 1`] = `
3131
style={
3232
{
3333
"alignItems": "center",
34-
"flex": 1,
3534
"flexDirection": "row",
3635
"height": 64,
3736
}
@@ -268,7 +267,6 @@ exports[`Balance should render correctly without a secondary balance 1`] = `
268267
style={
269268
{
270269
"alignItems": "center",
271-
"flex": 1,
272270
"flexDirection": "row",
273271
"height": 64,
274272
}

app/components/UI/Card/components/CardAssetItem/__snapshots__/CardAssetItem.test.tsx.snap

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,6 @@ exports[`CardAssetItem Component handles test network correctly 1`] = `
319319
style={
320320
{
321321
"alignItems": "center",
322-
"flex": 1,
323322
"flexDirection": "row",
324323
"height": 64,
325324
}
@@ -852,7 +851,6 @@ exports[`CardAssetItem Component renders non-native token and matches snapshot 1
852851
style={
853852
{
854853
"alignItems": "center",
855-
"flex": 1,
856854
"flexDirection": "row",
857855
"height": 64,
858856
}
@@ -1329,7 +1327,6 @@ exports[`CardAssetItem Component renders with all props and matches snapshot 1`]
13291327
style={
13301328
{
13311329
"alignItems": "center",
1332-
"flex": 1,
13331330
"flexDirection": "row",
13341331
"height": 64,
13351332
}
@@ -1801,7 +1798,6 @@ exports[`CardAssetItem Component renders with privacy mode enabled and matches s
18011798
style={
18021799
{
18031800
"alignItems": "center",
1804-
"flex": 1,
18051801
"flexDirection": "row",
18061802
"height": 64,
18071803
}
@@ -2273,7 +2269,6 @@ exports[`CardAssetItem Component renders with required props and matches snapsho
22732269
style={
22742270
{
22752271
"alignItems": "center",
2276-
"flex": 1,
22772272
"flexDirection": "row",
22782273
"height": 64,
22792274
}

app/components/UI/CollectibleMedia/CollectibleMedia.styles.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,16 @@ const styleSheet = (params: {
4949
borderRadius: 12,
5050
},
5151
textContainer: {
52+
flex: 1,
5253
alignItems: 'center',
53-
justifyContent: 'flex-start',
54+
justifyContent: 'center',
5455
backgroundColor: colors.background.section,
5556
borderRadius: 8,
5657
},
5758
textWrapper: {
58-
flex: 1,
5959
textAlign: 'center',
60-
marginTop: 16,
60+
alignItems: 'center',
61+
justifyContent: 'center',
6162
},
6263
textWrapperIcon: {
6364
fontSize: 18,

app/components/UI/CollectibleModal/__snapshots__/CollectibleModal.test.tsx.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ exports[`CollectibleModal should render correctly 1`] = `
116116
"alignItems": "center",
117117
"backgroundColor": "#f3f5f9",
118118
"borderRadius": 8,
119-
"justifyContent": "flex-start",
119+
"flex": 1,
120+
"justifyContent": "center",
120121
},
121122
{
122123
"borderRadius": 12,

0 commit comments

Comments
 (0)