diff --git a/.js.env.example b/.js.env.example index fd39a84c0d33..661eeafe5e18 100644 --- a/.js.env.example +++ b/.js.env.example @@ -99,6 +99,9 @@ export MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS="true" # Per dapp selected network feature flag export MM_PER_DAPP_SELECTED_NETWORK="" +# Remove global network selector flag +export MM_REMOVE_GLOBAL_NETWORK_SELECTOR="" + export MM_CHAIN_PERMISSIONS="true" # Permissions Settings feature flag specific to UI changes diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js index 2a9af3ba0b16..9b4f5a562fcc 100644 --- a/app/components/UI/Navbar/index.js +++ b/app/components/UI/Navbar/index.js @@ -358,9 +358,8 @@ export function getPaymentRequestOptionsTitle( const goBack = route.params?.dispatch; const innerStyles = StyleSheet.create({ headerTitleStyle: { - fontSize: 20, - color: themeColors.text.default, - ...fontStyles.normal, + justifyContent: 'center', + alignItems: 'center', }, headerIcon: { color: themeColors.primary.default, @@ -370,11 +369,18 @@ export function getPaymentRequestOptionsTitle( shadowColor: importedColors.transparent, elevation: 0, }, + headerCloseButton: { + marginRight: 16, + }, }); return { - title, - headerTitleStyle: innerStyles.headerTitleStyle, + headerTitleAlign: 'center', + headerTitle: () => ( + + {title} + + ), headerLeft: () => goBack ? ( // eslint-disable-next-line react/jsx-no-bind @@ -393,17 +399,13 @@ export function getPaymentRequestOptionsTitle( ), headerRight: () => ( - // eslint-disable-next-line react/jsx-no-bind - navigation.pop()} - style={styles.closeButton} - > - - + style={innerStyles.headerCloseButton} + testID={RequestPaymentViewSelectors.BACK_BUTTON_ID} + /> ), headerStyle: innerStyles.headerStyle, headerTintColor: themeColors.primary.default, diff --git a/app/components/UI/Navbar/index.test.jsx b/app/components/UI/Navbar/index.test.jsx index cb8d1e930ad1..cb795b9a9611 100644 --- a/app/components/UI/Navbar/index.test.jsx +++ b/app/components/UI/Navbar/index.test.jsx @@ -1,6 +1,7 @@ /* eslint-disable react/prop-types */ import React from 'react'; import { createStackNavigator } from '@react-navigation/stack'; +import { fireEvent } from '@testing-library/react-native'; import renderWithProvider from '../../../util/test/renderWithProvider'; import { backgroundState } from '../../../util/test/initial-root-state'; import { @@ -28,7 +29,11 @@ describe('getNetworkNavbarOptions', () => { const TestNavigator = ({ options }) => ( - options.header()} /> + null} + options={options} + /> ); @@ -42,9 +47,10 @@ describe('getNetworkNavbarOptions', () => { 'Test Title', false, mockNavigation, + mockTheme.colors ); - const { getByText, getByRole } = renderWithProvider( + const { getByText } = renderWithProvider( , { state: { diff --git a/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap b/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap index e5380faebfda..348f2c469b6f 100644 --- a/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap +++ b/app/components/UI/PaymentRequest/__snapshots__/index.test.tsx.snap @@ -459,3 +459,543 @@ exports[`PaymentRequest renders correctly 1`] = ` `; + +exports[`PaymentRequest renders correctly with network picker when feature flag is enabled 1`] = ` + + + + + + + + + + + + + + + + + + Choose an asset to request + + + + +  + + + + + + Top picks + + + + + + + + + + + + + + + ETH + + + Ether + + + + + + + + + + + + + + + + + SAI + + + Sai Stablecoin v1.0 + + + + + + + + + + +`; diff --git a/app/components/UI/PaymentRequest/index.js b/app/components/UI/PaymentRequest/index.js index 192780a80c05..fb6f5f9a375b 100644 --- a/app/components/UI/PaymentRequest/index.js +++ b/app/components/UI/PaymentRequest/index.js @@ -45,12 +45,18 @@ import { getTicker } from '../../../util/transactions'; import { toLowerCaseEquals } from '../../../util/general'; import { utils as ethersUtils } from 'ethers'; import { ThemeContext, mockTheme } from '../../../util/theme'; -import { isTestNet } from '../../../util/networks'; +import { + isTestNet, + getDecimalChainId, + isRemoveGlobalNetworkSelectorEnabled, +} from '../../../util/networks'; import { isTokenDetectionSupportedForNetwork } from '@metamask/assets-controllers'; import { selectChainId, selectEvmTicker, + selectNetworkConfigurations, } from '../../../selectors/networkController'; +import { selectNetworkImageSource } from '../../../selectors/networkInfos'; import { selectConversionRate, selectCurrentCurrency, @@ -59,8 +65,11 @@ import { selectTokenListArray } from '../../../selectors/tokenListController'; import { selectTokens } from '../../../selectors/tokensController'; import { selectContractExchangeRates } from '../../../selectors/tokenRatesController'; import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController'; - +import PickerNetwork from '../../../component-library/components/Pickers/PickerNetwork/PickerNetwork'; +import Routes from '../../../constants/navigation/Routes'; +import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import { RequestPaymentViewSelectors } from '../../../../e2e/selectors/Receive/RequestPaymentView.selectors'; +import { MetaMetricsEvents } from '../../../core/Analytics'; const KEYBOARD_OFFSET = 120; const createStyles = (colors) => @@ -302,6 +311,18 @@ class PaymentRequest extends PureComponent { * Object that represents the current route info like params passed to it */ route: PropTypes.object, + /** + * Metrics injected by withMetricsAwareness HOC + */ + metrics: PropTypes.object, + /** + * Network configurations + */ + networkConfigurations: PropTypes.object, + /** + * Network image source + */ + networkImageSource: PropTypes.string, }; amountInput = React.createRef(); @@ -868,13 +889,39 @@ class PaymentRequest extends PureComponent { ); }; + handleNetworkPickerPress = () => { + this.props.navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.SHEET.NETWORK_SELECTOR, + }); + this.props.metrics.trackEvent( + this.props.metrics + .createEventBuilder(MetaMetricsEvents.NETWORK_SELECTOR_PRESSED) + .addProperties({ + chain_id: getDecimalChainId(this.props.chainId), + }) + .build(), + ); + }; + render() { const { mode } = this.state; const colors = this.context.colors || mockTheme.colors; const styles = createStyles(colors); + const networkName = + this.props.networkConfigurations?.[this.props.chainId]?.name; + const networkImageSource = this.props.networkImageSource; return ( + {isRemoveGlobalNetworkSelectorEnabled() && ( + + + + )} ({ ticker: selectEvmTicker(state), chainId: selectChainId(state), tokenList: selectTokenListArray(state), + networkConfigurations: selectNetworkConfigurations(state), + networkImageSource: selectNetworkImageSource(state), }); -export default connect(mapStateToProps)(PaymentRequest); +export default withMetricsAwareness(connect(mapStateToProps)(PaymentRequest)); diff --git a/app/components/UI/PaymentRequest/index.test.tsx b/app/components/UI/PaymentRequest/index.test.tsx index 3eb9798cc25f..075df9bcd613 100644 --- a/app/components/UI/PaymentRequest/index.test.tsx +++ b/app/components/UI/PaymentRequest/index.test.tsx @@ -8,9 +8,14 @@ import { import PaymentRequest from './index'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; +import { SolScope } from '@metamask/keyring-api'; import { ThemeContext, mockTheme } from '../../../util/theme'; import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../util/test/accountsControllerTestUtils'; -import { SolScope } from '@metamask/keyring-api'; +import Routes from '../../../constants/navigation/Routes'; +import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors'; +// eslint-disable-next-line import/no-namespace +import * as networks from '../../../util/networks'; +import ethLogo from '../../../assets/images/eth-logo.png'; jest.mock('react', () => ({ ...jest.requireActual('react'), @@ -131,6 +136,7 @@ const renderComponent = (props = {}) => @@ -143,6 +149,18 @@ describe('PaymentRequest', () => { expect(toJSON()).toMatchSnapshot(); }); + it('renders correctly with network picker when feature flag is enabled', () => { + jest + .spyOn(networks, 'isRemoveGlobalNetworkSelectorEnabled') + .mockReturnValue(true); + + const { toJSON } = renderComponent({ + chainId: '0x1', + networkImageSource: ethLogo, + }); + expect(toJSON()).toMatchSnapshot(); + }); + it('displays the correct title for asset selection', () => { const { getByText } = renderComponent(); expect(getByText('Choose an asset to request')).toBeTruthy(); @@ -202,4 +220,56 @@ describe('PaymentRequest', () => { expect(mockSetShowError).toHaveBeenCalledWith(true); expect(queryByText('Invalid request, please try again')).toBeTruthy(); }); + + describe('handleNetworkPickerPress', () => { + it('should navigate to network selector modal when feature flag is enabled', () => { + jest + .spyOn(networks, 'isRemoveGlobalNetworkSelectorEnabled') + .mockReturnValue(true); + + const mockMetrics = { + trackEvent: jest.fn(), + createEventBuilder: jest.fn(() => ({ + addProperties: jest.fn(() => ({ + build: jest.fn(() => 'builtEvent'), + })), + })), + }; + + const { getByTestId } = renderComponent({ + metrics: mockMetrics, + chainId: '0x1', + networkImageSource: ethLogo, + }); + + const networkPicker = getByTestId( + WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER, + ); + + act(() => { + fireEvent.press(networkPicker); + }); + + expect(mockNavigation.navigate).toHaveBeenCalledWith( + Routes.MODAL.ROOT_MODAL_FLOW, + { + screen: Routes.SHEET.NETWORK_SELECTOR, + }, + ); + }); + + it('should not render network picker when feature flag is disabled', () => { + // Feature flag is already set to false in beforeEach + const { queryByTestId } = renderComponent({ + chainId: '0x1', + networkImageSource: ethLogo, + }); + + const networkPicker = queryByTestId( + WalletViewSelectorsIDs.NAVBAR_NETWORK_PICKER, + ); + + expect(networkPicker).toBeNull(); + }); + }); }); diff --git a/app/util/networks/index.js b/app/util/networks/index.js index b83edf09bbf6..c4cf7c395713 100644 --- a/app/util/networks/index.js +++ b/app/util/networks/index.js @@ -61,7 +61,11 @@ import { } from '../../selectors/multichainNetworkController'; import { formatBlockExplorerAddressUrl } from '../../core/Multichain/networks'; import { CHAIN_IDS } from '@metamask/transaction-controller'; -import { isCaipChainId, KnownCaipNamespace, parseCaipChainId } from '@metamask/utils'; +import { + isCaipChainId, + KnownCaipNamespace, + parseCaipChainId, +} from '@metamask/utils'; /** * List of the supported networks @@ -555,14 +559,14 @@ const getEvmNetworkImageSource = ({ networkType, chainId }) => { export const getNetworkImageSource = ({ networkType, chainId }) => { let hexChainId = chainId; if (isCaipChainId(chainId)) { - const {namespace, reference} = parseCaipChainId(chainId); + const { namespace, reference } = parseCaipChainId(chainId); if (namespace !== KnownCaipNamespace.Eip155) { return getNonEvmNetworkImageSourceByChainId(chainId); } hexChainId = toHex(reference === '0' ? '1' : reference); // default to mainnet if chainId is 0 } - return getEvmNetworkImageSource({ networkType, chainId: hexChainId}); + return getEvmNetworkImageSource({ networkType, chainId: hexChainId }); }; /** @@ -639,6 +643,9 @@ export const isPerDappSelectedNetworkEnabled = () => export const isPortfolioViewEnabled = () => process.env.PORTFOLIO_VIEW === 'true'; +export const isRemoveGlobalNetworkSelectorEnabled = () => + process.env.MM_REMOVE_GLOBAL_NETWORK_SELECTOR === 'true'; + // The whitelisted network names for the given chain IDs to prevent showing warnings on Network Settings. export const WHILELIST_NETWORK_NAME = { [ChainId.mainnet]: 'Mainnet', diff --git a/bitrise.yml b/bitrise.yml index e743ed0f93a4..15dc093b32a1 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -2365,6 +2365,9 @@ app: - opts: is_expand: false MM_PER_DAPP_SELECTED_NETWORK: false + - opts: + is_expand: false + MM_REMOVE_GLOBAL_NETWORK_SELECTOR: false - opts: is_expand: false MM_CHAIN_PERMISSIONS: true