diff --git a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts index 4033417ed62..953209d9e77 100644 --- a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts +++ b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts @@ -8,13 +8,14 @@ import { useFavorites } from '@/resources/favorites'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import { isAddress } from '@ethersproject/address'; import { rankings } from 'match-sorter'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'; import { useDebouncedCallback } from 'use-debounce'; import { TokenToBuyListItem } from '../components/TokenList/TokenToBuyList'; import { useSwapContext } from '../providers/swap-provider'; import { RecentSwap } from '@/__swaps__/types/swap'; import { useTokenDiscovery } from '../resources/search'; +import { analyticsV2 } from '@/analytics'; export type AssetToBuySectionId = 'bridge' | 'recent' | 'favorites' | 'verified' | 'unverified' | 'other_networks' | 'popular'; @@ -422,28 +423,29 @@ export function useSearchCurrencyLists() { } ); - return useMemo(() => { + const searchCurrencyLists = useMemo(() => { const toChainId = selectedOutputChainId.value ?? ChainId.mainnet; const bridgeResult = memoizedData.filteredBridgeAsset ?? undefined; const crosschainMatches = query === '' ? undefined : verifiedAssets?.filter(asset => asset.chainId !== toChainId); const verifiedResults = query === '' ? verifiedAssets : verifiedAssets?.filter(asset => asset.chainId === toChainId); const unverifiedResults = memoizedData.enableUnverifiedSearch ? unverifiedAssets : undefined; - return { - results: buildListSectionsData({ - combinedData: { - bridgeAsset: bridgeResult, - crosschainExactMatches: crosschainMatches, - unverifiedAssets: unverifiedResults, - verifiedAssets: verifiedResults, - recentSwaps: recentsForChain, - popularAssets: popularAssetsForChain, - }, - favoritesList, - filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address, - }), - isLoading: isLoadingVerifiedAssets || isLoadingUnverifiedAssets || isLoadingPopularAssets, - }; + const results = buildListSectionsData({ + combinedData: { + bridgeAsset: bridgeResult, + crosschainExactMatches: crosschainMatches, + unverifiedAssets: unverifiedResults, + verifiedAssets: verifiedResults, + recentSwaps: recentsForChain, + popularAssets: popularAssetsForChain, + }, + favoritesList, + filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address, + }); + + const isLoading = isLoadingVerifiedAssets || isLoadingUnverifiedAssets || isLoadingPopularAssets; + + return { results, isLoading }; }, [ favoritesList, isLoadingUnverifiedAssets, @@ -458,4 +460,17 @@ export function useSearchCurrencyLists() { recentsForChain, popularAssetsForChain, ]); + + useEffect(() => { + if (searchCurrencyLists.isLoading) return; + const params = { screen: 'swap' as const, total_tokens: 0, no_icon: 0, query }; + for (const assetOrHeader of searchCurrencyLists.results) { + if (assetOrHeader.listItemType === 'header') continue; + if (!assetOrHeader.icon_url) params.no_icon += 1; + params.total_tokens += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, [searchCurrencyLists.results, searchCurrencyLists.isLoading, query]); + + return searchCurrencyLists; } diff --git a/src/analytics/event.ts b/src/analytics/event.ts index 673e71a810d..3696b908736 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -169,6 +169,9 @@ export const event = { tokenDetailsErc20: 'token_details.erc20', tokenDetailsNFT: 'token_details.nft', + // token lists (wallet, swap, send) + tokenList: 'token_list', + // trending tokens viewTrendingToken: 'trending_tokens.view_trending_token', viewRankedCategory: 'trending_tokens.view_ranked_category', @@ -717,6 +720,14 @@ export type EventProperties = { available_data: { description: boolean; image_url: boolean; floorPrice: boolean }; }; + [event.tokenList]: { + screen: 'wallet' | 'swap' | 'send' | 'discover'; + total_tokens: number; + no_icon: number; + no_price?: number; + query?: string; // query is only sent for the swap screen + }; + [event.viewTrendingToken]: { address: TrendingToken['address']; chainId: TrendingToken['chainId']; diff --git a/src/components/Discover/DiscoverSearch.tsx b/src/components/Discover/DiscoverSearch.tsx index 92495a2100d..5aa4ac6d73b 100644 --- a/src/components/Discover/DiscoverSearch.tsx +++ b/src/components/Discover/DiscoverSearch.tsx @@ -7,7 +7,7 @@ import deviceUtils from '@/utils/deviceUtils'; import CurrencySelectionList from '@/components/CurrencySelectionList'; import { Row } from '@/components/layout'; import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; -import { analytics } from '@/analytics'; +import { analytics, analyticsV2 } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; import { useAccountSettings, useSearchCurrencyList, usePrevious, useHardwareBackOnFocus } from '@/hooks'; import { useNavigation } from '@/navigation'; @@ -25,6 +25,7 @@ import { useTheme } from '@/theme'; import { EnrichedExchangeAsset } from '@/components/ExchangeAssetList'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { ChainId, Network } from '@/state/backendNetworks/types'; +import { useTimeoutEffect } from '@/hooks/useTimeout'; export const SearchContainer = styled(Row)({ height: '100%', @@ -270,6 +271,29 @@ export default function DiscoverSearch() { }); }, [sectionListRef, isSearching]); + useTimeoutEffect( + () => { + const assets = currencyList + .filter(a => a.key !== 'profiles') + .map(asset => asset.data) + .flat(); + if (assets.length === 0) return; + const params = { + screen: 'discover' as const, + no_icon: 0, + no_price: 0, + total_tokens: assets.length, + query: searchQueryForSearch, + }; + for (const asset of assets) { + if (!asset.icon_url) params.no_icon += 1; + if (!isNaN(asset.price?.value)) params.no_price += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, + { timeout: 3000, enabled: !isLoading } + ); + return ( diff --git a/src/components/expanded-state/UniqueTokenExpandedState.tsx b/src/components/expanded-state/UniqueTokenExpandedState.tsx index a321d1d8ac4..136f1e1affa 100644 --- a/src/components/expanded-state/UniqueTokenExpandedState.tsx +++ b/src/components/expanded-state/UniqueTokenExpandedState.tsx @@ -417,18 +417,17 @@ const UniqueTokenExpandedState = ({ asset: passedAsset, external }: UniqueTokenE const hideNftMarketplaceAction = isPoap || !slug; - const mountedAt = useRef(Date.now()); useTimeoutEffect( - () => { + ({ elapsedTime }) => { const { address, chainId } = getAddressAndChainIdFromUniqueId(uniqueId); const { name, description, image_url } = asset; analyticsV2.track(analyticsV2.event.tokenDetailsNFT, { - eventSentAfterMs: Date.now() - mountedAt.current, + eventSentAfterMs: elapsedTime, token: { isPoap, isParty: !!isParty, isENS, address, chainId, name, image_url }, available_data: { description: !!description, image_url: !!image_url, floorPrice: !!offer?.floorPrice }, }); }, - 5 * 1000 // 5s + { timeout: 5 * 1000 } ); return ( <> diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index 3bf7aff28ff..4704316db9b 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -257,17 +257,16 @@ export default function ChartExpandedState({ asset }) { [nativeCurrency] ); - const mountedAt = useRef(Date.now()); useTimeoutEffect( - () => { + ({ elapsedTime }) => { const { address, chainId, symbol, name, icon_url, price } = assetWithPrice; analyticsV2.track(analyticsV2.event.tokenDetailsErc20, { - eventSentAfterMs: Date.now() - mountedAt.current, + eventSentAfterMs: elapsedTime, token: { address, chainId, symbol, name, icon_url, price }, available_data: { chart: showChart, description: !!data?.description, iconUrl: !!icon_url }, }); }, - 5 * 1000 // 5s + { timeout: 5 * 1000 } ); return ( diff --git a/src/hooks/useTimeout.ts b/src/hooks/useTimeout.ts index 3c663ea779c..a7d6d44b505 100644 --- a/src/hooks/useTimeout.ts +++ b/src/hooks/useTimeout.ts @@ -18,7 +18,10 @@ export default function useTimeout(): [(func: () => void, ms?: number) => void, return [start, stop, handle]; } -export function useTimeoutEffect(onTimeout: (cancelled: boolean) => void, delay: number) { +export function useTimeoutEffect( + onTimeout: (e: { cancelled: boolean; elapsedTime: number }) => void, + { timeout, enabled = true }: { timeout: number; enabled?: boolean } +) { const callback = useRef(onTimeout); useLayoutEffect(() => { callback.current = onTimeout; @@ -26,12 +29,21 @@ export function useTimeoutEffect(onTimeout: (cancelled: boolean) => void, delay: const timeoutRef = useRef(); useEffect(() => { + if (!enabled) return; const startedAt = Date.now(); - timeoutRef.current = setTimeout(() => callback.current(false), delay); - const timeout = timeoutRef.current; + timeoutRef.current = setTimeout(() => { + callback.current({ + cancelled: false, + elapsedTime: Date.now() - startedAt, + }); + }, timeout); return () => { - clearTimeout(timeout); - if (Date.now() - startedAt < delay) callback.current(true); + if (!timeoutRef.current) return; + clearTimeout(timeoutRef.current); + const elapsedTime = Date.now() - startedAt; + if (elapsedTime < timeout) { + callback.current({ cancelled: true, elapsedTime }); + } }; - }, [delay]); + }, [timeout, enabled]); } diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index a8f6072f1ea..9013f7106c2 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -123,6 +123,16 @@ export default function useWalletSectionsData({ const { isCoinListEdited } = useCoinListEdited(); + useEffect(() => { + if (isLoadingUserAssets || type !== 'wallet') return; + const params = { screen: 'wallet' as const, no_icon: 0, no_price: 0, total_tokens: sortedAssets.length }; + for (const asset of sortedAssets) { + if (!asset.icon_url) params.no_icon += 1; + if (!asset.price?.relative_change_24h) params.no_price += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, [isLoadingUserAssets, sortedAssets, type]); + const walletSections = useMemo(() => { const accountInfo = { hiddenAssets, diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx index c634befe7b0..b09d7d66bbd 100644 --- a/src/screens/SendSheet.tsx +++ b/src/screens/SendSheet.tsx @@ -10,7 +10,7 @@ import { SendAssetForm, SendAssetList, SendContactList, SendHeader } from '../co import { SheetActionButton } from '../components/sheet'; import { getDefaultCheckboxes } from './SendConfirmationSheet'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { analytics } from '@/analytics'; +import { analytics, analyticsV2 } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; import { AssetTypes, NewTransaction, ParsedAddressAsset, TransactionStatus, UniqueAsset } from '@/entities'; import { isNativeAsset } from '@/handlers/assets'; @@ -129,6 +129,7 @@ type OnSubmitProps = { export default function SendSheet() { const { goBack, navigate } = useNavigation(); const sortedAssets = useUserAssetsStore(state => state.legacyUserAssets); + const isLoadingUserAssets = useUserAssetsStore(state => state.isLoadingUserAssets); const { gasFeeParamsBySpeed, gasLimit, @@ -894,6 +895,16 @@ export default function SendSheet() { isUniqueAsset, ]); + useEffect(() => { + if (isLoadingUserAssets || !sortedAssets) return; + const params = { screen: 'send' as const, no_icon: 0, no_price: 0, total_tokens: sortedAssets.length }; + for (const asset of sortedAssets) { + if (!asset.icon_url) params.no_icon += 1; + if (!asset.price?.relative_change_24h) params.no_price += 1; + } + analyticsV2.track(analyticsV2.event.tokenList, params); + }, [isLoadingUserAssets, sortedAssets]); + const sendContactListDataKey = useMemo(() => `${ensSuggestions?.[0]?.address || '_'}`, [ensSuggestions]); const isEmptyWallet = !sortedAssets?.length && !sendableUniqueTokens?.length; diff --git a/src/screens/WalletScreen/index.tsx b/src/screens/WalletScreen/index.tsx index bf5fa201570..95cf97c05db 100644 --- a/src/screens/WalletScreen/index.tsx +++ b/src/screens/WalletScreen/index.tsx @@ -51,7 +51,12 @@ function WalletScreen() { const { wallets } = useWallets(); const walletReady = useSelector(({ appState: { walletReady } }: AppState) => walletReady); - const { isWalletEthZero, isLoadingUserAssets, isLoadingBalance, briefSectionsData: walletBriefSectionsData } = useWalletSectionsData(); + const { + isWalletEthZero, + isLoadingUserAssets, + isLoadingBalance, + briefSectionsData: walletBriefSectionsData, + } = useWalletSectionsData({ type: 'wallet' }); useEffect(() => { if (!wallets) return;