From 0303a66daf1ed86d20293a929c05611c5d0f8673 Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 29 Feb 2024 14:22:16 -0800 Subject: [PATCH 01/19] chore: remove old token balance screen --- src/components/TokenBalance.tsx | 12 +- src/navigator/Navigator.tsx | 6 - src/navigator/types.tsx | 6 - src/tokens/TokenBalances.test.tsx | 185 ------------- src/tokens/TokenBalances.tsx | 436 ------------------------------ 5 files changed, 2 insertions(+), 643 deletions(-) delete mode 100644 src/tokens/TokenBalances.test.tsx delete mode 100644 src/tokens/TokenBalances.tsx diff --git a/src/components/TokenBalance.tsx b/src/components/TokenBalance.tsx index 41b2dbeaca1..75234f5bdbf 100644 --- a/src/components/TokenBalance.tsx +++ b/src/components/TokenBalance.tsx @@ -242,11 +242,7 @@ export function HomeTokenBalance() { ValoraAnalytics.track(HomeEvents.view_token_balances, { totalBalance: totalBalance?.toString(), }) - navigate( - getFeatureGate(StatsigFeatureGates.SHOW_ASSET_DETAILS_SCREEN) - ? Screens.Assets - : Screens.TokenBalances - ) + navigate(Screens.Assets) } const onCloseDialog = () => { @@ -307,11 +303,7 @@ export function FiatExchangeTokenBalance() { ValoraAnalytics.track(FiatExchangeEvents.cico_landing_token_balance, { totalBalance: totalBalance?.toString(), }) - navigate( - getFeatureGate(StatsigFeatureGates.SHOW_ASSET_DETAILS_SCREEN) - ? Screens.Assets - : Screens.TokenBalances - ) + navigate(Screens.Assets) } return ( diff --git a/src/navigator/Navigator.tsx b/src/navigator/Navigator.tsx index 6c7de14c657..f83d7b01fa4 100644 --- a/src/navigator/Navigator.tsx +++ b/src/navigator/Navigator.tsx @@ -104,7 +104,6 @@ import ValidateRecipientIntro, { } from 'src/send/ValidateRecipientIntro' import SwapScreen from 'src/swap/SwapScreen' import AssetsScreen from 'src/tokens/Assets' -import TokenBalancesScreen from 'src/tokens/TokenBalances' import TokenDetailsScreen from 'src/tokens/TokenDetails' import TokenImportScreen from 'src/tokens/TokenImport' import TransactionDetailsScreen from 'src/transactions/feed/TransactionDetailsScreen' @@ -137,11 +136,6 @@ const commonScreens = (Navigator: typeof Stack) => { component={WebViewScreen} options={emptyHeader} /> - { - return { - getFeatureGate: jest.fn(), - getDynamicConfigParams: jest.fn().mockReturnValue({ - showBalances: ['celo-alfajores'], - }), - } -}) - -const storeWithHistoricalPrices = { - tokens: { - tokenBalances: { - ...mockTokenBalancesWithHistoricalPrices, - [mockTestTokenTokenId]: { - address: mockTestTokenAddress, - tokenId: mockTestTokenTokenId, - networkId: NetworkId['celo-alfajores'], - symbol: 'TT', - balance: '50', - priceUsd: '2', - priceFetchedAt: Date.now(), - historicalPricesUsd: { - lastDay: { - price: '1.3', - at: Date.now() - ONE_DAY_IN_MILLIS, - }, - }, - }, - }, - }, -} - -const storeWithPositions = { - tokens: { - tokenBalances: { - [mockCeurTokenId]: { - priceUsd: '1.16', - address: mockCeurAddress, - tokenId: mockCeurTokenId, - networkId: NetworkId['celo-alfajores'], - symbol: 'cEUR', - imageUrl: - 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cEUR.png', - name: 'Celo Euro', - decimals: 18, - balance: '5', - isFeeCurrency: true, - canTransferWithComment: true, - priceFetchedAt: Date.now(), - }, - [mockCusdTokenId]: { - priceUsd: '1.001', - address: mockCusdAddress, - tokenId: mockCusdTokenId, - networkId: NetworkId['celo-alfajores'], - symbol: 'cUSD', - imageUrl: - 'https://raw.githubusercontent.com/ubeswap/default-token-list/master/assets/asset_cUSD.png', - name: 'Celo Dollar', - decimals: 18, - balance: '10', - isFeeCurrency: true, - canTransferWithComment: true, - priceFetchedAt: Date.now(), - }, - }, - }, - positions: { - positions: mockPositions, // Total value of positions is ~$7.91 - }, -} - -const mockScreenProps = getMockStackScreenProps(Screens.TokenBalances) -const mockWalletAddress = '0x123' - -describe('TokenBalancesScreen', () => { - it('renders correctly the price change indicator', async () => { - const store = createMockStore(storeWithHistoricalPrices) - - const tree = render( - - - - ) - - expect(getElementText(tree.getByTestId('tokenBalance:POOF'))).toBe('5.00') - expect(getElementText(tree.getByTestId('tokenLocalBalance:POOF'))).toBe('₱0.67') - - expect(getElementText(tree.getByTestId('percentageIndicator:POOF'))).toBe('33.33%') - expect(tree.queryByTestId('percentageIndicator:POOF:DownIndicator')).toBeTruthy() - - expect(getElementText(tree.getByTestId('percentageIndicator:TT'))).toBe('53.85%') - expect(tree.queryByTestId('percentageIndicator:TT:UpIndicator')).toBeTruthy() - }) - - it('renders correctly the NFT viewer banner', () => { - const store = createMockStore({ - web3: { - account: mockWalletAddress, - }, - }) - - const tree = render( - - - - ) - expect(tree.queryByTestId('NftViewerBanner')).toBeTruthy() - - fireEvent.press(tree.getByTestId('NftViewerBanner')) - expect(ValoraAnalytics.track).toHaveBeenCalledWith(HomeEvents.view_nft_home_assets) - expect(navigate).toHaveBeenCalledWith(Screens.WebViewScreen, { - uri: `${networkConfig.nftsValoraAppUrl}?address=${mockWalletAddress}&hide-header=true`, - }) - }) - - it('renders the correct components when there are positions', () => { - jest.mocked(getFeatureGate).mockReturnValue(true) - const store = createMockStore(storeWithPositions) - - const { getByTestId, getAllByTestId, queryAllByTestId, getByText } = render( - - - - ) - - expect(getByTestId('TokenBalances/SegmentedControl')).toBeTruthy() - expect(getAllByTestId('TokenBalanceItem')).toHaveLength(2) - expect(queryAllByTestId('PositionItem')).toHaveLength(0) - - fireEvent.press(getByText('assetsSegmentedControl.dappPositions')) - - expect(getAllByTestId('PositionItem')).toHaveLength(3) - expect(queryAllByTestId('TokenBalanceItem')).toHaveLength(0) - }) - - it('renders the correct information in positions', () => { - jest.mocked(getFeatureGate).mockReturnValue(true) - const store = createMockStore(storeWithPositions) - - const { getAllByTestId, getByText } = render( - - - - ) - - fireEvent.press(getByText('assetsSegmentedControl.dappPositions')) - - const firstPositionItem = getAllByTestId('PositionItem')[0] - const lastPositionItem = getAllByTestId('PositionItem')[2] - - expect(firstPositionItem).toHaveTextContent('MOO / CELO') - expect(firstPositionItem).toHaveTextContent('Pool') - expect(firstPositionItem).toHaveTextContent('₱3.34') - - expect(lastPositionItem).toHaveTextContent('CELO / cUSD') - expect(lastPositionItem).toHaveTextContent('Farm') - expect(lastPositionItem).toHaveTextContent('₱1.76') - }) -}) diff --git a/src/tokens/TokenBalances.tsx b/src/tokens/TokenBalances.tsx deleted file mode 100644 index 6d4fdd0ea8c..00000000000 --- a/src/tokens/TokenBalances.tsx +++ /dev/null @@ -1,436 +0,0 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack' -import BigNumber from 'bignumber.js' -import React, { useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { - LayoutChangeEvent, - PixelRatio, - SectionList, - SectionListData, - SectionListProps, - StyleSheet, - Text, - View, -} from 'react-native' -import Animated, { - interpolateColor, - useAnimatedScrollHandler, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated' -import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { useSelector } from 'react-redux' -import { AssetsEvents, HomeEvents } from 'src/analytics/Events' -import ValoraAnalytics from 'src/analytics/ValoraAnalytics' -import Button, { BtnSizes, BtnTypes } from 'src/components/Button' -import { AssetsTokenBalance } from 'src/components/TokenBalance' -import Touchable from 'src/components/Touchable' -import OpenLinkIcon from 'src/icons/OpenLinkIcon' -import { useDollarsToLocalAmount } from 'src/localCurrency/hooks' -import { getLocalCurrencySymbol } from 'src/localCurrency/selectors' -import { headerWithBackButton } from 'src/navigator/Headers' -import { navigate } from 'src/navigator/NavigationService' -import { Screens } from 'src/navigator/Screens' -import useScrollAwareHeader from 'src/navigator/ScrollAwareHeader' -import { StackParamList } from 'src/navigator/types' -import { - positionsSelector, - positionsWithClaimableRewardsSelector, - totalPositionsBalanceUsdSelector, -} from 'src/positions/selectors' -import { Position } from 'src/positions/types' -import { getFeatureGate } from 'src/statsig' -import { StatsigFeatureGates } from 'src/statsig/types' -import Colors from 'src/styles/colors' -import fontStyles from 'src/styles/fonts' -import { Shadow, Spacing, getShadowStyle } from 'src/styles/styles' -import { PositionItem, TokenBalanceItem } from 'src/tokens/AssetItem' -import SegmentedControl from 'src/tokens/SegmentedControl' -import { - useTokenPricesAreStale, - useTokensWithTokenBalance, - useTotalTokenBalance, -} from 'src/tokens/hooks' -import { TokenBalance } from 'src/tokens/slice' -import { getSupportedNetworkIdsForTokenBalances, sortByUsdBalance } from 'src/tokens/utils' -import networkConfig from 'src/web3/networkConfig' -import { walletAddressSelector } from 'src/web3/selectors' - -type Props = NativeStackScreenProps -interface SectionData { - appName?: string -} - -const AnimatedSectionList = - Animated.createAnimatedComponent>( - SectionList - ) - -const assetIsPosition = (asset: Position | TokenBalance): asset is Position => - 'type' in asset && (asset.type === 'app-token' || asset.type === 'contract-position') - -export enum AssetViewType { - WalletAssets = 0, - Positions = 1, -} - -// offset relative to the bottom of the non sticky header component, where the -// screen header opacity animation starts -const HEADER_OPACITY_ANIMATION_START_OFFSET = 44 -// distance in points over which the screen header opacity animation is applied -const HEADER_OPACITY_ANIMATION_DISTANCE = 20 - -function TokenBalancesScreen({ navigation, route }: Props) { - const { t } = useTranslation() - - const activeView = route.params?.activeView ?? AssetViewType.WalletAssets - - const supportedNetworkIds = getSupportedNetworkIdsForTokenBalances() - const tokens = useTokensWithTokenBalance() - const localCurrencySymbol = useSelector(getLocalCurrencySymbol) - const totalTokenBalanceLocal = useTotalTokenBalance() ?? new BigNumber(0) - const tokensAreStale = useTokenPricesAreStale(supportedNetworkIds) - const shouldShowNftGallery = getFeatureGate(StatsigFeatureGates.SHOW_IN_APP_NFT_GALLERY) - const walletAddress = useSelector(walletAddressSelector) - const insets = useSafeAreaInsets() - - // TODO: Update this to filter out unsupported networks once positions support non-Celo chains - const positions = useSelector(positionsSelector) - const showPositions = getFeatureGate(StatsigFeatureGates.SHOW_POSITIONS) - const displayPositions = showPositions && positions.length > 0 - - const dappShortcutsEnabled = getFeatureGate(StatsigFeatureGates.SHOW_CLAIM_SHORTCUTS) - const positionsWithClaimableRewards = useSelector(positionsWithClaimableRewardsSelector) - const showClaimRewards = dappShortcutsEnabled && positionsWithClaimableRewards.length > 0 - - // TODO(ACT-1095): Update these to filter out unsupported networks once positions support non-Celo chains - const totalPositionsBalanceUsd = useSelector(totalPositionsBalanceUsdSelector) - const totalPositionsBalanceLocal = useDollarsToLocalAmount(totalPositionsBalanceUsd) - const totalBalanceLocal = totalTokenBalanceLocal?.plus(totalPositionsBalanceLocal ?? 0) - - const [nonStickyHeaderHeight, setNonStickyHeaderHeight] = useState(0) - const [listHeaderHeight, setListHeaderHeight] = useState(0) - const [listFooterHeight, setListFooterHeight] = useState(0) - - const scrollPosition = useSharedValue(0) - const footerPosition = useSharedValue(0) - const handleScroll = useAnimatedScrollHandler<{ prevScrollY: number }>( - { - onScroll: (event, ctx) => { - const scrollY = event.contentOffset.y - scrollPosition.value = scrollY - - function clamp(value: number, min: number, max: number) { - return Math.min(Math.max(value, min), max) - } - - // Omit overscroll in the calculation - const clampedScrollY = clamp( - scrollY, - 0, - event.contentSize.height - event.layoutMeasurement.height - ) - - // This does the same as React Native's Animated.diffClamp - const diff = clampedScrollY - ctx.prevScrollY - footerPosition.value = clamp(footerPosition.value + diff, 0, listFooterHeight) - ctx.prevScrollY = clampedScrollY - }, - onBeginDrag: (event, ctx) => { - ctx.prevScrollY = event.contentOffset.y - }, - }, - [listFooterHeight] - ) - - const animatedListHeaderStyles = useAnimatedStyle(() => { - if (nonStickyHeaderHeight === 0 || !displayPositions) { - return { - shadowColor: 'transparent', - transform: [ - { - translateY: -scrollPosition.value, - }, - ], - } - } - - return { - transform: [ - { - translateY: - scrollPosition.value > nonStickyHeaderHeight - ? -nonStickyHeaderHeight - : -scrollPosition.value, - }, - ], - shadowColor: interpolateColor( - scrollPosition.value, - [nonStickyHeaderHeight - 10, nonStickyHeaderHeight + 10], - ['transparent', 'rgba(48, 46, 37, 0.15)'] - ), - } - }, [scrollPosition.value, nonStickyHeaderHeight, displayPositions]) - - const animatedFooterStyles = useAnimatedStyle(() => { - return { - transform: [ - { - translateY: footerPosition.value, - }, - ], - } - }, [footerPosition.value]) - - useScrollAwareHeader({ - navigation, - title: t('totalAssets'), - subtitle: - !tokensAreStale && totalBalanceLocal.gte(0) - ? t('totalBalanceWithLocalCurrencySymbol', { - localCurrencySymbol, - totalBalance: totalBalanceLocal.toFormat(2), - }) - : `${localCurrencySymbol} -`, - scrollPosition, - startFadeInPosition: nonStickyHeaderHeight - HEADER_OPACITY_ANIMATION_START_OFFSET, - animationDistance: HEADER_OPACITY_ANIMATION_DISTANCE, - }) - - const onPressNFTsBanner = () => { - ValoraAnalytics.track(HomeEvents.view_nft_home_assets) - shouldShowNftGallery - ? navigate(Screens.NftGallery) - : navigate(Screens.WebViewScreen, { - uri: `${networkConfig.nftsValoraAppUrl}?address=${walletAddress}&hide-header=true`, - }) - } - - const handleMeasureNonStickyHeaderHeight = (event: LayoutChangeEvent) => { - setNonStickyHeaderHeight(event.nativeEvent.layout.height) - } - - const handleMeasureListHeaderHeight = (event: LayoutChangeEvent) => { - setListHeaderHeight(event.nativeEvent.layout.height) - } - - const handleMeasureListFooterHeight = (event: LayoutChangeEvent) => { - setListFooterHeight(event.nativeEvent.layout.height) - } - - const handleChangeActiveView = (_: string, index: number) => { - navigation.setParams({ activeView: index }) - ValoraAnalytics.track( - index === AssetViewType.WalletAssets - ? AssetsEvents.view_wallet_assets - : AssetsEvents.view_dapp_positions - ) - } - - const tokenItems = useMemo(() => tokens.sort(sortByUsdBalance), [tokens]) - const positionSections = useMemo(() => { - const positionsByDapp = new Map() - positions.forEach((position) => { - if (positionsByDapp.has(position.appName)) { - positionsByDapp.get(position.appName)?.push(position) - } else { - positionsByDapp.set(position.appName, [position]) - } - }) - - const sections: SectionListData[] = [] - positionsByDapp.forEach((positions, appName) => { - sections.push({ - data: positions, - appName, - }) - }) - return sections - }, [positions]) - - const sections = - activeView === AssetViewType.WalletAssets ? [{ data: tokenItems }] : positionSections - - const renderSectionHeader = ({ - section, - }: { - section: SectionListData - }) => { - if (section.appName) { - return ( - - - {section.appName.toLocaleUpperCase()} - - - ) - } - return null - } - - const keyExtractor = (item: TokenBalance | Position) => { - if (assetIsPosition(item)) { - return item.address - } - return item.tokenId - } - - const renderAssetItem = ({ item }: { item: TokenBalance | Position }) => { - if (assetIsPosition(item)) { - return - } - return - } - - const segmentedControlValues = useMemo( - () => [t('assetsSegmentedControl.walletAssets'), t('assetsSegmentedControl.dappPositions')], - [t] - ) - - return ( - <> - - - 1.5 - ? { marginTop: Spacing.Small12 } - : PixelRatio.getFontScale() > 1.25 - ? { marginTop: Spacing.Smallest8 } - : null - } - testID={'NftViewerBanner'} - onPress={onPressNFTsBanner} - > - - - {shouldShowNftGallery ? t('nftGallery.title') : t('nftViewer')} - - - {t('open')} - {!shouldShowNftGallery && } - - - - - - - {displayPositions && ( - - )} - - 0 ? 1 : 0, - }} - // ensure header is above the scrollbar on ios overscroll - scrollIndicatorInsets={{ top: listHeaderHeight }} - // @ts-ignore can't get the SectionList to accept a union type :( - sections={sections} - renderItem={renderAssetItem} - renderSectionHeader={renderSectionHeader} - keyExtractor={keyExtractor} - onScroll={handleScroll} - scrollEventThrottle={16} - ListHeaderComponent={} - /> - {showClaimRewards && ( - -