Skip to content

Commit

Permalink
feat: Token Network Filter UI [Mobile] (#11808)
Browse files Browse the repository at this point in the history
## **Description**

This PR introduces token network filter UI component. It lives behind a
feature flag `PORTFOLIO_VIEW` in order to allow this to get merged while
backend changes are in flight.

Integration with the multichain asset list will occur in a separate PR.
We'll likely want to add additional e2e tests when that happens.

### Running this branch

`yarn && yarn setup`
`PORTFOLIO_VIEW=1 yarn watch`
`yarn start:ios` or `i` from the watch process

Please ensure that this PR looks okay both with and without the feature
flag running. I have introduced a horizontal scroll view because there
simply was not enough screen real estate to make the network filter look
okay with truncated text.

## **Related issues**

Fixes:

## **Manual testing steps**

1. Run app with feature flag
2. Go to main Portfolio view
3. Pressing the network filter should trigger the bottom sheet, allowing
the user to select different filter options.

## **Screenshots/Recordings**


https://github.com/user-attachments/assets/636d1627-5da3-4826-83a1-924ff4f28603

## **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.

---------

Signed-off-by: Kai Huang <kai.huang@consensys.net>
Co-authored-by: salimtb <salim.toubal@outlook.com>
Co-authored-by: Vince Howard <vincenguyenhoward@gmail.com>
Co-authored-by: sethkfman <10342624+sethkfman@users.noreply.github.com>
Co-authored-by: runway-github[bot] <73448015+runway-github[bot]@users.noreply.github.com>
Co-authored-by: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com>
Co-authored-by: metamaskbot <metamaskbot@users.noreply.github.com>
Co-authored-by: tommasini <46944231+tommasini@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Xiaoming Wang <7315988+dawnseeker8@users.noreply.github.com>
Co-authored-by: Nico MASSART <NicolasMassart@users.noreply.github.com>
Co-authored-by: Xiaoming Wang <dawnseeker8@gmail.com>
Co-authored-by: Nicholas Ellul <nicholas.ellul1@gmail.com>
Co-authored-by: legobt <6wbvkn0j@anonaddy.me>
Co-authored-by: OGPoyraz <omergoktugpoyraz@gmail.com>
Co-authored-by: Pedro Pablo Aste Kompen <wachunei@gmail.com>
Co-authored-by: legobeat <109787230+legobeat@users.noreply.github.com>
Co-authored-by: tommasini <tommasini15@gmail.com>
Co-authored-by: Bryan Fullam <bryanfullam@gmail.com>
Co-authored-by: Mpendulo Ndlovu <mpendulo@elefantel.com>
Co-authored-by: Kaihuang72490 <147628638+Kaihuang72490@users.noreply.github.com>
Co-authored-by: Cal-L <cleun007@gmail.com>
Co-authored-by: Cal Leung <cal.leung@consensys.net>
Co-authored-by: sahar-fehri <sahar.fehri@consensys.net>
Co-authored-by: Matthew Walsh <matthew.walsh@consensys.net>
  • Loading branch information
1 parent 7a49a4c commit de05544
Show file tree
Hide file tree
Showing 17 changed files with 390 additions and 43 deletions.
7 changes: 6 additions & 1 deletion app/components/Nav/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ import Toast, {
ToastContext,
} from '../../../component-library/components/Toast';
import AccountSelector from '../../../components/Views/AccountSelector';
import TokenSortBottomSheet from '../../../components/UI/Tokens/TokensBottomSheet/TokenSortBottomSheet.tsx';
import { TokenSortBottomSheet } from '../../../components/UI/Tokens/TokensBottomSheet/TokenSortBottomSheet.tsx';
import { TokenFilterBottomSheet } from '../../../components/UI/Tokens/TokensBottomSheet/TokenFilterBottomSheet.tsx';
import AccountConnect from '../../../components/Views/AccountConnect';
import AccountPermissions from '../../../components/Views/AccountPermissions';
import { AccountPermissionsScreens } from '../../../components/Views/AccountPermissions/AccountPermissions.types';
Expand Down Expand Up @@ -442,6 +443,10 @@ const RootModalFlow = () => (
name={Routes.SHEET.TOKEN_SORT}
component={TokenSortBottomSheet}
/>
<Stack.Screen
name={Routes.SHEET.TOKEN_FILTER}
component={TokenFilterBottomSheet}
/>
<Stack.Screen
name={Routes.SHEET.BASIC_FUNCTIONALITY}
component={BasicFunctionalityModal}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { TokenFilterBottomSheet } from './TokenFilterBottomSheet';
import { useSelector } from 'react-redux';
import Engine from '../../../../core/Engine';
import { selectChainId } from '../../../../selectors/networkController';
import { selectTokenNetworkFilter } from '../../../../selectors/preferencesController';

jest.mock('react-redux', () => ({
useSelector: jest.fn(),
}));

jest.mock('../../../../util/theme', () => ({
useTheme: jest.fn(() => ({ colors: {} })),
}));

jest.mock('../../../../core/Engine', () => ({
context: {
PreferencesController: {
setTokenNetworkFilter: jest.fn(),
},
},
}));

jest.mock('@react-navigation/native', () => {
const reactNavigationModule = jest.requireActual('@react-navigation/native');
return {
...reactNavigationModule,
useNavigation: () => ({
navigate: jest.fn(),
goBack: jest.fn(),
}),
};
});

jest.mock('react-native-safe-area-context', () => {
// copied from BottomSheetDialog.test.tsx
const inset = { top: 1, right: 2, bottom: 3, left: 4 };
const frame = { width: 5, height: 6, x: 7, y: 8 };
return {
SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children),
SafeAreaConsumer: jest
.fn()
.mockImplementation(({ children }) => children(inset)),
useSafeAreaInsets: jest.fn().mockImplementation(() => inset),
useSafeAreaFrame: jest.fn().mockImplementation(() => frame),
};
});

describe('TokenFilterBottomSheet', () => {
beforeEach(() => {
(useSelector as jest.Mock).mockImplementation((selector) => {
if (selector === selectChainId) {
return '0x1'; // default chain ID
} else if (selector === selectTokenNetworkFilter) {
return {}; // default to show all networks
}
return null;
});
});

afterEach(() => {
jest.clearAllMocks();
});

it('renders correctly with the default option (All Networks) selected', () => {
const { queryByText } = render(<TokenFilterBottomSheet />);

expect(queryByText('All Networks')).toBeTruthy();
expect(queryByText('Current Network')).toBeTruthy();
});

it('sets filter to All Networks and closes bottom sheet when first option is pressed', async () => {
const { queryByText } = render(<TokenFilterBottomSheet />);

fireEvent.press(queryByText('All Networks'));

await waitFor(() => {
expect(
Engine.context.PreferencesController.setTokenNetworkFilter,
).toHaveBeenCalledWith({});
});
});

it('sets filter to Current Network and closes bottom sheet when second option is pressed', async () => {
const { queryByText } = render(<TokenFilterBottomSheet />);

fireEvent.press(queryByText('Current Network'));

await waitFor(() => {
expect(
Engine.context.PreferencesController.setTokenNetworkFilter,
).toHaveBeenCalledWith({
'0x1': true,
});
});
});

it('displays the correct selection based on tokenNetworkFilter', () => {
(useSelector as jest.Mock).mockImplementation((selector) => {
if (selector === selectChainId) {
return '0x1';
} else if (selector === selectTokenNetworkFilter) {
return { '0x1': true }; // filter by current network
}
return null;
});

const { queryByText } = render(<TokenFilterBottomSheet />);

expect(queryByText('Current Network')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useRef } from 'react';
import { useSelector } from 'react-redux';
import { selectChainId } from '../../../../selectors/networkController';
import { selectTokenNetworkFilter } from '../../../../selectors/preferencesController';
import BottomSheet, {
BottomSheetRef,
} from '../../../../component-library/components/BottomSheets/BottomSheet';
import { useTheme } from '../../../../util/theme';
import createStyles from '../styles';
import Engine from '../../../../core/Engine';
import { View } from 'react-native';
import Text, {
TextVariant,
} from '../../../../component-library/components/Texts/Text';
import ListItemSelect from '../../../../component-library/components/List/ListItemSelect';
import { VerticalAlignment } from '../../../../component-library/components/List/ListItem';
import { strings } from '../../../../../locales/i18n';

enum FilterOption {
AllNetworks = 0,
CurrentNetwork = 1,
}

const TokenFilterBottomSheet = () => {
const sheetRef = useRef<BottomSheetRef>(null);
const { colors } = useTheme();
const styles = createStyles(colors);

const chainId = useSelector(selectChainId);
const tokenNetworkFilter = useSelector(selectTokenNetworkFilter);

const onFilterControlsBottomSheetPress = (option: FilterOption) => {
const { PreferencesController } = Engine.context;
switch (option) {
case FilterOption.AllNetworks:
PreferencesController.setTokenNetworkFilter({});
sheetRef.current?.onCloseBottomSheet();
break;
case FilterOption.CurrentNetwork:
PreferencesController.setTokenNetworkFilter({
[chainId]: true,
});
sheetRef.current?.onCloseBottomSheet();
break;
default:
break;
}
};

const isSelectedNetwork = Boolean(tokenNetworkFilter?.[chainId]);

return (
<BottomSheet shouldNavigateBack ref={sheetRef}>
<View style={styles.bottomSheetWrapper}>
<Text variant={TextVariant.HeadingMD} style={styles.bottomSheetTitle}>
{strings('wallet.filter_by')}
</Text>
<ListItemSelect
onPress={() =>
onFilterControlsBottomSheetPress(FilterOption.AllNetworks)
}
isSelected={!isSelectedNetwork}
gap={8}
verticalAlignment={VerticalAlignment.Center}
>
<Text style={styles.bottomSheetText}>
{strings('wallet.all_networks')}
</Text>
</ListItemSelect>
<ListItemSelect
onPress={() =>
onFilterControlsBottomSheetPress(FilterOption.CurrentNetwork)
}
isSelected={isSelectedNetwork}
gap={8}
verticalAlignment={VerticalAlignment.Center}
>
<Text style={styles.bottomSheetText}>
{strings('wallet.current_network')}
</Text>
</ListItemSelect>
</View>
</BottomSheet>
);
};

export { TokenFilterBottomSheet };
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import TokenSortBottomSheet from './TokenSortBottomSheet';
import { TokenSortBottomSheet } from './TokenSortBottomSheet';
import { useSelector } from 'react-redux';
import Engine from '../../../../core/Engine';
import { selectTokenSortConfig } from '../../../../selectors/preferencesController';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,4 @@ const TokenSortBottomSheet = () => {
);
};

export default TokenSortBottomSheet;
export { TokenSortBottomSheet };
9 changes: 8 additions & 1 deletion app/components/UI/Tokens/TokensBottomSheet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,11 @@ export const createTokensBottomSheetNavDetails = createNavigationDetails(
Routes.MODAL.ROOT_MODAL_FLOW,
Routes.SHEET.TOKEN_SORT,
);
export { default } from './TokenSortBottomSheet';

export const createTokenBottomSheetFilterNavDetails = createNavigationDetails(
Routes.MODAL.ROOT_MODAL_FLOW,
Routes.SHEET.TOKEN_FILTER,
);

export { TokenSortBottomSheet } from './TokenSortBottomSheet';
export { TokenFilterBottomSheet } from './TokenFilterBottomSheet';
33 changes: 27 additions & 6 deletions app/components/UI/Tokens/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,8 @@ exports[`Tokens should hide zero balance tokens when setting is on 1`] = `
"flexDirection": "row",
"justifyContent": "space-between",
"paddingBottom": 16,
"paddingLeft": 16,
"paddingRight": 16,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
}
}
Expand All @@ -340,9 +340,13 @@ exports[`Tokens should hide zero balance tokens when setting is on 1`] = `
"flexDirection": "row",
"height": 40,
"justifyContent": "center",
"marginLeft": 5,
"marginRight": 5,
"maxWidth": "60%",
"paddingHorizontal": 16,
}
}
testID="sort-by"
>
<Text
accessibilityRole="none"
Expand Down Expand Up @@ -390,6 +394,9 @@ exports[`Tokens should hide zero balance tokens when setting is on 1`] = `
"flexDirection": "row",
"height": 40,
"justifyContent": "center",
"marginLeft": 5,
"marginRight": 5,
"maxWidth": "60%",
"paddingHorizontal": 16,
}
}
Expand Down Expand Up @@ -1563,8 +1570,8 @@ exports[`Tokens should render correctly 1`] = `
"flexDirection": "row",
"justifyContent": "space-between",
"paddingBottom": 16,
"paddingLeft": 16,
"paddingRight": 16,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
}
}
Expand All @@ -1586,9 +1593,13 @@ exports[`Tokens should render correctly 1`] = `
"flexDirection": "row",
"height": 40,
"justifyContent": "center",
"marginLeft": 5,
"marginRight": 5,
"maxWidth": "60%",
"paddingHorizontal": 16,
}
}
testID="sort-by"
>
<Text
accessibilityRole="none"
Expand Down Expand Up @@ -1636,6 +1647,9 @@ exports[`Tokens should render correctly 1`] = `
"flexDirection": "row",
"height": 40,
"justifyContent": "center",
"marginLeft": 5,
"marginRight": 5,
"maxWidth": "60%",
"paddingHorizontal": 16,
}
}
Expand Down Expand Up @@ -2809,8 +2823,8 @@ exports[`Tokens should show all balance tokens when hideZeroBalanceTokens settin
"flexDirection": "row",
"justifyContent": "space-between",
"paddingBottom": 16,
"paddingLeft": 16,
"paddingRight": 16,
"paddingLeft": 8,
"paddingRight": 8,
"paddingTop": 8,
}
}
Expand All @@ -2832,9 +2846,13 @@ exports[`Tokens should show all balance tokens when hideZeroBalanceTokens settin
"flexDirection": "row",
"height": 40,
"justifyContent": "center",
"marginLeft": 5,
"marginRight": 5,
"maxWidth": "60%",
"paddingHorizontal": 16,
}
}
testID="sort-by"
>
<Text
accessibilityRole="none"
Expand Down Expand Up @@ -2882,6 +2900,9 @@ exports[`Tokens should show all balance tokens when hideZeroBalanceTokens settin
"flexDirection": "row",
"height": 40,
"justifyContent": "center",
"marginLeft": 5,
"marginRight": 5,
"maxWidth": "60%",
"paddingHorizontal": 16,
}
}
Expand Down
4 changes: 2 additions & 2 deletions app/components/UI/Tokens/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,9 @@ describe('Tokens', () => {
});

it('triggers bottom sheet when sort controls are pressed', async () => {
const { getByText } = renderComponent(initialState);
const { getByTestId } = renderComponent(initialState);

await fireEvent.press(getByText('Sort by'));
await fireEvent.press(getByTestId(WalletViewSelectorsIDs.SORT_BY));

await waitFor(() => {
expect(createTokensBottomSheetNavDetails).toHaveBeenCalledWith({});
Expand Down
Loading

0 comments on commit de05544

Please sign in to comment.