diff --git a/app/components/Views/Settings/AutoDetectNFTSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/AutoDetectNFTSettings/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..16f08a0bb24 --- /dev/null +++ b/app/components/Views/Settings/AutoDetectNFTSettings/__snapshots__/index.test.tsx.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AutoDetectNFTSettings should render correctly 1`] = ` + + + + Autodetect NFTs + + + + + + + Let MetaMask add NFTs you own using third-party services (like OpenSea). Autodetecting NFTs exposes your IP and account address to these services. Enabling this feature could associate your IP address with your Ethereum address and display fake NFTs airdropped by scammers. You can add tokens manually to avoid this risk. + + +`; diff --git a/app/components/Views/Settings/AutoDetectNFTSettings/index.constants.ts b/app/components/Views/Settings/AutoDetectNFTSettings/index.constants.ts new file mode 100644 index 00000000000..f3047aa77ef --- /dev/null +++ b/app/components/Views/Settings/AutoDetectNFTSettings/index.constants.ts @@ -0,0 +1,2 @@ +export const NFT_AUTO_DETECT_MODE_SECTION = + 'nft-opensea-autodetect-mode-section'; diff --git a/app/components/Views/Settings/AutoDetectNFTSettings/index.styles.ts b/app/components/Views/Settings/AutoDetectNFTSettings/index.styles.ts new file mode 100644 index 00000000000..a7b2f4d82a8 --- /dev/null +++ b/app/components/Views/Settings/AutoDetectNFTSettings/index.styles.ts @@ -0,0 +1,29 @@ +import { StyleSheet } from 'react-native'; + +const createStyles = () => + StyleSheet.create({ + titleContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + title: { + flex: 1, + }, + switchElement: { + marginLeft: 16, + }, + switch: { + alignSelf: 'flex-start', + }, + halfSetting: { + marginTop: 16, + }, + desc: { + marginTop: 8, + }, + setting: { + marginTop: 32, + }, + }); + +export default createStyles; diff --git a/app/components/Views/Settings/AutoDetectNFTSettings/index.test.tsx b/app/components/Views/Settings/AutoDetectNFTSettings/index.test.tsx new file mode 100644 index 00000000000..fa84909edfa --- /dev/null +++ b/app/components/Views/Settings/AutoDetectNFTSettings/index.test.tsx @@ -0,0 +1,158 @@ +// Third party dependencies +import React from 'react'; +import { fireEvent } from '@testing-library/react-native'; +import { useNavigation } from '@react-navigation/native'; + +// External dependencies +import Engine from '../../../../core/Engine'; +import renderWithProvider from '../../../../util/test/renderWithProvider'; +import { backgroundState } from '../../../../util/test/initial-root-state'; + +// Internal dependencies +import AutoDetectNFTSettings from './index'; +import { NFT_AUTO_DETECT_MODE_SECTION } from './index.constants'; + +let mockSetDisplayNftMedia: jest.Mock; +let mockSetUseNftDetection: jest.Mock; +let mockAddTraitsToUser: jest.Mock; +let mockTrackEvent: jest.Mock; + +beforeEach(() => { + mockSetDisplayNftMedia.mockClear(); + mockSetUseNftDetection.mockClear(); + mockAddTraitsToUser.mockClear(); + mockTrackEvent.mockClear(); +}); + +const mockEngine = Engine; + +jest.mock('../../../../core/Engine', () => { + mockSetDisplayNftMedia = jest.fn(); + mockSetUseNftDetection = jest.fn(); + mockAddTraitsToUser = jest.fn(); + mockTrackEvent = jest.fn(); + return { + init: () => mockEngine.init({}), + context: { + PreferencesController: { + setDisplayNftMedia: mockSetDisplayNftMedia, + setUseNftDetection: mockSetUseNftDetection, + }, + }, + }; +}); + +const mockNavigation = { + goBack: jest.fn(), + setOptions: jest.fn(), +}; + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useNavigation: jest.fn(() => mockNavigation), +})); + +jest.mock('../../../hooks/useMetrics', () => ({ + useMetrics: () => ({ + addTraitsToUser: mockAddTraitsToUser, + trackEvent: mockTrackEvent, + }), + MetaMetricsEvents: { + NFT_AUTO_DETECTION_ENABLED: 'NFT_AUTO_DETECTION_ENABLED', + }, +})); + +jest.mock('../../../../util/general', () => ({ + timeoutFetch: jest.fn(), +})); + +describe('AutoDetectNFTSettings', () => { + beforeEach(() => { + jest.clearAllMocks(); + (useNavigation as jest.Mock).mockImplementation(() => mockNavigation); + }); + + const initialState = { + engine: { + backgroundState: { + ...backgroundState, + PreferencesController: { + ...backgroundState.PreferencesController, + useTokenDetection: true, + displayNftMedia: false, + useNftDetection: false, + }, + }, + }, + network: { + provider: { + chainId: '1', + }, + }, + }; + + it('should render correctly', () => { + const tree = renderWithProvider(, { + state: initialState, + }); + expect(tree).toMatchSnapshot(); + }); + + describe('NFT Autodetection', () => { + it('should render NFT autodetection switch', () => { + const { getByTestId } = renderWithProvider(, { + state: initialState, + }); + const autoDetectSwitch = getByTestId(NFT_AUTO_DETECT_MODE_SECTION); + expect(autoDetectSwitch).toBeTruthy(); + }); + + it('should toggle NFT autodetection when switch is pressed', () => { + const { getByTestId } = renderWithProvider(, { + state: initialState, + }); + const autoDetectSwitch = getByTestId(NFT_AUTO_DETECT_MODE_SECTION); + fireEvent(autoDetectSwitch, 'onValueChange', true); + + expect( + Engine.context.PreferencesController.setUseNftDetection, + ).toHaveBeenCalledWith(true); + expect( + Engine.context.PreferencesController.setDisplayNftMedia, + ).toHaveBeenCalledWith(true); + expect(mockAddTraitsToUser).toHaveBeenCalledWith({ + 'NFT Autodetection': 'ON', + }); + expect(mockTrackEvent).toHaveBeenCalledWith( + 'NFT_AUTO_DETECTION_ENABLED', + { + 'NFT Autodetection': 'ON', + location: 'app_settings', + }, + ); + }); + + it('should not enable display NFT media when autodetection is turned off', () => { + const { getByTestId } = renderWithProvider(, { + state: initialState, + }); + const autoDetectSwitch = getByTestId(NFT_AUTO_DETECT_MODE_SECTION); + expect(autoDetectSwitch).toBeTruthy(); + + fireEvent(autoDetectSwitch, 'onValueChange', false); + + expect(mockSetUseNftDetection).toHaveBeenCalledWith(false); + expect(mockSetDisplayNftMedia).not.toHaveBeenCalled(); + expect(mockAddTraitsToUser).toHaveBeenCalledWith({ + 'NFT Autodetection': 'OFF', + }); + expect(mockTrackEvent).toHaveBeenCalledWith( + 'NFT_AUTO_DETECTION_ENABLED', + { + 'NFT Autodetection': 'OFF', + location: 'app_settings', + }, + ); + }); + }); +}); diff --git a/app/components/Views/Settings/AutoDetectNFTSettings/index.tsx b/app/components/Views/Settings/AutoDetectNFTSettings/index.tsx new file mode 100644 index 00000000000..0cc7ecbe88d --- /dev/null +++ b/app/components/Views/Settings/AutoDetectNFTSettings/index.tsx @@ -0,0 +1,83 @@ +// Third party dependencies +import React, { useCallback } from 'react'; +import { View, Switch } from 'react-native'; +import { useSelector } from 'react-redux'; + +// External dependencies +import Engine from '../../../../core/Engine'; +import { selectUseNftDetection } from '../../../../selectors/preferencesController'; +import { useTheme } from '../../../../util/theme'; +import { strings } from '../../../../../locales/i18n'; +import { MetaMetricsEvents, useMetrics } from '../../../hooks/useMetrics'; +import Text, { + TextVariant, + TextColor, +} from '../../../../component-library/components/Texts/Text'; +import { UserProfileProperty } from '../../../../util/metrics/UserSettingsAnalyticsMetaData/UserProfileAnalyticsMetaData.types'; + +// Internal dependencies +import createStyles from './index.styles'; +import { NFT_AUTO_DETECT_MODE_SECTION } from './index.constants'; + +const AutoDetectNFTSettings = () => { + const { trackEvent, addTraitsToUser } = useMetrics(); + const theme = useTheme(); + const { colors } = theme; + const styles = createStyles(); + + const useNftDetection = useSelector(selectUseNftDetection); + + const toggleNftAutodetect = useCallback( + (value: boolean) => { + const { PreferencesController } = Engine.context; + if (value) { + PreferencesController.setDisplayNftMedia(value); + } + PreferencesController.setUseNftDetection(value); + const traits = { + [UserProfileProperty.NFT_AUTODETECTION]: value + ? UserProfileProperty.ON + : UserProfileProperty.OFF, + }; + addTraitsToUser(traits); + trackEvent(MetaMetricsEvents.NFT_AUTO_DETECTION_ENABLED, { + ...traits, + location: 'app_settings', + }); + }, + [addTraitsToUser, trackEvent], + ); + + return ( + + + + {strings('app_settings.nft_autodetect_mode')} + + + + + + + {strings('app_settings.autodetect_nft_desc')} + + + ); +}; + +export default AutoDetectNFTSettings; diff --git a/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx b/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx index f7ae9d6eff5..5dda58ef252 100644 --- a/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx +++ b/app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx @@ -58,7 +58,6 @@ import { selectIsIpfsGatewayEnabled, selectIsMultiAccountBalancesEnabled, selectDisplayNftMedia, - selectUseNftDetection, selectShowIncomingTransactionNetworks, selectShowTestNetworks, selectUseSafeChainsListValidation, @@ -89,7 +88,6 @@ import { HASH_STRING, HASH_TO_TEST, IPFS_GATEWAY_SECTION, - NFT_AUTO_DETECT_MODE_SECTION, NFT_DISPLAY_MEDIA_MODE_SECTION, PASSCODE_CHOICE_STRING, SDK_SECTION, @@ -121,7 +119,6 @@ import ProfileSyncingComponent from '../../../UI/ProfileSyncing/ProfileSyncing'; import Routes from '../../../../constants/navigation/Routes'; import { MetaMetrics } from '../../../../core/Analytics'; import MetaMetricsAndDataCollectionSection from './Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection'; -import { UserProfileProperty } from '../../../../util/metrics/UserSettingsAnalyticsMetaData/UserProfileAnalyticsMetaData.types'; import { selectIsMetamaskNotificationsEnabled, selectIsProfileSyncingEnabled, @@ -132,6 +129,8 @@ import { RootState } from '../../../../reducers'; import { EtherscanSupportedHexChainId } from '@metamask/preferences-controller'; import { useDisableNotifications } from '../../../../util/notifications/hooks/useNotifications'; import { isNotificationsFeatureEnabled } from '../../../../util/notifications'; +import AutoDetectNFTSettings from '../../Settings/AutoDetectNFTSettings'; + const Heading: React.FC = ({ children, first }) => { const { colors } = useTheme(); const styles = createStyles(colors); @@ -145,7 +144,7 @@ const Heading: React.FC = ({ children, first }) => { }; const Settings: React.FC = () => { - const { trackEvent, isEnabled, addTraitsToUser } = useMetrics(); + const { trackEvent, isEnabled } = useMetrics(); const theme = useTheme(); const { colors } = theme; const styles = createStyles(colors); @@ -198,8 +197,6 @@ const Settings: React.FC = () => { selectUseTransactionSimulations, ); - const useNftDetection = useSelector(selectUseNftDetection); - const isNotificationEnabled = useSelector( selectIsMetamaskNotificationsEnabled, ); @@ -586,27 +583,6 @@ const Settings: React.FC = () => { if (!value) PreferencesController?.setUseNftDetection(value); }; - const toggleNftAutodetect = useCallback( - (value) => { - const { PreferencesController } = Engine.context; - if (value) { - PreferencesController.setDisplayNftMedia(value); - } - PreferencesController.setUseNftDetection(value); - const traits = { - [UserProfileProperty.NFT_AUTODETECTION]: value - ? UserProfileProperty.ON - : UserProfileProperty.OFF, - }; - addTraitsToUser(traits); - trackEvent(MetaMetricsEvents.NFT_AUTO_DETECTION_ENABLED, { - ...traits, - location: 'app_settings', - }); - }, - [addTraitsToUser, trackEvent], - ); - const renderDisplayNftMedia = useCallback( () => ( @@ -732,43 +708,6 @@ const Settings: React.FC = () => { [colors, styles, useTransactionSimulations, theme.brandColors.white], ); - const renderAutoDetectNft = useCallback( - () => ( - - - - {strings('app_settings.nft_autodetect_mode')} - - - - - - - {strings('app_settings.autodetect_nft_desc')} - - - ), - [colors, styles, useNftDetection, theme, toggleNftAutodetect], - ); - const setIpfsGateway = (gateway: string) => { const { PreferencesController } = Engine.context; PreferencesController.setIpfsGateway(gateway); @@ -1154,7 +1093,11 @@ const Settings: React.FC = () => { {strings('app_settings.token_nft_ens_subheading')} {renderDisplayNftMedia()} - {isMainnet && renderAutoDetectNft()} + {isMainnet && ( + + + + )} {renderIpfsGateway()} - + - - Autodetect NFTs - - + Autodetect NFTs + + + + [ + { + "alignSelf": "flex-start", + }, + { + "backgroundColor": "#bbc0c566", + "borderRadius": 16, + }, + ], + ] + } + testID="nft-opensea-autodetect-mode-section" + thumbTintColor="#ffffff" + tintColor="#bbc0c566" + value={true} + /> + - - - Let MetaMask add NFTs you own using third-party services (like OpenSea). Autodetecting NFTs exposes your IP and account address to these services. Enabling this feature could associate your IP address with your Ethereum address and display fake NFTs airdropped by scammers. You can add tokens manually to avoid this risk. - + > + Let MetaMask add NFTs you own using third-party services (like OpenSea). Autodetecting NFTs exposes your IP and account address to these services. Enabling this feature could associate your IP address with your Ethereum address and display fake NFTs airdropped by scammers. You can add tokens manually to avoid this risk. + +