import { NativeStackScreenProps } from '@react-navigation/native-stack'
import BigNumber from 'bignumber.js'
import React, { useMemo, useRef, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { LayoutChangeEvent, Platform, StyleSheet, Text, View } from 'react-native'
import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { EarnEvents } from 'src/analytics/Events'
import { EarnCommonProperties } from 'src/analytics/Properties'
import { openUrl } from 'src/app/actions'
import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet'
import Button, { BtnSizes, BtnTypes } from 'src/components/Button'
import { IconSize } from 'src/components/TokenIcon'
import Touchable from 'src/components/Touchable'
import { useDepositEntrypointInfo } from 'src/earn/hooks'
import BeforeDepositBottomSheet from 'src/earn/poolInfoScreen/BeforeDepositBottomSheet'
import {
AgeCard,
DailyYieldRateCard,
DepositAndEarningsCard,
TvlCard,
YieldCard,
} from 'src/earn/poolInfoScreen/Cards'
import { SafetyCard } from 'src/earn/poolInfoScreen/SafetyCard'
import TokenIcons from 'src/earn/poolInfoScreen/TokenIcons'
import WithdrawBottomSheet from 'src/earn/poolInfoScreen/WithdrawBottomSheet'
import OpenLinkIcon from 'src/icons/OpenLinkIcon'
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 { positionsWithBalanceSelector } from 'src/positions/selectors'
import { EarnPosition } from 'src/positions/types'
import { useDispatch, useSelector } from 'src/redux/hooks'
import { NETWORK_NAMES } from 'src/shared/conts'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import Colors from 'src/styles/colors'
import { typeScale } from 'src/styles/fonts'
import { Spacing } from 'src/styles/styles'
import variables from 'src/styles/variables'
import { tokensByIdSelector } from 'src/tokens/selectors'
import { TokenBalance } from 'src/tokens/slice'
import { navigateToURI } from 'src/utils/linking'
function HeaderTitleSection({
earnPosition,
tokensInfo,
}: {
earnPosition: EarnPosition
tokensInfo: TokenBalance[]
}) {
return (
{/* View wrapper is needed to prevent token icons from overlapping title text */}
{earnPosition.displayProps.title}
)
}
function TitleSection({
title,
tokensInfo,
providerName,
networkName,
onLayout,
}: {
title: string
tokensInfo: TokenBalance[]
providerName: string
networkName: string
onLayout?: (event: LayoutChangeEvent) => void
}) {
return (
{/* View wrapper is needed to prevent TokenIcons from taking up the whole line */}
{title}
)
}
function LearnMoreTouchable({
url,
providerName,
commonAnalyticsProps,
}: {
url: string
providerName: string
commonAnalyticsProps: EarnCommonProperties
}) {
const { t } = useTranslation()
return (
{
AppAnalytics.track(EarnEvents.earn_pool_info_view_pool, commonAnalyticsProps)
navigateToURI(url)
}}
>
{t('earnFlow.poolInfoScreen.learnMoreOnProvider', { providerName })}
)
}
function ActionButtons({
earnPosition,
onPressDeposit,
onPressWithdraw,
}: {
earnPosition: EarnPosition
onPressDeposit: () => void
onPressWithdraw: () => void
}) {
const { bottom } = useSafeAreaInsets()
const insetsStyle = {
paddingBottom: Math.max(bottom, Spacing.Regular16),
}
const { t } = useTranslation()
const { availableShortcutIds } = earnPosition
const deposit = availableShortcutIds.includes('deposit')
const withdraw =
availableShortcutIds.includes('withdraw') && new BigNumber(earnPosition.balance).gt(0)
return (
{withdraw && (
)}
{deposit && (
)}
)
}
type Props = NativeStackScreenProps
export default function EarnPoolInfoScreen({ route, navigation }: Props) {
const { pool } = route.params
const { networkId, tokens, displayProps, appName, dataProps, appId, positionId, balance } = pool
const allTokens = useSelector((state) => tokensByIdSelector(state, [networkId]))
const tokensInfo = useMemo(() => {
return tokens
.map((token) => allTokens[token.tokenId])
.filter((token): token is TokenBalance => !!token)
}, [tokens, allTokens])
const depositToken = allTokens[dataProps.depositTokenId]
if (!depositToken) {
// This should never happen
throw new Error(`Token ${dataProps.depositTokenId} not found`)
}
const commonAnalyticsProps: EarnCommonProperties = {
providerId: appId,
poolId: positionId,
networkId,
depositTokenId: dataProps.depositTokenId,
}
const {
hasDepositToken,
hasTokensOnSameNetwork,
hasTokensOnOtherNetworks,
canCashIn,
exchanges,
} = useDepositEntrypointInfo({ allTokens, pool })
const allPositionsWithBalance = useSelector(positionsWithBalanceSelector)
const hasRewards = useMemo(() => {
const rewardsPositions = allPositionsWithBalance.filter((position) =>
pool.dataProps.rewardsPositionIds?.includes(position.positionId)
)
return rewardsPositions.length > 0
}, [allPositionsWithBalance])
const onPressWithdraw = () => {
AppAnalytics.track(EarnEvents.earn_pool_info_tap_withdraw, {
poolId: positionId,
providerId: appId,
poolAmount: balance,
networkId,
depositTokenId: dataProps.depositTokenId,
})
const partialWithdrawalsEnabled = getFeatureGate(
StatsigFeatureGates.ALLOW_EARN_PARTIAL_WITHDRAWAL
)
if (partialWithdrawalsEnabled) {
withdrawBottomSheetRef.current?.snapToIndex(0)
} else {
navigate(Screens.EarnConfirmationScreen, { pool, mode: 'exit', useMax: true })
}
}
const onPressDeposit = () => {
AppAnalytics.track(EarnEvents.earn_pool_info_tap_deposit, {
...commonAnalyticsProps,
hasDepositToken,
hasTokensOnSameNetwork,
hasTokensOnOtherNetworks,
})
beforeDepositBottomSheetRef.current?.snapToIndex(0)
}
const beforeDepositBottomSheetRef = useRef(null)
const depositInfoBottomSheetRef = useRef(null)
const tvlInfoBottomSheetRef = useRef(null)
const ageInfoBottomSheetRef = useRef(null)
const yieldRateInfoBottomSheetRef = useRef(null)
const withdrawBottomSheetRef = useRef(null)
const dailyYieldRateInfoBottomSheetRef = useRef(null)
const safetyScoreInfoBottomSheetRef = useRef(null)
// Scroll Aware Header
const scrollPosition = useSharedValue(0)
const [titleHeight, setTitleHeight] = useState(0)
const handleMeasureTitleHeight = (event: LayoutChangeEvent) => {
setTitleHeight(event.nativeEvent.layout.height)
}
const handleScroll = useAnimatedScrollHandler((event) => {
scrollPosition.value = event.contentOffset.y
})
useScrollAwareHeader({
navigation,
title: ,
scrollPosition,
// Numbers selected through trial and error
startFadeInPosition: titleHeight * 0.1,
animationDistance: titleHeight * 0.66,
})
return (
{new BigNumber(balance).gt(0) && (
{
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'deposit',
...commonAnalyticsProps,
})
depositInfoBottomSheetRef.current?.snapToIndex(0)
}}
/>
)}
{
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'yieldRate',
...commonAnalyticsProps,
})
yieldRateInfoBottomSheetRef.current?.snapToIndex(0)
}}
tokensInfo={tokensInfo}
earnPosition={pool}
/>
{!!dataProps.dailyYieldRatePercentage && dataProps.dailyYieldRatePercentage > 0 && (
{
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'dailyYieldRate',
...commonAnalyticsProps,
})
dailyYieldRateInfoBottomSheetRef.current?.snapToIndex(0)
}}
/>
)}
{!!dataProps.safety && (
{
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'safetyScore',
...commonAnalyticsProps,
})
safetyScoreInfoBottomSheetRef.current?.snapToIndex(0)
}}
/>
)}
{
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'tvl',
...commonAnalyticsProps,
})
tvlInfoBottomSheetRef.current?.snapToIndex(0)
}}
/>
{dataProps.contractCreatedAt ? (
{
AppAnalytics.track(EarnEvents.earn_pool_info_tap_info_icon, {
type: 'age',
...commonAnalyticsProps,
})
ageInfoBottomSheetRef.current?.snapToIndex(0)
}}
/>
) : null}
{dataProps.manageUrl && appName ? (
) : null}
)
}
function InfoBottomSheet({
infoBottomSheetRef,
titleKey,
descriptionKey,
descriptionUrl,
providerName,
testId,
linkUrl,
linkKey,
}: {
infoBottomSheetRef: React.RefObject
titleKey: string
descriptionKey: string
descriptionUrl?: string
providerName: string
testId: string
linkUrl?: string
linkKey?: string
}) {
const { t } = useTranslation()
const dispatch = useDispatch()
const onPressDismiss = () => {
infoBottomSheetRef.current?.close()
}
const onPressUrl = () => {
descriptionUrl && dispatch(openUrl(descriptionUrl, true))
}
return (
{descriptionUrl ? (
) : (
{t(descriptionKey, { providerName })}
)}
{!!linkUrl && !!linkKey && (
{
navigateToURI(linkUrl)
}}
>
)}
)
}
const styles = StyleSheet.create({
headerTitle: {
flexDirection: 'row',
gap: Spacing.Smallest8,
},
headerTitleText: {
...typeScale.labelSemiBoldMedium,
},
flex: {
flex: 1,
},
scrollContainer: {
paddingHorizontal: Spacing.Thick24,
...(Platform.OS === 'android' && {
minHeight: variables.height,
}),
},
title: {
...typeScale.titleMedium,
},
subtitleLabel: {
...typeScale.bodyMedium,
color: Colors.contentSecondary,
},
subtitleInfo: {
...typeScale.labelMedium,
},
titleContainer: {
gap: Spacing.Smallest8,
},
titleTokenContainer: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start',
gap: Spacing.Smallest8,
},
subtitleContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-start',
gap: Spacing.Thick24,
rowGap: 0, // Set to Zero to prevent gap between rows when flexWrap is set to wrap
flexWrap: 'wrap',
},
contentContainer: {
gap: Spacing.Regular16,
},
learnMoreContainer: {
flexShrink: 1,
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
},
learnMoreView: {
flex: 1,
gap: Spacing.Tiny4,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: Spacing.Thick24,
},
learnMoreText: {
...typeScale.labelSemiBoldSmall,
},
buttonContainer: {
flexShrink: 1,
flexDirection: 'row',
padding: Spacing.Regular16,
gap: Spacing.Smallest8,
},
infoBottomSheetTitle: {
...typeScale.titleSmall,
},
infoBottomSheetText: {
...typeScale.bodySmall,
marginBottom: Spacing.Thick24,
},
linkText: {
textDecorationLine: 'underline',
},
})