From 05364a94e0972dd266c42116b4a7b5f8ccd996ef Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 20 Dec 2024 16:17:33 -0300 Subject: [PATCH 1/2] Track token lists (#6303) --- .../Swap/hooks/useSearchCurrencyLists.ts | 49 ++++++++++++------- src/analytics/event.ts | 11 +++++ src/components/Discover/DiscoverSearch.tsx | 26 +++++++++- .../UniqueTokenExpandedState.tsx | 7 ++- .../asset/ChartExpandedState.js | 7 ++- src/hooks/useTimeout.ts | 24 ++++++--- src/hooks/useWalletSectionsData.ts | 10 ++++ src/screens/SendSheet.tsx | 13 ++++- src/screens/WalletScreen/index.tsx | 7 ++- 9 files changed, 120 insertions(+), 34 deletions(-) diff --git a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts index 0b3057aaf0b..9760b638d49 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; From 011df9700a4bd5a3f135c6336c2f3bbd28df11c2 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 20 Dec 2024 16:03:32 -0500 Subject: [PATCH 2/2] trending tokens + network selector + explain sheet remove local networkColors (#6363) --- src/components/Discover/TrendingTokens.tsx | 12 ++++++------ src/components/NetworkSwitcher.tsx | 5 +++-- src/screens/ExplainSheet.js | 14 +++++++------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 489b90bbf83..de88bc4b52e 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -1,4 +1,4 @@ -import { DropdownMenu, MenuItem } from '@/components/DropdownMenu'; +import { DropdownMenu } from '@/components/DropdownMenu'; import { globalColors, Text, TextIcon, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; @@ -19,7 +19,6 @@ import { swapsStore } from '@/state/swaps/swapsStore'; import { sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; import { colors } from '@/styles'; import { darkModeThemeColors } from '@/styles/colors'; -import { useTheme } from '@/theme'; import { useCallback, useEffect, useMemo } from 'react'; import React, { Dimensions, FlatList, View } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; @@ -514,7 +513,6 @@ function NoResults() { function NetworkFilter() { const { isDarkMode } = useColorMode(); - const { colors } = useTheme(); const selected = useSharedValue(undefined); const chainId = useTrendingTokensStore(state => state.chainId); @@ -522,6 +520,8 @@ function NetworkFilter() { const { icon, label, lightenedNetworkColor } = useMemo(() => { if (!chainId) return { icon: '􀤆', label: i18n.t(t.all), lightenedNetworkColor: undefined }; + const lightenedNetworkColor = useBackendNetworksStore.getState().getColorsForChainId(chainId, isDarkMode); + return { icon: ( @@ -529,11 +529,11 @@ function NetworkFilter() { ), label: useBackendNetworksStore.getState().getChainsLabel()[chainId], - lightenedNetworkColor: colors.networkColors[chainId] - ? getMixedColor(colors.networkColors[chainId], globalColors.white100, isDarkMode ? 0.55 : 0.6) + lightenedNetworkColor: lightenedNetworkColor + ? getMixedColor(lightenedNetworkColor, globalColors.white100, isDarkMode ? 0.55 : 0.6) : undefined, }; - }, [chainId, colors.networkColors, isDarkMode]); + }, [chainId, isDarkMode]); const setSelected = useCallback( (chainId: ChainId | undefined) => { diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index 61394cdc5d4..2b385abb1fe 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -373,9 +373,10 @@ function AllNetworksSection({ } function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: SharedValue }) { - const { colors } = useTheme(); + const { isDarkMode } = useColorMode(); + const getColorsForChainId = useBackendNetworksStore(state => state.getColorsForChainId); const chainName = useBackendNetworksStore.getState().getChainsLabel()[chainId]; - const chainColor = colors.networkColors[chainId] ? colors.networkColors[chainId] : undefined; + const chainColor = getColorsForChainId(chainId, isDarkMode); const isSelected = useDerivedValue(() => selected.value === chainId); const { animatedStyle } = useNetworkOptionStyle(isSelected, chainColor); diff --git a/src/screens/ExplainSheet.js b/src/screens/ExplainSheet.js index 9e30b06daa6..b63fa165f32 100644 --- a/src/screens/ExplainSheet.js +++ b/src/screens/ExplainSheet.js @@ -733,7 +733,7 @@ export const explainers = (params, theme) => { }, swap_refuel_add: { logo: ( - + { networkName: params?.networkName, gasToken: params?.gasToken, }), - textColor: colors?.networkColors[chainId], - bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.05), + textColor: color, + bgColor: color && colors?.alpha(color, 0.05), onPress: params?.onRefuel, }, }, swap_refuel_deduct: { logo: ( - + { networkName: params?.networkName, gasToken: params?.gasToken, }), - textColor: colors?.networkColors[chainId], - bgColor: colors?.networkColors[chainId] && colors?.alpha(colors?.networkColors[chainId], 0.05), + textColor: color, + bgColor: color && colors?.alpha(color, 0.05), onPress: params?.onRefuel, }, }, swap_refuel_notice: { extraHeight: 50, logo: ( - +