From ea14ef75de9bf7291a22d7b4286e4652209d5389 Mon Sep 17 00:00:00 2001 From: EtherWizard33 <165834542+EtherWizard33@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:43:42 +0300 Subject: [PATCH] feat: edit networks UI redesign (#10040) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR adds the ability to edit networks. This PR also includes Salim's work from the feat-edit-delete-network-menu branch. For a short demo please view this loom: https://www.loom.com/share/1192d8d7846845e69da9894def13aeeb ## **Related issues** Fixes: ## **Manual testing steps** 1. Click the network selector at the center top of the screen, a bottom sheet comes up with a list of networks 2. On the right side of enabled networks, a 3 dot icon is displayed as a 'more' menu, tap it to edit the network 3. The edit form shows up pre-filled with the network details to be edited ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] 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. ## **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. --------- Co-authored-by: salimtb --- .storybook/storybook.requires.js | 1 + .../CellSelectWithMenu.constants.ts | 34 ++ .../CellSelectWithMenu.stories.tsx | 41 +++ .../CellSelectWithMenu.styles.ts | 35 ++ .../CellSelectWithMenu.test.tsx | 24 ++ .../CellSelectWithMenu/CellSelectWithMenu.tsx | 50 +++ .../CellSelectWithMenu.types.ts | 18 + .../CellSelectWithMenu.test.tsx.snap | 317 +++++++++++++++++ .../CellSelectWithMenu/index.ts | 1 + .../ListItemMultiSelectButton.constants.ts | 19 + .../ListItemMultiSelectButton.stories.tsx | 53 +++ .../ListItemMultiSelectButton.styles.ts | 65 ++++ .../ListItemMultiSelectButton.test.tsx | 75 ++++ .../ListItemMultiSelectButton.tsx | 71 ++++ .../ListItemMultiSelectButton.types.ts | 43 +++ .../ListItemMultiSelectButton.test.tsx.snap | 79 +++++ .../ListItemMultiSelectButton/index.ts | 1 + .../components/Cells/Cell/Cell.tsx | 8 + .../components/Cells/Cell/Cell.types.ts | 3 + .../NetworkSearchTextInput.styles.ts | 18 + .../NetworkSearchTextInput.tsx | 37 +- .../NetworkSelector/NetworkSelector.styles.ts | 50 +++ .../Views/NetworkSelector/NetworkSelector.tsx | 333 ++++++++++++++++-- .../CustomNetworkView/CustomNetwork.tsx | 5 +- .../CustomNetworkView/CustomNetwork.types.ts | 11 + e2e/selectors/Modals/CellModal.selectors.js | 1 + .../Modals/NetworkListModal.selectors.js | 1 + storybook/storyLoader.js | 2 + 28 files changed, 1369 insertions(+), 27 deletions(-) create mode 100644 app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.constants.ts create mode 100644 app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx create mode 100644 app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts create mode 100644 app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx create mode 100644 app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx create mode 100644 app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.types.ts create mode 100644 app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap create mode 100644 app/component-library/components-temp/CellSelectWithMenu/index.ts create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap create mode 100644 app/component-library/components-temp/ListItemMultiSelectButton/index.ts diff --git a/.storybook/storybook.requires.js b/.storybook/storybook.requires.js index b2fdc79d50a..8c929c9fe7d 100644 --- a/.storybook/storybook.requires.js +++ b/.storybook/storybook.requires.js @@ -116,6 +116,7 @@ const getStories = () => { './app/component-library/components-temp/TagColored/TagColored.stories.tsx': require('../app/component-library/components-temp/TagColored/TagColored.stories.tsx'), './app/components/UI/Name/Name.stories.tsx': require('../app/components/UI/Name/Name.stories.tsx'), "./app/components/UI/SimulationDetails/SimulationDetails.stories.tsx": require("../app/components/UI/SimulationDetails/SimulationDetails.stories.tsx"), + "./app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx": require("../app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx"), }; }; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.constants.ts b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.constants.ts new file mode 100644 index 00000000000..05abf2a0d46 --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.constants.ts @@ -0,0 +1,34 @@ +// External dependencies. +import { IconName } from '../../../component-library/components/Icons/Icon'; +import { + AvatarVariant, + AvatarAccountType, +} from '../../../component-library/components/Avatars/Avatar'; +import { AvatarProps } from '../../../component-library/components/Avatars/Avatar/Avatar.types'; + +// Internal dependencies. +import { CellSelectWithMenuProps } from './CellSelectWithMenu.types'; + +// Sample consts +const SAMPLE_CELLSELECT_WITH_BUTTON_TITLE = 'Orangefox.eth'; +const SAMPLE_CELLSELECT_WITH_BUTTON_SECONDARYTEXT = + '0x2990079bcdEe240329a520d2444386FC119da21a'; +const SAMPLE_CELLSELECT_WITH_BUTTON_TERTIARY_TEXT = 'Updated 1 sec ago'; +const SAMPLE_CELLSELECT_WITH_BUTTON_TAGLABEL = 'Imported'; +const SAMPLE_CELLSELECT_WITH_BUTTON_AVATARPROPS: AvatarProps = { + variant: AvatarVariant.Account, + accountAddress: '0x2990079bcdEe240329a520d2444386FC119da21a', + type: AvatarAccountType.JazzIcon, +}; + +// eslint-disable-next-line import/prefer-default-export +export const SAMPLE_CELLSELECT_WITH_BUTTON_PROPS: CellSelectWithMenuProps = { + title: SAMPLE_CELLSELECT_WITH_BUTTON_TITLE, + secondaryText: SAMPLE_CELLSELECT_WITH_BUTTON_SECONDARYTEXT, + tertiaryText: SAMPLE_CELLSELECT_WITH_BUTTON_TERTIARY_TEXT, + tagLabel: SAMPLE_CELLSELECT_WITH_BUTTON_TAGLABEL, + avatarProps: SAMPLE_CELLSELECT_WITH_BUTTON_AVATARPROPS, + isSelected: false, + isDisabled: false, + buttonIcon: IconName.MoreVertical, +}; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx new file mode 100644 index 00000000000..46e8a658e40 --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.stories.tsx @@ -0,0 +1,41 @@ +// Internal dependencies. +import { default as CellSelectWithMenu } from './CellSelectWithMenu'; +import { SAMPLE_CELLSELECT_WITH_BUTTON_PROPS } from './CellSelectWithMenu.constants'; + +const CellSelectWithMenuMeta = { + title: 'Component Library / Cells', + component: CellSelectWithMenu, + argTypes: { + title: { + control: { type: 'text' }, + defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.title, + }, + secondaryText: { + control: { type: 'text' }, + defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.secondaryText, + }, + tertiaryText: { + control: { type: 'text' }, + defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.tertiaryText, + }, + tagLabel: { + control: { type: 'text' }, + defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.tagLabel, + }, + isSelected: { + control: { type: 'boolean' }, + defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.isSelected, + }, + isDisabled: { + control: { type: 'boolean' }, + defaultValue: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.isDisabled, + }, + }, +}; +export default CellSelectWithMenuMeta; + +export const CellMultiSelectWithMenu = { + args: { + avatarProps: SAMPLE_CELLSELECT_WITH_BUTTON_PROPS.avatarProps, + }, +}; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts new file mode 100644 index 00000000000..9f4933dfc18 --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.styles.ts @@ -0,0 +1,35 @@ +// Third library dependencies. +import { StyleSheet, ViewStyle } from 'react-native'; + +// External dependencies. +import { CellSelectWithMenuStyleSheetVars } from './CellSelectWithMenu.types'; + +// Internal dependencies. +import { Theme } from '../../../util/theme/models'; + +/** + * Style sheet function for CellSelect component. + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { + theme: Theme; + vars: CellSelectWithMenuStyleSheetVars; +}) => { + const { vars } = params; + const { style } = vars; + + return StyleSheet.create({ + base: Object.assign( + { + padding: 16, + } as ViewStyle, + style, + ) as ViewStyle, + }); +}; + +export default styleSheet; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx new file mode 100644 index 00000000000..280df813ba8 --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.test.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; + +import CellSelectWithMenu from './CellSelectWithMenu'; +import { CellModalSelectorsIDs } from '../../../../e2e/selectors/Modals/CellModal.selectors'; + +import { SAMPLE_CELLSELECT_WITH_BUTTON_PROPS } from './CellSelectWithMenu.constants'; + +describe('CellSelectWithMenu', () => { + it('should render with default settings correctly', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render CellSelectWithMenu', () => { + const { queryByTestId } = render( + , + ); + // Adjust the testID to match the one used in CellSelectWithMenu, if different + expect(queryByTestId(CellModalSelectorsIDs.MULTISELECT)).not.toBe(null); + }); +}); diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx new file mode 100644 index 00000000000..d349d01ad4f --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.tsx @@ -0,0 +1,50 @@ +/* eslint-disable react/prop-types */ + +// Third library dependencies. +import React from 'react'; + +// External dependencies. +import { useStyles } from '../../hooks'; +import CellBase from '../../../component-library/components/Cells/Cell/foundation/CellBase'; + +// Internal dependencies. +import styleSheet from './CellSelectWithMenu.styles'; +import { CellSelectWithMenuProps } from './CellSelectWithMenu.types'; +import { CellModalSelectorsIDs } from '../../../../e2e/selectors/Modals/CellModal.selectors'; +import ListItemMultiSelectButton from '../ListItemMultiSelectButton/ListItemMultiSelectButton'; + +const CellSelectWithMenu = ({ + style, + avatarProps, + title, + secondaryText, + tertiaryText, + tagLabel, + isSelected = false, + children, + ...props +}: CellSelectWithMenuProps) => { + const { styles } = useStyles(styleSheet, { style }); + + return ( + + + {children} + + + ); +}; + +export default CellSelectWithMenu; diff --git a/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.types.ts b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.types.ts new file mode 100644 index 00000000000..d7b8282bfdb --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/CellSelectWithMenu.types.ts @@ -0,0 +1,18 @@ +// External dependencies. +import { CellBaseProps } from '../../../component-library/components/Cells/Cell/foundation/CellBase/CellBase.types'; +import { ListItemMultiSelectButtonProps } from '../ListItemMultiSelectButton/ListItemMultiSelectButton.types'; + +/** + * Cell Account Select component props. + */ +export interface CellSelectWithMenuProps + extends CellBaseProps, + Omit {} + +/** + * Style sheet input parameters. + */ +export type CellSelectWithMenuStyleSheetVars = Pick< + CellSelectWithMenuProps, + 'style' +>; diff --git a/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap new file mode 100644 index 00000000000..d091087055c --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/__snapshots__/CellSelectWithMenu.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CellSelectWithMenu should render with default settings correctly 1`] = ` + + + + + + + + + + + + + + + + + + Orangefox.eth + + + 0x2990079bcdEe240329a520d2444386FC119da21a + + + Updated 1 sec ago + + + + Imported + + + + + + + + + + + + +`; diff --git a/app/component-library/components-temp/CellSelectWithMenu/index.ts b/app/component-library/components-temp/CellSelectWithMenu/index.ts new file mode 100644 index 00000000000..814c64969c4 --- /dev/null +++ b/app/component-library/components-temp/CellSelectWithMenu/index.ts @@ -0,0 +1 @@ +export { default } from './CellSelectWithMenu'; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts new file mode 100644 index 00000000000..fa1de756de1 --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.constants.ts @@ -0,0 +1,19 @@ +// External dependencies. +import { IconName } from '../../../component-library/components/Icons/Icon'; +import { SAMPLE_LISTITEM_PROPS } from '../../../component-library/components/List/ListItem/ListItem.constants'; + +// Internal dependencies. +import { ListItemMultiSelectButtonProps } from './ListItemMultiSelectButton.types'; + +// Defaults +export const DEFAULT_LISTITEMMULTISELECT_GAP = 16; +export const BUTTON_TEST_ID = 'button-menu-select-test-id'; + +// Sample consts +export const SAMPLE_LISTITEMMULTISELECT_PROPS: ListItemMultiSelectButtonProps = + { + isSelected: false, + isDisabled: false, + buttonIcon: IconName.Arrow2Right, + ...SAMPLE_LISTITEM_PROPS, + }; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx new file mode 100644 index 00000000000..f1db9cbef12 --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.stories.tsx @@ -0,0 +1,53 @@ +/* eslint-disable react/display-name */ +import React from 'react'; + +// External dependencies. +import ListItemColumn, { + WidthType, +} from '../../../component-library/components/List/ListItemColumn'; +import Icon, { + IconName, +} from '../../../component-library/components/Icons/Icon'; +import Text, { + TextVariant, +} from '../../../component-library/components/Texts/Text'; + +// Internal dependencies. +import { default as ListItemSelectWithButtonComponent } from './ListItemMultiSelectButton'; +import { SAMPLE_LISTITEMMULTISELECT_PROPS } from './ListItemMultiSelectButton.constants'; +import { ListItemMultiSelectButtonProps } from './ListItemMultiSelectButton.types'; + +const ListItemSelectWithButtonMeta = { + title: 'Component Library / List', + component: ListItemSelectWithButtonComponent, + argTypes: { + isSelected: { + control: { type: 'boolean' }, + defaultValue: SAMPLE_LISTITEMMULTISELECT_PROPS.isSelected, + }, + isDisabled: { + control: { type: 'boolean' }, + defaultValue: SAMPLE_LISTITEMMULTISELECT_PROPS.isDisabled, + }, + }, +}; +export default ListItemSelectWithButtonMeta; + +export const ListItemWithButtonSelect = { + render: (args: JSX.IntrinsicAttributes & ListItemMultiSelectButtonProps) => ( + + + + + + + {'Sample Title'} + + {'Sample Description'} + + + + + + ), +}; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts new file mode 100644 index 00000000000..3e647f27cfe --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.styles.ts @@ -0,0 +1,65 @@ +// Third party dependencies. +import { StyleSheet, ViewStyle } from 'react-native'; + +// External dependencies. +import { Theme } from '../../../util/theme/models'; + +// Internal dependencies. +import { ListItemMultiSelectButtonStyleSheetVars } from './ListItemMultiSelectButton.types'; + +/** + * Style sheet function for ListItemSelect component. + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { + theme: Theme; + vars: ListItemMultiSelectButtonStyleSheetVars; +}) => { + const { vars, theme } = params; + const { colors } = theme; + const { style, isDisabled, isSelected } = vars; + return StyleSheet.create({ + base: Object.assign( + { + position: 'relative', + opacity: isDisabled ? 0.5 : 1, + padding: 16, + backgroundColor: colors.background.default, + width: '95%', + } as ViewStyle, + style, + ) as ViewStyle, + underlay: { + ...StyleSheet.absoluteFillObject, + flexDirection: 'row', + backgroundColor: colors.primary.muted, + }, + underlayBar: { + marginVertical: 4, + marginLeft: 4, + width: 4, + borderRadius: 2, + backgroundColor: colors.primary.default, + }, + listItem: { + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + }, + container: { + backgroundColor: isSelected + ? colors.primary.muted + : colors.background.default, + paddingRight: 20, + flexDirection: 'row', + alignItems: 'center', + }, + }); +}; + +export default styleSheet; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx new file mode 100644 index 00000000000..7e73e164d40 --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.test.tsx @@ -0,0 +1,75 @@ +// Third party dependencies. +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import { View } from 'react-native'; + +// Internal dependencies. +import ListItemMultiSelectButton from './ListItemMultiSelectButton'; +import { IconName } from '../../../component-library/components/Icons/Icon'; // Adjust the import path as necessary +import { BUTTON_TEST_ID } from './ListItemMultiSelectButton.constants'; + +describe('ListItemMultiSelectButton', () => { + it('should render correctly with default props', () => { + const wrapper = render( + + + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('should not render the underlay view if isSelected is false', () => { + const { queryByRole } = render( + + + , + ); + expect(queryByRole('checkbox')).toBeNull(); + }); + + it('should render the underlay view if isSelected is true', () => { + const { queryByRole } = render( + + + , + ); + expect(queryByRole('checkbox')).not.toBeNull(); + }); + + it('should call onPress when the button is pressed', () => { + const mockOnPress = jest.fn(); + const { getByRole } = render( + + + , + ); + fireEvent.press(getByRole('button')); + expect(mockOnPress).toHaveBeenCalled(); + }); + + it('should render the button icon with the correct name', () => { + const { getByTestId } = render( + + + , + ); + expect(getByTestId(BUTTON_TEST_ID)).not.toBeNull(); + }); + + it('should call onButtonClick when the button icon is pressed', () => { + const mockOnButtonClick = jest.fn(); + const { getByTestId } = render( + + + , + ); + fireEvent.press(getByTestId(BUTTON_TEST_ID)); + expect(mockOnButtonClick).toHaveBeenCalled(); + }); +}); diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx new file mode 100644 index 00000000000..e335843c3a9 --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.tsx @@ -0,0 +1,71 @@ +/* eslint-disable react/prop-types */ + +// Third party dependencies. +import React from 'react'; +import { TouchableOpacity, View } from 'react-native'; + +// External dependencies. +import { useStyles } from '../../hooks'; +import ListItem from '../../../component-library/components/List/ListItem/ListItem'; + +// Internal dependencies. +import styleSheet from './ListItemMultiSelectButton.styles'; +import { ListItemMultiSelectButtonProps } from './ListItemMultiSelectButton.types'; +import { + BUTTON_TEST_ID, + DEFAULT_LISTITEMMULTISELECT_GAP, +} from './ListItemMultiSelectButton.constants'; +import ButtonIcon from '../../../component-library/components/Buttons/ButtonIcon'; +import { + IconColor, + IconName, +} from '../../../component-library/components/Icons/Icon'; + +const ListItemMultiSelectButton: React.FC = ({ + style, + isSelected = false, + isDisabled = false, + children, + gap = DEFAULT_LISTITEMMULTISELECT_GAP, + buttonIcon = IconName.MoreVertical, + ...props +}) => { + const { styles } = useStyles(styleSheet, { + style, + gap, + isDisabled, + isSelected, + }); + + return ( + + + + {children} + + {isSelected && ( + + + + )} + + + + + + ); +}; + +export default ListItemMultiSelectButton; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts new file mode 100644 index 00000000000..c9943e6e45a --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/ListItemMultiSelectButton.types.ts @@ -0,0 +1,43 @@ +// Third party dependencies. +import { TouchableOpacityProps } from 'react-native'; + +// External dependencies. +import { ListItemProps } from '../ListItem/ListItem.types'; +import { IconName } from '../../Icons/Icon'; +import { GestureResponderEvent } from 'react-native-modal'; + +/** + * ListItemMultiSelect component props. + */ +export interface ListItemMultiSelectButtonProps + extends TouchableOpacityProps, + Omit { + /** + * Optional prop to determine if the item is selected. + */ + isSelected?: boolean; + /** + * Optional prop to determine if the item is disabled. + */ + isDisabled?: boolean; + + /** + * Optional Button icon type. + */ + buttonIcon?: IconName; + + /** + * Optional button onClick function + */ + onButtonClick?: ((event: GestureResponderEvent) => void) | undefined; +} + +/** + * Style sheet input parameters. + */ +export type ListItemMultiSelectButtonStyleSheetVars = Pick< + ListItemMultiSelectButtonProps, + 'style' | 'isDisabled' | 'isSelected' +> & { + gap: number | string | undefined; +}; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap b/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap new file mode 100644 index 00000000000..49f9a1b0c2d --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/__snapshots__/ListItemMultiSelectButton.test.tsx.snap @@ -0,0 +1,79 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ListItemMultiSelectButton should render correctly with default props 1`] = ` + + + + + + + + + + + + +`; diff --git a/app/component-library/components-temp/ListItemMultiSelectButton/index.ts b/app/component-library/components-temp/ListItemMultiSelectButton/index.ts new file mode 100644 index 00000000000..b44be2a7359 --- /dev/null +++ b/app/component-library/components-temp/ListItemMultiSelectButton/index.ts @@ -0,0 +1 @@ +export { default } from './ListItemMultiSelectButton'; diff --git a/app/component-library/components/Cells/Cell/Cell.tsx b/app/component-library/components/Cells/Cell/Cell.tsx index 60c209e5e77..127b3d23015 100644 --- a/app/component-library/components/Cells/Cell/Cell.tsx +++ b/app/component-library/components/Cells/Cell/Cell.tsx @@ -5,6 +5,7 @@ import React from 'react'; import CellDisplay from './variants/CellDisplay'; import CellMultiSelect from './variants/CellMultiSelect'; import CellSelect from './variants/CellSelect'; +import CellSelectWithMenu from '../../../components-temp/CellSelectWithMenu'; import { CellModalSelectorsIDs } from '../../../../../e2e/selectors/Modals/CellModal.selectors'; // Internal dependencies. @@ -23,6 +24,13 @@ const Cell = ({ variant, ...props }: CellProps) => { ); case CellVariant.Select: return ; + case CellVariant.SelectWithMenu: + return ( + + ); default: throw new Error('Invalid Cell Variant'); } diff --git a/app/component-library/components/Cells/Cell/Cell.types.ts b/app/component-library/components/Cells/Cell/Cell.types.ts index 71e06149847..4ffe31fa279 100644 --- a/app/component-library/components/Cells/Cell/Cell.types.ts +++ b/app/component-library/components/Cells/Cell/Cell.types.ts @@ -2,6 +2,7 @@ import { CellDisplayProps } from './variants/CellDisplay/CellDisplay.types'; import { CellMultiSelectProps } from './variants/CellMultiSelect/CellMultiSelect.types'; import { CellSelectProps } from './variants/CellSelect/CellSelect.types'; +import { CellSelectWithMenuProps } from '../../../components-temp/CellSelectWithMenu/CellSelectWithMenu.types'; /** * Cell variants. @@ -10,6 +11,7 @@ export enum CellVariant { Select = 'Select', MultiSelect = 'MultiSelect', Display = 'Display', + SelectWithMenu = 'SelectWithMenu', } /** @@ -19,6 +21,7 @@ export type CellProps = ( | CellDisplayProps | CellMultiSelectProps | CellSelectProps + | CellSelectWithMenuProps ) & { /** * Variant of Cell diff --git a/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.styles.ts b/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.styles.ts index 584db48d9e0..9590cf40794 100644 --- a/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.styles.ts +++ b/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.styles.ts @@ -14,6 +14,16 @@ const createStyles = (colors: Colors) => borderColor: colors.border.default, color: colors.text.default, }, + focusedInputWrapper: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 10, + paddingVertical: 10, + borderRadius: 5, + borderWidth: 1, + borderColor: colors.primary.default, + color: colors.text.default, + }, input: { flex: 1, fontSize: 14, @@ -21,6 +31,14 @@ const createStyles = (colors: Colors) => ...fontStyles.normal, paddingLeft: 10, }, + unfocusedInput: { + flex: 1, + fontSize: 14, + color: colors.text.default, + ...fontStyles.normal, + paddingLeft: 10, + borderColor: colors.border.default, + }, }); export default createStyles; diff --git a/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.tsx b/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.tsx index fca39434c03..2f595433ab8 100644 --- a/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.tsx +++ b/app/components/Views/NetworkSelector/NetworkSearchTextInput/NetworkSearchTextInput.tsx @@ -1,10 +1,11 @@ // Third party dependencies. -import React from 'react'; +import React, { useState } from 'react'; import { TextInput, View } from 'react-native'; // External dependencies. import { strings } from '../../../../../locales/i18n'; import { mockTheme, useTheme } from '../../../../util/theme'; +import { isNetworkUiRedesignEnabled } from '../../../../util/networks'; // Internal dependencies import Icon from 'react-native-vector-icons/Ionicons'; @@ -26,17 +27,45 @@ function NetworkSearchTextInput({ const theme = useTheme(); const { colors } = theme; const styles = createStyles(colors || mockTheme.colors); + const [isSearchFieldFocused, setIsSearchFieldFocused] = useState(false); + const searchPlaceHolder = isNetworkUiRedesignEnabled + ? 'search-short' + : 'search'; + + const propsWhichAreFeatureFlagged = isNetworkUiRedesignEnabled + ? { + onFocus: () => { + isNetworkUiRedesignEnabled && setIsSearchFieldFocused(true); + }, + onBlur: () => { + isNetworkUiRedesignEnabled && setIsSearchFieldFocused(false); + }, + } + : {}; + + const inputStylesWhichAreFeatureFlagged = !isNetworkUiRedesignEnabled + ? styles.input + : isSearchFieldFocused + ? styles.input + : styles.unfocusedInput; + + const containerInputStylesWhichAreFeatureFlagged = !isNetworkUiRedesignEnabled + ? styles.inputWrapper + : isSearchFieldFocused + ? styles.focusedInputWrapper + : styles.inputWrapper; return ( - + {searchString.length > 0 && ( ? 12 : 0, }, + networkMenu: { + alignItems: 'center', + }, + containerDeleteText: { + paddingLeft: 16, + paddingRight: 8, + alignItems: 'center', + }, + textCentred: { + textAlign: 'center', + }, + buttonWrapper: { + flexDirection: 'row', + flex: 1, + width: '80%', + }, + button: { + width: '100%', + }, + container: { + flexDirection: 'row', + justifyContent: 'space-around', + alignItems: 'center', + backgroundColor: colors.background.default, + }, + item: { + paddingLeft: 8, + }, + buttonMenu: { + backgroundColor: colors.background.alternative, + }, switchContainer: { flexDirection: 'row', justifyContent: 'space-between', @@ -26,6 +57,13 @@ const createStyles = (colors: Colors) => marginVertical: 16, marginHorizontal: 16, }, + popularNetworkTitleContainer: { + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + marginVertical: 16, + marginHorizontal: 16, + }, addtionalNetworksContainer: { marginHorizontal: 16, }, @@ -102,6 +140,18 @@ const createStyles = (colors: Colors) => marginRight: 16, marginBottom: 8, }, + gasInfoContainer: { + paddingHorizontal: 4, + }, + gasInfoIcon: { + color: colors.icon.alternative, + }, + hitSlop: { + top: 10, + left: 10, + bottom: 10, + right: 10, + }, }); export default createStyles; diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx index d508c671c2c..3a2d2431d9c 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ // Third party dependencies. -import React, { useRef, useState } from 'react'; -import { Linking, Switch, View } from 'react-native'; +import { Linking, Switch, TouchableOpacity, View } from 'react-native'; +import React, { useCallback, useRef, useState } from 'react'; import { ScrollView } from 'react-native-gesture-handler'; import images from 'images/image-icons'; import { useNavigation } from '@react-navigation/native'; @@ -20,6 +19,7 @@ import { strings } from '../../../../locales/i18n'; import BottomSheet, { BottomSheetRef, } from '../../../component-library/components/BottomSheets/BottomSheet'; +import { IconName } from '../../../component-library/components/Icons/Icon'; import { useSelector } from 'react-redux'; import { selectNetworkConfigurations, @@ -61,17 +61,27 @@ import { useMetrics } from '../../../components/hooks/useMetrics'; // Internal dependencies import createStyles from './NetworkSelector.styles'; -import { TESTNET_TICKER_SYMBOLS } from '@metamask/controller-utils'; +import { + InfuraNetworkType, + TESTNET_TICKER_SYMBOLS, +} from '@metamask/controller-utils'; import InfoModal from '../../../../app/components/UI/Swaps/components/InfoModal'; import hideKeyFromUrl from '../../../util/hideKeyFromUrl'; import CustomNetwork from '../Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork'; import { NetworksSelectorSelectorsIDs } from '../../../../e2e/selectors/Settings/NetworksView.selectors'; import { PopularList } from '../../../util/networks/customNetworks'; import NetworkSearchTextInput from './NetworkSearchTextInput'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader'; +import AccountAction from '../AccountAction'; +import { ButtonsAlignment } from '../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { ButtonProps } from '../../../component-library/components/Buttons/Button/Button.types'; +import BottomSheetFooter from '../../../component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter'; +import { ExtendedNetwork } from '../Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.types'; const NetworkSelector = () => { const [showPopularNetworkModal, setShowPopularNetworkModal] = useState(false); - const [popularNetwork, setPopularNetwork] = useState(undefined); + const [popularNetwork, setPopularNetwork] = useState(); const [showWarningModal, setShowWarningModal] = useState(false); const [searchString, setSearchString] = useState(''); const { navigate } = useNavigation(); @@ -86,12 +96,35 @@ const NetworkSelector = () => { const networkConfigurations = useSelector(selectNetworkConfigurations); const avatarSize = isNetworkUiRedesignEnabled ? AvatarSize.Sm : undefined; + const modalTitle = isNetworkUiRedesignEnabled + ? 'networks.additional_network_information_title' + : 'networks.network_warning_title'; + const modalDescription = isNetworkUiRedesignEnabled + ? 'networks.additonial_network_information_desc' + : 'networks.network_warning_desc'; const buttonLabelAddNetwork = isNetworkUiRedesignEnabled ? 'app_settings.network_add_custom_network' : 'app_settings.network_add_network'; + const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState({ + isVisible: false, + networkName: '', + entry: {}, + }); + + const [showNetworkMenuModal, setNetworkMenuModal] = useState({ + isVisible: false, + chainId: '', + displayEdit: false, + networkTypeOrRpcUrl: '', + isReadOnly: false, + }); + + const networkMenuSheetRef = useRef(null); + + const deleteModalSheetRef = useRef(null); // The only possible value types are mainnet, linea-mainnet, sepolia and linea-sepolia - const onNetworkChange = (type: string) => { + const onNetworkChange = (type: InfuraNetworkType) => { const { NetworkController, CurrencyRateController, @@ -150,9 +183,41 @@ const NetworkSelector = () => { } }; - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const showNetworkModal = (networkConfiguration: any) => { + const openModal = useCallback( + (chainId, displayEdit, networkTypeOrRpcUrl, isReadOnly) => { + setNetworkMenuModal({ + isVisible: true, + chainId, + displayEdit, + networkTypeOrRpcUrl, + isReadOnly, + }); + networkMenuSheetRef.current?.onOpenBottomSheet(); + }, + [], + ); + + const closeModal = useCallback(() => { + setNetworkMenuModal(() => ({ + chainId: '', + isVisible: false, + displayEdit: false, + networkTypeOrRpcUrl: '', + isReadOnly: false, + })); + networkMenuSheetRef.current?.onCloseBottomSheet(); + }, []); + + const closeDeleteModal = useCallback(() => { + setShowConfirmDeleteModal(() => ({ + networkName: '', + isVisible: false, + entry: {}, + })); + networkMenuSheetRef.current?.onCloseBottomSheet(); + }, []); + + const showNetworkModal = (networkConfiguration: ExtendedNetwork) => { setShowPopularNetworkModal(true); setPopularNetwork({ ...networkConfiguration, @@ -170,12 +235,16 @@ const NetworkSelector = () => { const toggleWarningModal = () => { setShowWarningModal(!showWarningModal); }; + const goToLearnMore = () => { Linking.openURL(strings('networks.learn_more_url')); }; - const filterNetworksByName = (networks: any[], networkName: string) => { - const searchResult: any = networks.filter(({ name }) => + const filterNetworksByName = ( + networks: ExtendedNetwork[], + networkName: string, + ) => { + const searchResult: ExtendedNetwork[] = networks.filter(({ name }) => name.toLowerCase().includes(networkName.toLowerCase()), ); @@ -202,6 +271,31 @@ const NetworkSelector = () => { if (isNetworkUiRedesignEnabled && isNoSearchResults(MAINNET)) return null; + if (isNetworkUiRedesignEnabled) { + return ( + onNetworkChange(MAINNET)} + style={styles.networkCell} + buttonIcon={IconName.MoreVertical} + onButtonClick={() => { + openModal(chainId, false, MAINNET, true); + }} + /> + ); + } + return ( { if (isNetworkUiRedesignEnabled && isNoSearchResults('linea-mainnet')) return null; + if (isNetworkUiRedesignEnabled) { + return ( + onNetworkChange(LINEA_MAINNET)} + style={styles.networkCell} + buttonIcon={IconName.MoreVertical} + onButtonClick={() => { + openModal(chainId, false, LINEA_MAINNET, true); + }} + /> + ); + } + return ( { //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional const image = getNetworkImageSource({ chainId: chainId?.toString() }); + if (isNetworkUiRedesignEnabled) { + return ( + onSetRpcTarget(rpcUrl)} + style={styles.networkCell} + buttonIcon={IconName.MoreVertical} + onButtonClick={() => { + openModal(chainId, true, rpcUrl, false); + }} + /> + ); + } + return ( { const renderOtherNetworks = () => { const getOtherNetworks = () => getAllNetworks().slice(2); return getOtherNetworks().map((networkType) => { - // TODO: Provide correct types for network. - // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const { name, imageSource, chainId } = (Networks as any)[networkType]; if (isNetworkUiRedesignEnabled && isNoSearchResults(name)) return null; + if (isNetworkUiRedesignEnabled) { + return ( + onNetworkChange(networkType)} + style={styles.networkCell} + buttonIcon={IconName.MoreVertical} + onButtonClick={() => { + openModal(chainId, false, networkType, true); + }} + /> + ); + } + return ( { searchString.length > 0 ? filteredNetworks : undefined } showCompletionMessage={false} + hideWarningIcons /> ); }; - const renderTitle = (title: string) => ( + const renderPopularNetworksTitle = () => ( + + + {strings('networks.additional_networks')} + + + + + + ); + + const renderEnabledNetworksTitle = () => ( - {strings(title)} + {strings('networks.enabled_networks')} ); - const handleSearchTextChange = (text: any) => { + const handleSearchTextChange = (text: string) => { setSearchString(text); }; @@ -379,6 +563,54 @@ const NetworkSelector = () => { setSearchString(''); }; + const removeRpcUrl = (networkId: string) => { + const entry = Object.entries(networkConfigurations).find( + ([, { chainId }]) => chainId === networkId, + ); + + if (!entry) { + throw new Error(`Unable to find network with chain id ${networkId}`); + } + + const [, { nickname }] = entry; + + closeModal(); + + setShowConfirmDeleteModal({ + isVisible: true, + networkName: nickname, + entry, + }); + }; + + const confirmRemoveRpc = () => { + const [networkConfigurationId] = showConfirmDeleteModal.entry; + + const { NetworkController } = Engine.context; + + NetworkController.removeNetworkConfiguration(networkConfigurationId); + + setShowConfirmDeleteModal({ + isVisible: false, + networkName: '', + entry: {}, + }); + }; + + const cancelButtonProps: ButtonProps = { + variant: ButtonVariants.Secondary, + label: strings('accountApproval.cancel'), + size: ButtonSize.Lg, + onPress: () => closeDeleteModal(), + }; + + const deleteButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('app_settings.delete'), + size: ButtonSize.Lg, + onPress: () => confirmRemoveRpc(), + }; + const renderBottomSheetContent = () => ( <> @@ -398,13 +630,13 @@ const NetworkSelector = () => { )} {isNetworkUiRedesignEnabled && searchString.length === 0 && - renderTitle('networks.enabled_networks')} + renderEnabledNetworksTitle()} {renderMainnet()} {renderLineaMainnet()} {renderRpcNetworks()} {isNetworkUiRedesignEnabled && searchString.length === 0 && - renderTitle('networks.additional_networks')} + renderPopularNetworksTitle()} {isNetworkUiRedesignEnabled && renderAdditonalNetworks()} {searchString.length === 0 && renderTestNetworksSwitch()} {showTestNetworks && renderOtherNetworks()} @@ -435,12 +667,10 @@ const NetworkSelector = () => { {showWarningModal ? ( - - {strings('networks.network_warning_desc')} - {' '} + {strings(modalDescription)}{' '} {strings('networks.learn_more')} @@ -449,6 +679,65 @@ const NetworkSelector = () => { toggleModal={toggleWarningModal} /> ) : null} + + {showNetworkMenuModal.isVisible ? ( + + + { + navigate(Routes.ADD_NETWORK, { + shouldNetworkSwitchPopToWallet: false, + shouldShowPopularNetworks: false, + network: showNetworkMenuModal.networkTypeOrRpcUrl, + }); + }} + /> + {showNetworkMenuModal.chainId !== providerConfig.chainId && + showNetworkMenuModal.displayEdit ? ( + removeRpcUrl(showNetworkMenuModal.chainId)} + /> + ) : null} + + + ) : null} + + {showConfirmDeleteModal.isVisible ? ( + + + + {strings('app_settings.delete')}{' '} + {showConfirmDeleteModal.networkName}{' '} + {strings('asset_details.network')} + + + + + {strings('app_settings.network_delete')} + + + + + ) : null} ); }; diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx index a2bfb722ab5..612ff8e0de9 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx @@ -28,6 +28,7 @@ const CustomNetwork = ({ showAddedNetworks, customNetworksList, showCompletionMessage = true, + hideWarningIcons = false, }: CustomNetworkProps) => { const networkConfigurations = useSelector(selectNetworkConfigurations); @@ -97,7 +98,9 @@ const CustomNetwork = ({ - {toggleWarningModal && networkConfiguration.warning ? ( + {!hideWarningIcons && + toggleWarningModal && + networkConfiguration.warning ? (