From 631343e8e760ea4b1c279db2686aa62e18b97696 Mon Sep 17 00:00:00 2001 From: Will Beaumont <4557711+wjbeau@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:42:46 +0100 Subject: [PATCH 1/2] fix: show asset suggestions on empty Add Asset query [PERA-4159] --- .../AddAssetScreen/useAddAssetScreen.ts | 2 +- .../api/assets/__tests__/endpoints.test.ts | 9 +++ .../assets/src/api/assets/search-endpoints.ts | 5 +- .../hooks/__tests__/useGlobalSearch.spec.ts | 75 +++++++++++++++++++ packages/search/src/hooks/useGlobalSearch.ts | 20 ++++- 5 files changed, 105 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/modules/assets/screens/AddAssetScreen/useAddAssetScreen.ts b/apps/mobile/src/modules/assets/screens/AddAssetScreen/useAddAssetScreen.ts index 345aabb0b..78e772624 100644 --- a/apps/mobile/src/modules/assets/screens/AddAssetScreen/useAddAssetScreen.ts +++ b/apps/mobile/src/modules/assets/screens/AddAssetScreen/useAddAssetScreen.ts @@ -82,7 +82,7 @@ export const useAddAssetScreen = ( } = useGlobalSearch({ debounceMs: SEARCH_DEBOUNCE_TIME, scopes: ['assets'], - remoteAssets: { hasCollectible }, + remoteAssets: { hasCollectible, showOnEmptyQuery: true }, }) const results = searchResults.remoteAssets const isError = false diff --git a/packages/assets/src/api/assets/__tests__/endpoints.test.ts b/packages/assets/src/api/assets/__tests__/endpoints.test.ts index 4786f292c..f6bc5110b 100644 --- a/packages/assets/src/api/assets/__tests__/endpoints.test.ts +++ b/packages/assets/src/api/assets/__tests__/endpoints.test.ts @@ -211,4 +211,13 @@ describe('searchAssets', () => { expect(call.params).not.toHaveProperty('cursor') expect(call.params).not.toHaveProperty('has_collectible') }) + + test('omits q when query is empty so the backend returns suggestions', async () => { + queryClientMock.mockResolvedValue({ data: validSearchResponse }) + + await searchAssets({ query: '', network: 'mainnet' }) + + const call = queryClientMock.mock.calls[0][0] + expect(call.params).not.toHaveProperty('q') + }) }) diff --git a/packages/assets/src/api/assets/search-endpoints.ts b/packages/assets/src/api/assets/search-endpoints.ts index ce6a0e1ec..32620f264 100644 --- a/packages/assets/src/api/assets/search-endpoints.ts +++ b/packages/assets/src/api/assets/search-endpoints.ts @@ -36,10 +36,13 @@ export const searchAssets = async ({ hasCollectible, }: SearchAssetsParams) => { const params: Record = { - q: query, limit, } + if (query.length > 0) { + params.q = query + } + if (cursor) { params.cursor = cursor } diff --git a/packages/search/src/hooks/__tests__/useGlobalSearch.spec.ts b/packages/search/src/hooks/__tests__/useGlobalSearch.spec.ts index e8b3b7a9f..26e8d5d49 100644 --- a/packages/search/src/hooks/__tests__/useGlobalSearch.spec.ts +++ b/packages/search/src/hooks/__tests__/useGlobalSearch.spec.ts @@ -191,6 +191,81 @@ describe('useGlobalSearch', () => { expect(lastCall?.[1]?.enabled).toBe(false) }) + test('showOnEmptyQuery runs the remote search with no query and surfaces suggestions', () => { + mockAllAccounts.mockReturnValue([makeAccount('ALICE', 'Alice')]) + mockFindContacts.mockReturnValue([]) + setOwnedAssets([]) + const suggestions = [ + { + assetId: '31566704', + name: 'USD Coin', + unitName: 'USDC', + logo: null, + verificationTier: 'trusted', + usdValue: null, + type: 'standard_asset', + collectibleTitle: null, + collectibleImage: null, + collectionName: null, + }, + ] + mockUseAssetSearchQuery.mockReturnValue({ + results: suggestions, + isLoading: false, + isError: false, + isFetchingNextPage: false, + hasNextPage: false, + fetchNextPage: vi.fn(), + }) + + const { result } = renderHook( + () => + useGlobalSearch({ + debounceMs: 0, + scopes: ['assets'], + remoteAssets: { showOnEmptyQuery: true }, + }), + { wrapper: makeWrapper() }, + ) + + const lastCall = + mockUseAssetSearchQuery.mock.calls[ + mockUseAssetSearchQuery.mock.calls.length - 1 + ] + expect(lastCall?.[0]).toBe('') + expect(lastCall?.[1]?.enabled).toBe(true) + expect(result.current.value).toBe('') + expect(result.current.results.remoteAssets).toEqual(suggestions) + expect(result.current.results.accounts).toEqual([]) + expect(result.current.hasResults).toBe(true) + }) + + test('showOnEmptyQuery surfaces the remote loading state for the suggestion fetch', () => { + mockAllAccounts.mockReturnValue([]) + mockFindContacts.mockReturnValue([]) + setOwnedAssets([]) + mockUseAssetSearchQuery.mockReturnValue({ + results: [], + isLoading: true, + isError: false, + isFetchingNextPage: false, + hasNextPage: false, + fetchNextPage: vi.fn(), + }) + + const { result } = renderHook( + () => + useGlobalSearch({ + debounceMs: 0, + scopes: ['assets'], + remoteAssets: { showOnEmptyQuery: true }, + }), + { wrapper: makeWrapper() }, + ) + + expect(result.current.isLoading).toBe(true) + }) + test('remoteAssets option surfaces backend search results', async () => { mockAllAccounts.mockReturnValue([]) mockFindContacts.mockReturnValue([]) diff --git a/packages/search/src/hooks/useGlobalSearch.ts b/packages/search/src/hooks/useGlobalSearch.ts index 0b3673e9e..8c305814c 100644 --- a/packages/search/src/hooks/useGlobalSearch.ts +++ b/packages/search/src/hooks/useGlobalSearch.ts @@ -31,6 +31,10 @@ const DEFAULT_DEBOUNCE_MS = 300 export type RemoteAssetsOptions = { /** Restrict remote asset results to collectibles (NFTs). */ hasCollectible?: boolean + /** When true, run the remote asset search even with an empty query so + * the backend returns a default suggestion list. Local sections + * (accounts, contacts, owned assets) remain empty until the user types. */ + showOnEmptyQuery?: boolean } export type UseGlobalSearchOptions = { @@ -87,15 +91,22 @@ export const useGlobalSearch = ( return allOwnedAssets.filter(assetFilter) }, [includesAssets, allOwnedAssets, assetFilter]) + const showRemoteOnEmpty = remoteAssetsOptions?.showOnEmptyQuery ?? false const shouldRunRemoteAssets = - remoteAssetsEnabled && includesAssets && hasQuery + remoteAssetsEnabled && includesAssets && (hasQuery || showRemoteOnEmpty) const remoteAssetQuery = useAssetSearchQuery(debouncedValue, { hasCollectible: remoteAssetsOptions?.hasCollectible, enabled: shouldRunRemoteAssets, }) const results = useMemo(() => { - if (!hasQuery) return EMPTY_GLOBAL_SEARCH_RESULTS + if (!hasQuery) { + if (!shouldRunRemoteAssets) return EMPTY_GLOBAL_SEARCH_RESULTS + return { + ...EMPTY_GLOBAL_SEARCH_RESULTS, + remoteAssets: remoteAssetQuery.results ?? [], + } + } const lowered = debouncedValue.toLowerCase() const compare = (a: string, b: string) => @@ -174,8 +185,9 @@ export const useGlobalSearch = ( results, hasResults, isLoading: - value.length > 0 && - (isDebouncing || isOwnedAssetsLoading || isRemoteLoading), + (value.length > 0 && + (isDebouncing || isOwnedAssetsLoading || isRemoteLoading)) || + (value.length === 0 && showRemoteOnEmpty && isRemoteLoading), hasNextRemotePage: shouldRunRemoteAssets ? remoteAssetQuery.hasNextPage : false, From 03cd8d5afa3068bbafc536b7b3a7dfd83241f416 Mon Sep 17 00:00:00 2001 From: Will Beaumont <4557711+wjbeau@users.noreply.github.com> Date: Mon, 27 Apr 2026 17:48:12 +0100 Subject: [PATCH 2/2] refactor: clean up variable conditions --- packages/search/src/hooks/useGlobalSearch.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/search/src/hooks/useGlobalSearch.ts b/packages/search/src/hooks/useGlobalSearch.ts index 8c305814c..d0a6c9fe9 100644 --- a/packages/search/src/hooks/useGlobalSearch.ts +++ b/packages/search/src/hooks/useGlobalSearch.ts @@ -178,16 +178,19 @@ export const useGlobalSearch = ( results.remoteAssets.length > 0 const isRemoteLoading = shouldRunRemoteAssets && remoteAssetQuery.isLoading + const hasUserQuery = value.length > 0 + const isLoadingTypedQuery = + hasUserQuery && + (isDebouncing || isOwnedAssetsLoading || isRemoteLoading) + const isLoadingEmptyQuerySuggestions = + !hasUserQuery && showRemoteOnEmpty && isRemoteLoading return { value, setValue, results, hasResults, - isLoading: - (value.length > 0 && - (isDebouncing || isOwnedAssetsLoading || isRemoteLoading)) || - (value.length === 0 && showRemoteOnEmpty && isRemoteLoading), + isLoading: isLoadingTypedQuery || isLoadingEmptyQuerySuggestions, hasNextRemotePage: shouldRunRemoteAssets ? remoteAssetQuery.hasNextPage : false,