From ba7656724fdc7546428c5c8335429fdc2da5105b Mon Sep 17 00:00:00 2001 From: sahar-fehri Date: Wed, 26 Jun 2024 20:09:49 +0200 Subject: [PATCH] fix: create nft auto detection modal and remove nft polling logic (#9857) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** This PR removes any code that triggers polling on NftDetectionController. Calling `detectNfts()` function is now only triggered when the user clicks on the NFT tab. It also Enables NFTDetection by default for new users. >[!NOTE] > this PR has an asset-cpntroller patch from this [core PR #4281](https://github.com/MetaMask/core/pull/4281) > This patch keeps NFTDetection controller extending the polling controller while the PR makes it extend BaseController but it makes sure the polling is not triggered anymore in the client code. > It will be fully updated in newer versions of assets-controllers > I removed what triggers the `start()` of the polling, and now instead, mobile calls `detectNfts()` directly. > Then i made updates on the fct `getOwnerNfts` so that it only fetches information for a specific cursor instead of looping through all user nfts. > The loop is now being done inside the `detectNfts()` fct, and we basically fetch the first page of NFTs and save it to state so its available to clients to view. **New functionalities highlight:** _1- Enable NFT autodetection by default to new users. 2- Old users who have the NFT autodetection off will see the new NFT detection modal to encourage them to enable the modal. 3- When users click on the banner notice, they are no longer directed to settings and instead we enable the NFT detection after they click on “enable nft autodetection” and we show the Toast. We wanted to reduce friction and we removed that redirection to settings. 4- The code should not do any of the 3mins polling in the background anymore._ ## **Related issues** Related: https://github.com/MetaMask/core/pull/4281 ## **Manual testing steps** 1. Import MM application 2. Go to settings => Security and privacy and you should see that the NFT detection is enabled. 3. Go back to home page and click on NFT tab; you should be able to see your NFTs being added. 4. Switch to another account that has NFTs 5. Click on NFTs tab and you should also see your NFTs being added to sate. 6. Go to settings => Security and privacy and turn OFF the NFT detection toggle. 7. You should see a new modal. Clicking on "allow" button should re-enable the NFT detection toggle. ## **Screenshots/Recordings** ### **Before** https://github.com/MetaMask/metamask-mobile/assets/10994169/83e86852-3455-4b7c-bfc6-a658dc20c1b0 ### **After** Testing a fresh wallet import: https://github.com/MetaMask/metamask-mobile/assets/10994169/331668b1-3a27-493f-a648-3568e4ba67c2 Testing an upgrade scenario: First build on main, where you should see the NFT auto detection disabled by default if the user did not enable it Then build on this PR: You should see the new NFT detection modal and clicking on `allow` button should enable the NFT detection in the settings. https://github.com/MetaMask/metamask-mobile/assets/10994169/6bcb77f5-13ef-429e-a1f5-4d907145ee30 NFT detection banner with new Toast: https://github.com/MetaMask/metamask-mobile/assets/10994169/2f7465c0-4c42-409b-bfe4-ed70d9e1ca2c ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask 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: MetaMask Bot <37885440+metamaskbot@users.noreply.github.com> Co-authored-by: metamaskbot Co-authored-by: sethkfman Co-authored-by: Nico MASSART --- app/actions/security/index.ts | 16 +- app/actions/security/state.ts | 1 + .../components/Toast/Toast.tsx | 12 + .../components/Toast/Toast.types.ts | 11 +- app/components/Nav/App/index.js | 5 + .../UI/CollectibleContracts/constants.ts | 5 +- .../UI/CollectibleContracts/index.js | 30 +- .../UI/CollectibleContracts/index.test.tsx | 120 ++++ .../UI/CollectibleDetectionModal/index.tsx | 59 +- app/components/Views/Collectible/index.js | 7 +- .../NFTAutoDetectionModal.styles.ts | 29 + .../NFTAutoDetectionModal.test.tsx | 99 +++ .../NFTAutoDetectionModal.tsx | 103 +++ .../NFTAutoDetectionModal.test.tsx.snap | 669 ++++++++++++++++++ .../SecuritySettings.test.tsx.snap | 2 +- app/components/Views/Wallet/index.tsx | 95 ++- app/constants/navigation/Routes.ts | 1 + app/core/Analytics/MetaMetrics.events.ts | 10 + app/core/Engine.ts | 6 +- app/images/wallet-alpha.png | Bin 0 -> 22880 bytes app/reducers/collectibles/index.js | 26 + app/reducers/collectibles/index.test.ts | 9 + app/reducers/security/index.test.ts | 1 + app/reducers/security/index.ts | 6 + app/util/test/initial-background-state.json | 2 +- e2e/fixtures/fixture-builder.js | 2 +- e2e/pages/modals/NftDetectionModal.js | 27 + .../Modals/NftDetectionModal.selectors.js | 10 + e2e/specs/assets/nft-detection-modal.spec.js | 95 +++ .../@metamask+assets-controllers+30.0.0.patch | 231 ++++++ 30 files changed, 1635 insertions(+), 54 deletions(-) create mode 100644 app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.styles.ts create mode 100644 app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.test.tsx create mode 100644 app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx create mode 100644 app/components/Views/NFTAutoDetectionModal/__snapshots__/NFTAutoDetectionModal.test.tsx.snap create mode 100644 app/images/wallet-alpha.png create mode 100644 e2e/pages/modals/NftDetectionModal.js create mode 100644 e2e/selectors/Modals/NftDetectionModal.selectors.js create mode 100644 e2e/specs/assets/nft-detection-modal.spec.js diff --git a/app/actions/security/index.ts b/app/actions/security/index.ts index 1b6ce9ae390..e0e87a0ef55 100644 --- a/app/actions/security/index.ts +++ b/app/actions/security/index.ts @@ -7,6 +7,7 @@ export enum ActionType { USER_SELECTED_AUTOMATIC_SECURITY_CHECKS_OPTION = 'USER_SELECTED_AUTOMATIC_SECURITY_CHECKS_OPTION', SET_AUTOMATIC_SECURITY_CHECKS_MODAL_OPEN = 'SET_AUTOMATIC_SECURITY_CHECKS_MODAL_OPEN', SET_DATA_COLLECTION_FOR_MARKETING = 'SET_DATA_COLLECTION_FOR_MARKETING', + SET_NFT_AUTO_DETECTION_MODAL_OPEN = 'SET_NFT_AUTO_DETECTION_MODAL_OPEN', } export interface AllowLoginWithRememberMeUpdated @@ -29,6 +30,11 @@ export interface SetAutomaticSecurityChecksModalOpen open: boolean; } +export interface SetNftAutoDetectionModalOpen + extends ReduxAction { + open: boolean; +} + export interface SetDataCollectionForMarketing extends ReduxAction { enabled: boolean; @@ -39,7 +45,8 @@ export type Action = | AutomaticSecurityChecks | UserSelectedAutomaticSecurityChecksOptions | SetAutomaticSecurityChecksModalOpen - | SetDataCollectionForMarketing; + | SetDataCollectionForMarketing + | SetNftAutoDetectionModalOpen; export const setAllowLoginWithRememberMe = ( enabled: boolean, @@ -68,6 +75,13 @@ export const setAutomaticSecurityChecksModalOpen = ( open, }); +export const setNftAutoDetectionModalOpen = ( + open: boolean, +): SetNftAutoDetectionModalOpen => ({ + type: ActionType.SET_NFT_AUTO_DETECTION_MODAL_OPEN, + open, +}); + export const setDataCollectionForMarketing = (enabled: boolean) => ({ type: ActionType.SET_DATA_COLLECTION_FOR_MARKETING, enabled, diff --git a/app/actions/security/state.ts b/app/actions/security/state.ts index 8c393555156..00c41ca3f8d 100644 --- a/app/actions/security/state.ts +++ b/app/actions/security/state.ts @@ -3,6 +3,7 @@ export interface SecuritySettingsState { automaticSecurityChecksEnabled: boolean; hasUserSelectedAutomaticSecurityCheckOption: boolean; isAutomaticSecurityChecksModalOpen: boolean; + isNFTAutoDetectionModalViewed: boolean; // 'null' represents the user not having set his preference over dataCollectionForMarketing yet dataCollectionForMarketing: boolean | null; } diff --git a/app/component-library/components/Toast/Toast.tsx b/app/component-library/components/Toast/Toast.tsx index b14c5cbd230..e4a37893279 100644 --- a/app/component-library/components/Toast/Toast.tsx +++ b/app/component-library/components/Toast/Toast.tsx @@ -187,6 +187,18 @@ const Toast = forwardRef((_, ref: React.ForwardedRef) => { /> ); } + case ToastVariants.Icon: { + const { iconName, iconColor, backgroundColor } = toastOptions; + return ( + + ); + } } }; diff --git a/app/component-library/components/Toast/Toast.types.ts b/app/component-library/components/Toast/Toast.types.ts index 9b0d3b0413f..998d3a17056 100644 --- a/app/component-library/components/Toast/Toast.types.ts +++ b/app/component-library/components/Toast/Toast.types.ts @@ -12,6 +12,7 @@ export enum ToastVariants { Plain = 'Plain', Account = 'Account', Network = 'Network', + Icon = 'Icon', } /** @@ -65,13 +66,21 @@ interface NetworkToastOption extends BaseToastVariants { networkImageSource: ImageSourcePropType; } +interface IconToastOption extends BaseToastVariants { + variant: ToastVariants.Icon; + iconName?: string; + iconColor?: string; + backgroundColor?: string; +} + /** * Different toast options combined in a union type. */ export type ToastOptions = | PlainToastOption | AccountToastOption - | NetworkToastOption; + | NetworkToastOption + | IconToastOption; /** * Toast component reference. diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 6021355e2ac..f0316226cd3 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -110,6 +110,7 @@ import OnboardingSuccess from '../../Views/OnboardingSuccess'; import DefaultSettings from '../../Views/OnboardingSuccess/DefaultSettings'; import BasicFunctionalityModal from '../../UI/BasicFunctionality/BasicFunctionalityModal/BasicFunctionalityModal'; import SmartTransactionsOptInModal from '../../Views/SmartTransactionsOptInModal/SmartTranactionsOptInModal'; +import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal'; const clearStackNavigatorOptions = { headerShown: false, @@ -693,6 +694,10 @@ const App = ({ userLoggedIn }) => { name={Routes.SHEET.SHOW_NFT_DISPLAY_MEDIA} component={ShowDisplayNftMediaSheet} /> + ); diff --git a/app/components/UI/CollectibleContracts/constants.ts b/app/components/UI/CollectibleContracts/constants.ts index 8d33e4b7a69..2682130644b 100644 --- a/app/components/UI/CollectibleContracts/constants.ts +++ b/app/components/UI/CollectibleContracts/constants.ts @@ -1,3 +1,2 @@ -const RefreshTestId = 'refreshControl'; - -export default RefreshTestId; +export const RefreshTestId = 'refreshControl'; +export const SpinnerTestId = 'spinner'; diff --git a/app/components/UI/CollectibleContracts/index.js b/app/components/UI/CollectibleContracts/index.js index 5d76737516c..866d8b96d7b 100644 --- a/app/components/UI/CollectibleContracts/index.js +++ b/app/components/UI/CollectibleContracts/index.js @@ -8,6 +8,7 @@ import { Platform, FlatList, RefreshControl, + ActivityIndicator, } from 'react-native'; import { connect } from 'react-redux'; import { fontStyles } from '../../../styles/common'; @@ -19,6 +20,7 @@ import { collectibleContractsSelector, collectiblesSelector, favoritesCollectiblesSelector, + isNftFetchingProgressSelector, } from '../../../reducers/collectibles'; import { removeFavoriteCollectible } from '../../../actions/collectibles'; import Text from '../../Base/Text'; @@ -44,7 +46,7 @@ import { NFT_TAB_CONTAINER_ID, } from '../../../../wdio/screen-objects/testIDs/Screens/WalletView.testIds'; import { useMetrics } from '../../../components/hooks/useMetrics'; -import RefreshTestId from './constants'; +import { RefreshTestId, SpinnerTestId } from './constants'; const createStyles = (colors) => StyleSheet.create({ @@ -87,6 +89,9 @@ const createStyles = (colors) => marginBottom: 8, fontSize: 14, }, + spinner: { + marginBottom: 8, + }, }); /** @@ -100,6 +105,7 @@ const CollectibleContracts = ({ navigation, collectibleContracts, collectibles: allCollectibles, + isNftFetchingProgress, favoriteCollectibles, removeFavoriteCollectible, useNftDetection, @@ -219,6 +225,14 @@ const CollectibleContracts = ({ const renderFooter = useCallback( () => ( + {isNftFetchingProgress ? ( + + ) : null} + {strings('wallet.no_collectibles')} @@ -233,7 +247,7 @@ const CollectibleContracts = ({ ), - [goToAddCollectible, isAddNFTEnabled, styles], + [goToAddCollectible, isAddNFTEnabled, styles, isNftFetchingProgress], ); const renderCollectibleContract = useCallback( @@ -282,7 +296,7 @@ const CollectibleContracts = ({ NftDetectionController.detectNfts(), NftController.checkAndUpdateAllNftsOwnershipStatus(), ]; - await Promise.all(actions); + await Promise.allSettled(actions); setRefreshing(false); }); }, [setRefreshing]); @@ -322,7 +336,7 @@ const CollectibleContracts = ({ <> {isCollectionDetectionBannerVisible && ( - + )} {renderFavoriteCollectibles()} @@ -355,7 +369,6 @@ const CollectibleContracts = ({ renderFooter, renderEmpty, isCollectionDetectionBannerVisible, - navigation, styles.emptyView, ], ); @@ -391,7 +404,11 @@ CollectibleContracts.propTypes = { * Array of collectibles objects */ collectibles: PropTypes.array, - + /** + * boolean indicating if fetching status is + * still in progress + */ + isNftFetchingProgress: PropTypes.bool, /** * Navigation object required to push * the Asset detail view @@ -426,6 +443,7 @@ const mapStateToProps = (state) => ({ useNftDetection: selectUseNftDetection(state), collectibleContracts: collectibleContractsSelector(state), collectibles: collectiblesSelector(state), + isNftFetchingProgress: isNftFetchingProgressSelector(state), favoriteCollectibles: favoritesCollectiblesSelector(state), isIpfsGatewayEnabled: selectIsIpfsGatewayEnabled(state), displayNftMedia: selectDisplayNftMedia(state), diff --git a/app/components/UI/CollectibleContracts/index.test.tsx b/app/components/UI/CollectibleContracts/index.test.tsx index b3ea91bbf42..946a654ba62 100644 --- a/app/components/UI/CollectibleContracts/index.test.tsx +++ b/app/components/UI/CollectibleContracts/index.test.tsx @@ -495,4 +495,124 @@ describe('CollectibleContracts', () => { expect(spyOnUpdateNftMetadata).toHaveBeenCalledTimes(0); expect(spyOnDetectNfts).toHaveBeenCalledTimes(1); }); + + it('shows spinner if nfts are still being fetched', async () => { + const CURRENT_ACCOUNT = '0x1a'; + const mockState = { + collectibles: { + favorites: {}, + isNftFetchingProgress: true, + }, + engine: { + backgroundState: { + ...initialBackgroundState, + NetworkController: { + network: '1', + providerConfig: { + ticker: 'ETH', + type: 'mainnet', + chainId: '1', + }, + }, + AccountTrackerController: { + accounts: { [CURRENT_ACCOUNT]: { balance: '0' } }, + }, + PreferencesController: { + useNftDetection: true, + displayNftMedia: true, + selectedAddress: CURRENT_ACCOUNT, + identities: { + [CURRENT_ACCOUNT]: { + address: CURRENT_ACCOUNT, + name: 'Account 1', + }, + }, + }, + NftController: { + addNft: jest.fn(), + updateNftMetadata: jest.fn(), + allNfts: { + [CURRENT_ACCOUNT]: { + '1': [], + }, + }, + allNftContracts: { + [CURRENT_ACCOUNT]: { + '1': [], + }, + }, + }, + NftDetectionController: { + detectNfts: jest.fn(), + }, + }, + }, + }; + const { queryByTestId } = renderWithProvider(, { + state: mockState, + }); + + const spinner = queryByTestId('spinner'); + expect(spinner).not.toBeNull(); + }); + + it('Does not show spinner if nfts are not still being fetched', async () => { + const CURRENT_ACCOUNT = '0x1a'; + const mockState = { + collectibles: { + favorites: {}, + }, + engine: { + backgroundState: { + ...initialBackgroundState, + NetworkController: { + network: '1', + providerConfig: { + ticker: 'ETH', + type: 'mainnet', + chainId: '1', + }, + }, + AccountTrackerController: { + accounts: { [CURRENT_ACCOUNT]: { balance: '0' } }, + }, + PreferencesController: { + useNftDetection: true, + displayNftMedia: true, + selectedAddress: CURRENT_ACCOUNT, + identities: { + [CURRENT_ACCOUNT]: { + address: CURRENT_ACCOUNT, + name: 'Account 1', + }, + }, + }, + NftController: { + addNft: jest.fn(), + updateNftMetadata: jest.fn(), + allNfts: { + [CURRENT_ACCOUNT]: { + '1': [], + }, + }, + allNftContracts: { + [CURRENT_ACCOUNT]: { + '1': [], + }, + }, + }, + NftDetectionController: { + detectNfts: jest.fn(), + }, + }, + }, + }; + + const { queryByTestId } = renderWithProvider(, { + state: mockState, + }); + + const spinner = queryByTestId('spinner'); + expect(spinner).toBeNull(); + }); }); diff --git a/app/components/UI/CollectibleDetectionModal/index.tsx b/app/components/UI/CollectibleDetectionModal/index.tsx index 50d2061585d..b66303f25b2 100644 --- a/app/components/UI/CollectibleDetectionModal/index.tsx +++ b/app/components/UI/CollectibleDetectionModal/index.tsx @@ -1,10 +1,24 @@ -import React from 'react'; +import React, { useCallback, useContext } from 'react'; import { StyleSheet, View } from 'react-native'; import { strings } from '../../../../locales/i18n'; import Banner from '../../../component-library/components/Banners/Banner/Banner'; import { BannerVariant } from '../../../component-library/components/Banners/Banner'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; import { TextVariant } from '../../../component-library/components/Texts/Text'; +import { + ToastContext, + ToastVariants, +} from '../../../component-library/components/Toast'; +import { + IconColor, + IconName, +} from '../../../component-library/components/Icons/Icon'; +import { useTheme } from '../../../util/theme'; +import Engine from '../../../core/Engine'; +import { + hideNftFetchingLoadingIndicator, + showNftFetchingLoadingIndicator, +} from '../../../reducers/collectibles'; const styles = StyleSheet.create({ alertBar: { @@ -13,24 +27,33 @@ const styles = StyleSheet.create({ }, }); -interface Props { - /** - * Navigation object needed to link to settings - */ - // TODO: Replace "any" with type - // eslint-disable-next-line @typescript-eslint/no-explicit-any - navigation: any; -} +const CollectibleDetectionModal = () => { + const { colors } = useTheme(); + const { toastRef } = useContext(ToastContext); -const CollectibleDetectionModal = ({ navigation }: Props) => { - const goToSecuritySettings = () => { - navigation.navigate('SettingsView', { - screen: 'SecuritySettings', - params: { - scrollToDetectNFTs: true, - }, + const showToastAndEnableNFtDetection = useCallback(async () => { + // show toast + toastRef?.current?.showToast({ + variant: ToastVariants.Icon, + labelOptions: [{ label: strings('toast.nft_detection_enabled') }], + iconName: IconName.CheckBold, + iconColor: IconColor.Default, + backgroundColor: colors.primary.inverse, + hasNoTimeout: false, }); - }; + // set nft autodetection + const { PreferencesController, NftDetectionController } = Engine.context; + PreferencesController.setDisplayNftMedia(true); + PreferencesController.setUseNftDetection(true); + // Call detect nfts + showNftFetchingLoadingIndicator(); + try { + await NftDetectionController.detectNfts(); + } finally { + hideNftFetchingLoadingIndicator(); + } + }, [colors.primary.inverse, toastRef]); + return ( { actionButtonProps={{ variant: ButtonVariants.Link, label: strings('wallet.nfts_autodetect_cta'), - onPress: goToSecuritySettings, + onPress: showToastAndEnableNFtDetection, textVariant: TextVariant.BodyMD, }} /> diff --git a/app/components/Views/Collectible/index.js b/app/components/Views/Collectible/index.js index 4bbd42ae402..0af7da45279 100644 --- a/app/components/Views/Collectible/index.js +++ b/app/components/Views/Collectible/index.js @@ -78,8 +78,11 @@ class Collectible extends PureComponent { onRefresh = async () => { this.setState({ refreshing: true }); const { NftDetectionController } = Engine.context; - await NftDetectionController.detectNfts(); - this.setState({ refreshing: false }); + try { + await NftDetectionController.detectNfts(); + } finally { + this.setState({ refreshing: false }); + } }; hideCollectibleContractModal = () => { diff --git a/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.styles.ts b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.styles.ts new file mode 100644 index 00000000000..87f6afbc2bc --- /dev/null +++ b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.styles.ts @@ -0,0 +1,29 @@ +import { StyleSheet } from 'react-native'; + +/** + * Style sheet function for NFT auto detection modal component. + * + * @returns StyleSheet object. + */ +const styleSheet = () => + StyleSheet.create({ + container: { + alignItems: 'center', + }, + image: { + width: 219, + height: 219, + }, + description: { + marginLeft: 32, + marginRight: 32, + }, + buttonsContainer: { + paddingTop: 24, + marginLeft: 16, + marginRight: 16, + }, + spacer: { height: 8 }, + }); + +export default styleSheet; diff --git a/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.test.tsx b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.test.tsx new file mode 100644 index 00000000000..f21fa098833 --- /dev/null +++ b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import NFTAutoDetectionModal from './NFTAutoDetectionModal'; +import renderWithProvider from '../../../util/test/renderWithProvider'; +import { createStackNavigator } from '@react-navigation/stack'; +import Routes from '../../../constants/navigation/Routes'; +import Engine from '../../../core/Engine'; +import { fireEvent } from '@testing-library/react-native'; +import { RootState } from 'app/reducers'; + +const mockEngine = Engine; + +const setUseNftDetectionSpy = jest.spyOn( + Engine.context.PreferencesController, + 'setUseNftDetection', +); + +const setDisplayNftMediaSpy = jest.spyOn( + Engine.context.PreferencesController, + 'setDisplayNftMedia', +); +jest.mock('../../../core/Engine', () => ({ + init: () => mockEngine.init({}), + context: { + PreferencesController: { + setUseNftDetection: jest.fn(), + setDisplayNftMedia: jest.fn(), + }, + }, +})); + +type PartialDeepState = { + [P in keyof T]?: PartialDeepState; +}; + +const initialState = { + engine: { + backgroundState: { + PreferencesController: { + displayNftMedia: true, + }, + }, + }, +}; + +const Stack = createStackNavigator(); + +const renderComponent = (state: PartialDeepState = {}) => + renderWithProvider( + + + {() => } + + , + { state }, + ); +describe('NFT Auto detection modal', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('render matches snapshot', () => { + const { toJSON } = renderComponent(initialState); + expect(toJSON()).toMatchSnapshot(); + }); + + it('calls setUseNftDetection and setDisplayNftMedia when clicking on allow button with nftDisplayMedia initially off', () => { + const { getByTestId } = renderComponent({ + engine: { + backgroundState: { + PreferencesController: { + displayNftMedia: false, + }, + }, + }, + }); + const allowButton = getByTestId('allow'); + + fireEvent.press(allowButton); + expect(setUseNftDetectionSpy).toHaveBeenCalled(); + expect(setDisplayNftMediaSpy).toHaveBeenCalled(); + }); + + it('calls setDisplayNftMedia when clicking on allow button if displayNftMedia if on', () => { + const { getByTestId } = renderComponent(initialState); + const allowButton = getByTestId('allow'); + + fireEvent.press(allowButton); + expect(setUseNftDetectionSpy).toHaveBeenCalled(); + expect(setDisplayNftMediaSpy).not.toHaveBeenCalled(); + }); + + it('Does not call setUseNftDetection nor setDisplayNftMedia when clicking on not right now button', () => { + const { getByTestId } = renderComponent(initialState); + const cancelButton = getByTestId('cancel'); + + fireEvent.press(cancelButton); + expect(setUseNftDetectionSpy).not.toHaveBeenCalled(); + expect(setDisplayNftMediaSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx new file mode 100644 index 00000000000..bf787491664 --- /dev/null +++ b/app/components/Views/NFTAutoDetectionModal/NFTAutoDetectionModal.tsx @@ -0,0 +1,103 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable import/no-commonjs */ +/* eslint-disable @typescript-eslint/no-var-requires */ +import React, { useRef, useCallback } from 'react'; +import BottomSheet, { + BottomSheetRef, +} from '../../../component-library/components/BottomSheets/BottomSheet'; +import { strings } from '../../../../locales/i18n'; +import { useStyles } from '../../../component-library/hooks'; +import styleSheet from './NFTAutoDetectionModal.styles'; +import SheetHeader from '../../../component-library/components/Sheet/SheetHeader'; +import Text from '../../../component-library/components/Texts/Text'; +import { View, Image } from 'react-native'; +import { NftDetectionModalSelectorsIDs } from '../../../../e2e/selectors/Modals/NftDetectionModal.selectors'; + +import Button, { + ButtonSize, + ButtonVariants, + ButtonWidthTypes, +} from '../../../component-library/components/Buttons/Button'; +import { useNavigation } from '@react-navigation/native'; +import Engine from '../../../core/Engine'; +import { useMetrics } from '../../../components/hooks/useMetrics'; +import { MetaMetricsEvents } from '../../../core/Analytics'; +import { selectChainId } from '../../../selectors/networkController'; +import { useSelector } from 'react-redux'; +import { selectDisplayNftMedia } from '../../../selectors/preferencesController'; + +const walletImage = require('../../../images/wallet-alpha.png'); + +const NFTAutoDetectionModal = () => { + const { styles } = useStyles(styleSheet, {}); + const sheetRef = useRef(null); + const navigation = useNavigation(); + const chainId = useSelector(selectChainId); + const displayNftMedia = useSelector(selectDisplayNftMedia); + const { trackEvent } = useMetrics(); + const enableNftDetectionAndDismissModal = useCallback( + (value: boolean) => { + if (value) { + const { PreferencesController } = Engine.context; + if (!displayNftMedia) { + PreferencesController.setDisplayNftMedia(true); + } + PreferencesController.setUseNftDetection(true); + trackEvent(MetaMetricsEvents.NFT_AUTO_DETECTION_MODAL_ENABLE, { + chainId, + }); + } else { + trackEvent(MetaMetricsEvents.NFT_AUTO_DETECTION_MODAL_DISABLE, { + chainId, + }); + } + + if (sheetRef?.current) { + sheetRef.current.onCloseBottomSheet(); + } else { + navigation.goBack(); + } + }, + [displayNftMedia, trackEvent, chainId, navigation], + ); + + return ( + + + + + + + + {strings('enable_nft-auto-detection.description')} + + • {strings('enable_nft-auto-detection.immediateAccess')} + • {strings('enable_nft-auto-detection.navigate')} + • {strings('enable_nft-auto-detection.dive')} + + +