Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions app/components/UI/NftGrid/NftGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ const NftGrid = forwardRef<TabRefreshHandle, NftGridProps>(
useState<Nft | null>(null);
const tw = useTailwind();
const { colors } = useTheme();
const { refreshing, onRefresh } = useNftRefresh();
const { detectNfts, abortDetection, chainIdsToDetectNftsFor } =
useNftDetection();
const { refreshing, onRefresh } = useNftRefresh({
detectNfts,
chainIdsToDetectNftsFor,
});

useImperativeHandle(ref, () => ({
refresh: onRefresh,
Expand Down Expand Up @@ -134,9 +139,6 @@ const NftGrid = forwardRef<TabRefreshHandle, NftGridProps>(
)(state, undefined, addressesOverride),
);

const { detectNfts, abortDetection, chainIdsToDetectNftsFor } =
useNftDetection();

const isInitialMount = useRef(true);
const hasTrackedScreenViewRef = useRef(false);
const hasSeenNftFetchingRef = useRef(false);
Expand Down
75 changes: 27 additions & 48 deletions app/components/UI/NftGrid/useNftRefresh.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { renderHook, act } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import { Hex } from '@metamask/utils';
import { useNftRefresh } from './useNftRefresh';
import Engine from '../../../core/Engine';
import { useNftDetection } from '../../hooks/useNftDetection';
import { selectEvmNetworkConfigurationsByChainId } from '../../../selectors/networkController';
import { selectTokenNetworkFilter } from '../../../selectors/preferencesController';

jest.mock('react-redux', () => ({
useSelector: jest.fn((selector) => selector()),
Expand All @@ -18,10 +17,6 @@ jest.mock('../../../core/Engine', () => ({
},
}));

jest.mock('../../hooks/useNftDetection', () => ({
useNftDetection: jest.fn(),
}));

jest.mock('../../../selectors/networkController', () => ({
selectEvmNetworkConfigurationsByChainId: jest.fn(() => ({
'0x1': {
Expand All @@ -35,36 +30,26 @@ jest.mock('../../../selectors/networkController', () => ({
})),
}));

jest.mock('../../../selectors/preferencesController', () => ({
selectTokenNetworkFilter: jest.fn(() => ({
'0x1': true,
'0x89': true,
})),
}));

describe('useNftRefresh', () => {
const mockDetectNfts = jest.fn();
const mockCheckAndUpdateAllNftsOwnershipStatus = jest.fn();
const defaultChainIds: Hex[] = ['0x1', '0x89'];

const mockUseSelector = useSelector as jest.MockedFunction<
typeof useSelector
>;
const mockUseNftDetection = useNftDetection as jest.MockedFunction<
typeof useNftDetection
>;

const defaultProps = {
detectNfts: mockDetectNfts,
chainIdsToDetectNftsFor: defaultChainIds,
};

beforeEach(() => {
jest.clearAllMocks();

mockDetectNfts.mockResolvedValue(undefined);
mockCheckAndUpdateAllNftsOwnershipStatus.mockResolvedValue(undefined);

mockUseNftDetection.mockReturnValue({
detectNfts: mockDetectNfts,
chainIdsToDetectNftsFor: ['0x1', '0x89'],
abortDetection: jest.fn(),
});

(
Engine.context.NftController
.checkAndUpdateAllNftsOwnershipStatus as jest.Mock
Expand All @@ -83,26 +68,20 @@ describe('useNftRefresh', () => {
},
};
}
if (selector === selectTokenNetworkFilter) {
return {
'0x1': true,
'0x89': true,
};
}
return undefined;
});
});

it('returns refreshing and onRefresh', () => {
const { result } = renderHook(() => useNftRefresh());
const { result } = renderHook(() => useNftRefresh(defaultProps));

expect(result.current.refreshing).toBe(false);
expect(result.current.onRefresh).toBeDefined();
expect(typeof result.current.onRefresh).toBe('function');
});

it('sets refreshing to true during refresh and false after', async () => {
const { result } = renderHook(() => useNftRefresh());
const { result } = renderHook(() => useNftRefresh(defaultProps));

expect(result.current.refreshing).toBe(false);

Expand All @@ -113,8 +92,8 @@ describe('useNftRefresh', () => {
expect(result.current.refreshing).toBe(false);
});

it('calls useNftDetection.detectNfts on refresh', async () => {
const { result } = renderHook(() => useNftRefresh());
it('calls detectNfts on refresh', async () => {
const { result } = renderHook(() => useNftRefresh(defaultProps));

await act(async () => {
await result.current.onRefresh();
Expand All @@ -124,7 +103,7 @@ describe('useNftRefresh', () => {
});

it('calls NftController.checkAndUpdateAllNftsOwnershipStatus for each network', async () => {
const { result } = renderHook(() => useNftRefresh());
const { result } = renderHook(() => useNftRefresh(defaultProps));

await act(async () => {
await result.current.onRefresh();
Expand All @@ -143,7 +122,7 @@ describe('useNftRefresh', () => {
const mockError = new Error('Detection failed');
mockDetectNfts.mockRejectedValueOnce(mockError);

const { result } = renderHook(() => useNftRefresh());
const { result } = renderHook(() => useNftRefresh(defaultProps));

await act(async () => {
await result.current.onRefresh();
Expand All @@ -152,15 +131,13 @@ describe('useNftRefresh', () => {
expect(result.current.refreshing).toBe(false);
});

it('does not call checkAndUpdateAllNftsOwnershipStatus when no network client IDs', async () => {
mockUseSelector.mockImplementation((selector: unknown) => {
if (selector === selectTokenNetworkFilter) {
return {};
}
return undefined;
});

const { result } = renderHook(() => useNftRefresh());
it('does not call checkAndUpdateAllNftsOwnershipStatus when chainIdsToDetectNftsFor is empty', async () => {
const { result } = renderHook(() =>
useNftRefresh({
detectNfts: mockDetectNfts,
chainIdsToDetectNftsFor: [],
}),
);

await act(async () => {
await result.current.onRefresh();
Expand All @@ -180,13 +157,15 @@ describe('useNftRefresh', () => {
},
};
}
if (selector === selectTokenNetworkFilter) {
return { '0x1': true };
}
return undefined;
});

const { result } = renderHook(() => useNftRefresh());
const { result } = renderHook(() =>
useNftRefresh({
detectNfts: mockDetectNfts,
chainIdsToDetectNftsFor: ['0x1'],
}),
);

await act(async () => {
await result.current.onRefresh();
Expand All @@ -210,7 +189,7 @@ describe('useNftRefresh', () => {
callOrder.push('ownership-end');
});

const { result } = renderHook(() => useNftRefresh());
const { result } = renderHook(() => useNftRefresh(defaultProps));

await act(async () => {
await result.current.onRefresh();
Expand Down
27 changes: 15 additions & 12 deletions app/components/UI/NftGrid/useNftRefresh.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Hex } from '@metamask/utils';

import Engine from '../../../core/Engine';
import { useNftDetection } from '../../hooks/useNftDetection';
import { selectTokenNetworkFilter } from '../../../selectors/preferencesController';
import { selectEvmNetworkConfigurationsByChainId } from '../../../selectors/networkController';

interface UseNftRefreshOptions {
detectNfts: (firstPageOnly?: boolean) => Promise<void>;
chainIdsToDetectNftsFor: Hex[];
}

interface UseNftRefreshReturn {
refreshing: boolean;
onRefresh: () => Promise<void>;
}

export const useNftRefresh = (): UseNftRefreshReturn => {
export const useNftRefresh = ({
detectNfts,
chainIdsToDetectNftsFor,
}: UseNftRefreshOptions): UseNftRefreshReturn => {
const allEVMNetworks = useSelector(selectEvmNetworkConfigurationsByChainId);
const tokenNetworkFilter = useSelector(selectTokenNetworkFilter);
const { detectNfts } = useNftDetection();

const [refreshing, setRefreshing] = useState(false);

const allNetworkClientIds = useMemo(
() =>
Object.keys(tokenNetworkFilter).flatMap((chainId) => {
chainIdsToDetectNftsFor.flatMap((chainId) => {
const entry = allEVMNetworks[chainId as `0x${string}`];
if (!entry) {
return [];
Expand All @@ -29,7 +34,7 @@ export const useNftRefresh = (): UseNftRefreshReturn => {
const endpoint = entry.rpcEndpoints[index];
return endpoint?.networkClientId ? [endpoint.networkClientId] : [];
}),
[tokenNetworkFilter, allEVMNetworks],
[chainIdsToDetectNftsFor, allEVMNetworks],
);

const onRefresh = useCallback(async () => {
Expand All @@ -38,13 +43,11 @@ export const useNftRefresh = (): UseNftRefreshReturn => {
setRefreshing(true);

try {
// Use useNftDetection.detectNfts which:
// - Checks if NFT detection is enabled in user preferences
// - Dispatches loading indicators
// - Handles analytics tracking
// detectNfts: checks if NFT detection is enabled, dispatches loading
// indicators, and handles analytics tracking
const detectNftsPromise = detectNfts();

// Also update ownership status for all NFTs
// Also update ownership status for all NFTs across all currently enabled networks
const ownershipPromises = allNetworkClientIds.map((networkClientId) =>
NftController.checkAndUpdateAllNftsOwnershipStatus(networkClientId),
);
Expand Down
8 changes: 6 additions & 2 deletions app/components/Views/Homepage/Sections/NFTs/NFTsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,12 @@ const NFTsSection = forwardRef<SectionRefreshHandle, NFTsSectionProps>(
const ownedNfts = useOwnedNfts();
const hasNfts = ownedNfts.length > 0;
const isNftFetchingProgress = useSelector(isNftFetchingProgressSelector);
const { onRefresh } = useNftRefresh();
const { detectNfts, abortDetection } = useNftDetection();
const { detectNfts, abortDetection, chainIdsToDetectNftsFor } =
useNftDetection();
const { onRefresh } = useNftRefresh({
detectNfts,
chainIdsToDetectNftsFor,
});
const hasLoadedOnceRef = useRef(false);
const isSilentDetectionRef = useRef(false);

Expand Down
Loading