import { NativeStackScreenProps } from '@react-navigation/native-stack' import BigNumber from 'bignumber.js' import { default as React, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { LayoutChangeEvent, StyleSheet, Text, View } from 'react-native' import { ScrollView } from 'react-native-gesture-handler' import Animated, { interpolateColor, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import AppAnalytics from 'src/analytics/AppAnalytics' import { EarnEvents } from 'src/analytics/Events' import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet' import Button, { BtnSizes, BtnTypes } from 'src/components/Button' import FilterChipsCarousel, { FilterChip, NetworkFilterChip, isNetworkChip, } from 'src/components/FilterChipsCarousel' import NetworkMultiSelectBottomSheet from 'src/components/multiSelect/NetworkMultiSelectBottomSheet' import { TIME_UNTIL_TOKEN_INFO_BECOMES_STALE } from 'src/config' import EarnTabBar from 'src/earn/EarnTabBar' import PoolList from 'src/earn/PoolList' import { EarnTabType } from 'src/earn/types' import AttentionIcon from 'src/icons/Attention' import { Screens } from 'src/navigator/Screens' import useScrollAwareHeader from 'src/navigator/ScrollAwareHeader' import { StackParamList } from 'src/navigator/types' import { refreshPositions } from 'src/positions/actions' import { earnPositionsSelector, positionsFetchedAtSelector, positionsStatusSelector, } from 'src/positions/selectors' import { useDispatch, useSelector } from 'src/redux/hooks' import Colors from 'src/styles/colors' import { typeScale } from 'src/styles/fonts' import { Shadow, Spacing, getShadowStyle } from 'src/styles/styles' import { tokensByIdSelector } from 'src/tokens/selectors' import { TokenBalance } from 'src/tokens/slice' import { NetworkId } from 'src/transactions/types' const HEADER_OPACITY_ANIMATION_START_OFFSET = 44 const HEADER_OPACITY_ANIMATION_DISTANCE = 20 type Props = NativeStackScreenProps function useFilterChips(): FilterChip[] { const { t } = useTranslation() const pools = useSelector(earnPositionsSelector) const supportedNetworkIds = [...new Set(pools.map((pool) => pool.networkId))] const networkChipConfig: NetworkFilterChip = { id: 'network-ids', name: t('tokenBottomSheet.filters.selectNetwork'), filterFn: (token: TokenBalance, selected?: NetworkId[]) => { return !!selected && selected.includes(token.networkId) }, isSelected: false, allNetworkIds: supportedNetworkIds, selectedNetworkIds: supportedNetworkIds, } return [networkChipConfig] } export default function EarnHome({ navigation, route }: Props) { const { t } = useTranslation() const dispatch = useDispatch() const filterChipsCarouselRef = useRef(null) const pools = useSelector(earnPositionsSelector) const activeTab = route.params?.activeEarnTab ?? EarnTabType.AllPools const insets = useSafeAreaInsets() const insetsStyle = { paddingBottom: Math.max(insets.bottom, Spacing.Regular16), } const supportedNetworkIds = [...new Set(pools.map((pool) => pool.networkId))] const allTokens = useSelector((state) => tokensByIdSelector(state, supportedNetworkIds)) // Scroll Aware Header const scrollPosition = useSharedValue(0) const [listHeaderHeight, setListHeaderHeight] = useState(0) const [nonStickyHeaderHeight, setNonStickyHeaderHeight] = useState(0) const animatedListHeaderStyles = useAnimatedStyle(() => { if (nonStickyHeaderHeight === 0) { 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', Colors.softShadow] ), } }, [scrollPosition.value, nonStickyHeaderHeight]) const networkChipRef = useRef(null) const tokenBottomSheetRef = useRef(null) const learnMoreBottomSheetRef = useRef(null) // The NetworkMultiSelectBottomSheet and TokenBottomSheet must be rendered at this level in order to be in // front of the bottom tabs navigator when they render. So, we need to manage the state of the filters here and pass them down // This is not ideal, and we should be wary of how this affects the performance of the home tabs since it renders // on all of them, not just the Earn tab. const chips = useFilterChips() const [filters, setFilters] = useState(chips) const activeFilters = useMemo(() => filters.filter((filter) => filter.isSelected), [filters]) const networkChip = useMemo( () => filters.find((chip): chip is NetworkFilterChip => isNetworkChip(chip)), [filters] ) const tokens = [...new Set(pools.flatMap((pool) => pool.tokens))] const tokensInfo = useMemo(() => { return tokens .map((token) => allTokens[token.tokenId]) .filter((token): token is TokenBalance => !!token) }, [allTokens]) const tokenList = useMemo(() => { return tokensInfo.filter((token) => { // Exclude the token if it does not match the active filters if ( !activeFilters.every((filter) => { if (isNetworkChip(filter)) { return filter.filterFn(token, filter.selectedNetworkIds) } return filter.filterFn(token) }) ) { return false } return true }) }, [tokensInfo, activeFilters]) const handleToggleFilterChip = (chip: FilterChip) => { if (isNetworkChip(chip)) { return networkChipRef.current?.snapToIndex(0) } return tokenBottomSheetRef.current?.snapToIndex(0) } // These function params mimic the params of the setSelectedNetworkIds function in // const [selectedNetworkIds, setSelectedNetworkIds] = useState([]) // This custom function is used to keep the same shared state between the network filter and the other filters // which made the rest of the code more readable and maintainable const setSelectedNetworkIds = (arg: NetworkId[] | ((networkIds: NetworkId[]) => NetworkId[])) => { setFilters((prev) => { return prev.map((chip) => { if (isNetworkChip(chip)) { const selectedNetworkIds = typeof arg === 'function' ? arg(chip.selectedNetworkIds) : arg return { ...chip, selectedNetworkIds, isSelected: selectedNetworkIds.length !== chip.allNetworkIds.length, } } return { ...chip, isSelected: false, } }) }) } const handleMeasureListHeadereHeight = (event: LayoutChangeEvent) => { setListHeaderHeight(event.nativeEvent.layout.height) } const handleScroll = useAnimatedScrollHandler((event) => { scrollPosition.value = event.contentOffset.y }) const handleMeasureNonStickyHeaderHeight = (event: LayoutChangeEvent) => { setNonStickyHeaderHeight(event.nativeEvent.layout.height) } useScrollAwareHeader({ navigation, title: t('earnFlow.home.title'), scrollPosition, startFadeInPosition: nonStickyHeaderHeight - HEADER_OPACITY_ANIMATION_START_OFFSET, animationDistance: HEADER_OPACITY_ANIMATION_DISTANCE, }) const handleChangeActiveView = (selectedTab: EarnTabType) => { navigation.setParams({ activeEarnTab: selectedTab }) } const displayPools = useMemo(() => { return activeTab === EarnTabType.AllPools ? pools : pools.filter( (pool) => new BigNumber(pool.balance).gt(0) && !!allTokens[pool.dataProps.depositTokenId] ) }, [pools, allTokens, activeTab]) const onPressLearnMore = () => { AppAnalytics.track(EarnEvents.earn_home_learn_more_press) learnMoreBottomSheetRef.current?.snapToIndex(0) } const onPressTryAgain = () => { AppAnalytics.track(EarnEvents.earn_home_error_try_again) dispatch(refreshPositions()) } const positionsStatus = useSelector(positionsStatusSelector) const positionsFetchedAt = useSelector(positionsFetchedAtSelector) const errorLoadingPools = positionsStatus === 'error' && (pools.length === 0 || Date.now() - (positionsFetchedAt ?? 0) > TIME_UNTIL_TOKEN_INFO_BECOMES_STALE) const zeroPoolsinMyPoolsTab = !errorLoadingPools && displayPools.length === 0 && activeTab === EarnTabType.MyPools return ( <> {t('earnFlow.home.title')} {errorLoadingPools && ( {t('earnFlow.home.errorTitle')} {t('earnFlow.home.errorDescription')} )} {zeroPoolsinMyPoolsTab && ( {t('earnFlow.home.noPoolsTitle')} {t('earnFlow.home.noPoolsDescription')} )} {!errorLoadingPools && !zeroPoolsinMyPoolsTab && ( pool.tokens.some((token) => tokenList.map((token) => token.tokenId).includes(token.tokenId) ) )} onPressLearnMore={onPressLearnMore} /> )} {errorLoadingPools && (