Skip to content

Commit

Permalink
Add search to EarnScene
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-edge committed Jan 25, 2025
1 parent 76dbc71 commit 28f9306
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 104 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased (develop)

- added: Search bar to `EarnScene`
- added: Add Unizen DEX

## 4.21.0 (2025-01-22)
Expand Down
325 changes: 221 additions & 104 deletions src/components/scenes/Staking/EarnScene.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useIsFocused } from '@react-navigation/native'
import { EdgeCurrencyInfo, EdgeCurrencyWallet } from 'edge-core-js'
import * as React from 'react'
import { ActivityIndicator } from 'react-native'
import { ActivityIndicator, ListRenderItemInfo, View } from 'react-native'
import Animated from 'react-native-reanimated'

import { SCROLL_INDICATOR_INSET_FIX } from '../../../constants/constantSettings'
import { SPECIAL_CURRENCY_INFO } from '../../../constants/WalletAndCurrencyConstants'
import { ENV } from '../../../env'
import { useAsyncEffect } from '../../../hooks/useAsyncEffect'
Expand All @@ -11,6 +13,8 @@ import { useWatch } from '../../../hooks/useWatch'
import { lstrings } from '../../../locales/strings'
import { getStakePlugins } from '../../../plugins/stake-plugins/stakePlugins'
import { StakePlugin, StakePolicy, StakePosition } from '../../../plugins/stake-plugins/types'
import { FooterRender } from '../../../state/SceneFooterState'
import { useSceneScrollHandler } from '../../../state/SceneScrollState'
import { useDispatch, useSelector } from '../../../types/reactRedux'
import { EdgeAppSceneProps, NavigationBase } from '../../../types/routerTypes'
import { getPositionAllocations } from '../../../util/stakeUtils'
Expand All @@ -23,6 +27,7 @@ import { SectionHeader } from '../../common/SectionHeader'
import { WalletListModal, WalletListResult } from '../../modals/WalletListModal'
import { Airship, showDevError } from '../../services/AirshipInstance'
import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext'
import { SearchFooter } from '../../themed/SearchFooter'

interface Props extends EdgeAppSceneProps<'earnScene'> {}

Expand Down Expand Up @@ -96,9 +101,30 @@ export const EarnScene = (props: Props) => {
const [isLoadingPortfolio, setIsLoadingPortfolio] = React.useState(true)
const [isPrevFocused, setIsPrevFocused] = React.useState<boolean>()

const [searchText, setSearchText] = React.useState<string>('')
const [isSearching, setIsSearching] = React.useState<boolean>(false)
const [footerHeight, setFooterHeight] = React.useState<number | undefined>()

const handleSelectEarn = useHandler(() => setIsPortfolioSelected(false))
const handleSelectPortfolio = useHandler(() => setIsPortfolioSelected(true))

const handleStartSearching = useHandler(() => {
setIsSearching(true)
})

const handleDoneSearching = useHandler(() => {
setSearchText('')
setIsSearching(false)
})

const handleChangeText = useHandler((value: string) => {
setSearchText(value)
})

const handleFooterLayoutHeight = useHandler((height: number) => {
setFooterHeight(height)
})

const isFocused = useIsFocused()

useAsyncEffect(
Expand All @@ -124,7 +150,6 @@ export const EarnScene = (props: Props) => {
}
})

console.debug('getStakePlugins', pluginId, 'complete')
setIsLoadingDiscover(false)
}

Expand Down Expand Up @@ -206,121 +231,212 @@ export const EarnScene = (props: Props) => {
'EarnScene Refresh Portfolio Data'
)

const renderDiscoverItem = (discoverStakeInfo: DiscoverStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy } = discoverStakeInfo

const handlePress = async () => {
let walletId: string | undefined

const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId)
if (matchingWallets.length === 1) {
// Only one compatible wallet, auto-select it
const wallet = matchingWallets[0]
walletId = wallet.id
} else {
// Select an existing wallet that matches this policy or create a new one
const allowedAssets = stakePolicy.stakeAssets.map(stakeAsset => ({
pluginId: stakeAsset.pluginId,
tokenId: null
}))

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedAssets={allowedAssets}
headerTitle={lstrings.select_wallet}
showCreateWallet
navigation={navigation as NavigationBase}
/>
))

if (result?.type === 'wallet') {
walletId = result.walletId
const filterStakeInfo = (info: DiscoverStakeInfo | PortfolioStakeInfo): boolean => {
if (!searchText) return true
const searchLower = searchText.toLowerCase()

// Match against policy provider name
if (info.stakePolicy.stakeProviderInfo.displayName.toLowerCase().includes(searchLower)) return true

// Match against stake assets
for (const stakeAsset of info.stakePolicy.stakeAssets) {
const currencyInfo = currencyConfigMap[stakeAsset.pluginId].currencyInfo
if (currencyInfo.displayName.toLowerCase().includes(searchLower)) return true
if (currencyInfo.currencyCode.toLowerCase().includes(searchLower)) return true
// Also check asset's own display name if available
if (stakeAsset.displayName?.toLowerCase().includes(searchLower)) return true
if (stakeAsset.currencyCode?.toLowerCase().includes(searchLower)) return true
}

// Match against reward assets
for (const rewardAsset of info.stakePolicy.rewardAssets) {
const currencyInfo = currencyConfigMap[rewardAsset.pluginId].currencyInfo
if (currencyInfo.displayName.toLowerCase().includes(searchLower)) return true
if (currencyInfo.currencyCode.toLowerCase().includes(searchLower)) return true
// Also check asset's own display name if available
if (rewardAsset.displayName?.toLowerCase().includes(searchLower)) return true
if (rewardAsset.currencyCode?.toLowerCase().includes(searchLower)) return true
}

return false
}

const renderDiscoverItem = React.useCallback(
(discoverStakeInfo: DiscoverStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy } = discoverStakeInfo

const handlePress = async () => {
let walletId: string | undefined

const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId)
if (matchingWallets.length === 1) {
// Only one compatible wallet, auto-select it
const wallet = matchingWallets[0]
walletId = wallet.id
} else {
// Select an existing wallet that matches this policy or create a new one
const allowedAssets = stakePolicy.stakeAssets.map(stakeAsset => ({
pluginId: stakeAsset.pluginId,
tokenId: null
}))

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedAssets={allowedAssets}
headerTitle={lstrings.select_wallet}
showCreateWallet
navigation={navigation as NavigationBase}
/>
))

if (result?.type === 'wallet') {
walletId = result.walletId
}
}
}

// User backed out of the WalletListModal
if (walletId == null) return
// User backed out of the WalletListModal
if (walletId == null) return

dispatch({ type: 'STAKING/ADD_POLICY', walletId, stakePolicy })
dispatch({ type: 'STAKING/ADD_POLICY', walletId, stakePolicy })

navigation.push('stakeOverview', {
walletId,
stakePlugin,
stakePolicyId: stakePolicy.stakePolicyId
})
}
navigation.push('stakeOverview', {
walletId,
stakePlugin,
stakePolicyId: stakePolicy.stakePolicyId
})
}

return (
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition={false} onPress={handlePress} />
</EdgeAnim>
)
}
return (
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition={false} onPress={handlePress} />
</EdgeAnim>
)
},
[dispatch, navigation, wallets]
)

const renderPortfolioItem = (portfolioStakeInfo: PortfolioStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy, walletStakeInfos } = portfolioStakeInfo
if (walletStakeInfos.length === 0) return null

const handlePress = async () => {
let walletId: string | undefined
let stakePosition: StakePosition | undefined

const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId)
if (matchingWallets.length === 1) {
// Only one wallet with an open position, auto-select it
const { wallet, stakePosition: existingStakePosition } = walletStakeInfos[0]
walletId = wallet.id
stakePosition = existingStakePosition
} else {
// Select from wallets that have an open position
const allowedWalletIds = walletStakeInfos.map(walletStakePosition => walletStakePosition.wallet.id)

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedWalletIds={allowedWalletIds}
headerTitle={lstrings.select_wallet}
showCreateWallet={false}
navigation={navigation as NavigationBase}
/>
))

if (result?.type === 'wallet') {
walletId = result.walletId
stakePosition = walletStakeInfos.find(walletStakeInfo => walletStakeInfo.wallet.id === result.walletId)?.stakePosition
const renderPortfolioItem = React.useCallback(
(portfolioStakeInfo: PortfolioStakeInfo, currencyInfo: EdgeCurrencyInfo) => {
const { stakePlugin, stakePolicy, walletStakeInfos } = portfolioStakeInfo
if (walletStakeInfos.length === 0) return null

const handlePress = async () => {
let walletId: string | undefined
let stakePosition: StakePosition | undefined

const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId)
if (matchingWallets.length === 1) {
// Only one wallet with an open position, auto-select it
const { wallet, stakePosition: existingStakePosition } = walletStakeInfos[0]
walletId = wallet.id
stakePosition = existingStakePosition
} else {
// Select from wallets that have an open position
const allowedWalletIds = walletStakeInfos.map(walletStakePosition => walletStakePosition.wallet.id)

const result = await Airship.show<WalletListResult>(bridge => (
<WalletListModal
bridge={bridge}
allowedWalletIds={allowedWalletIds}
headerTitle={lstrings.select_wallet}
showCreateWallet={false}
navigation={navigation as NavigationBase}
/>
))

if (result?.type === 'wallet') {
walletId = result.walletId
stakePosition = walletStakeInfos.find(walletStakeInfo => walletStakeInfo.wallet.id === result.walletId)?.stakePosition
}
}

// User backed out of the WalletListModal
if (walletId == null || stakePosition == null) return

dispatch({ type: 'STAKING/UPDATE', walletId, stakePolicy, stakePosition })

navigation.push('stakeOverview', {
walletId,
stakePlugin,
stakePolicyId: stakePolicy.stakePolicyId
})
}

// User backed out of the WalletListModal
if (walletId == null || stakePosition == null) return
return (
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition onPress={handlePress} />
</EdgeAnim>
)
},
[dispatch, navigation, wallets]
)

dispatch({ type: 'STAKING/UPDATE', walletId, stakePolicy, stakePosition })
const renderFooter: FooterRender = React.useCallback(
sceneWrapperInfo => {
return (
<SearchFooter
name="EarnScene-SearchFooter"
placeholder={lstrings.earn_search}
isSearching={isSearching}
searchText={searchText}
sceneWrapperInfo={sceneWrapperInfo}
onStartSearching={handleStartSearching}
onDoneSearching={handleDoneSearching}
onChangeText={handleChangeText}
onLayoutHeight={handleFooterLayoutHeight}
/>
)
},
[handleChangeText, handleDoneSearching, handleFooterLayoutHeight, handleStartSearching, isSearching, searchText]
)

navigation.push('stakeOverview', {
walletId,
stakePlugin,
stakePolicyId: stakePolicy.stakePolicyId
})
}
const filteredPortfolioItems = React.useMemo(() => {
const items: Array<PortfolioStakeInfo | null> = Object.values(portfolioMap).filter(filterStakeInfo)
if (isLoadingPortfolio) items.push(null)
return items
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [portfolioMap, filterStakeInfo, searchText, isLoadingPortfolio])

const filteredDiscoverItems = React.useMemo(() => {
const items: Array<DiscoverStakeInfo | null> = Object.values(discoverMap).filter(filterStakeInfo)
if (isLoadingDiscover) items.push(null)
return items
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [discoverMap, filterStakeInfo, searchText, isLoadingDiscover])

const renderItem = React.useCallback(
(info: ListRenderItemInfo<DiscoverStakeInfo | PortfolioStakeInfo | null>) => {
if (info.item === null) {
return <ActivityIndicator style={styles.loader} size="large" color={theme.primaryText} />
}
const currencyInfo = currencyConfigMap[info.item.stakePolicy.stakeAssets[0].pluginId].currencyInfo
return isPortfolioSelected
? renderPortfolioItem(info.item as PortfolioStakeInfo, currencyInfo)
: renderDiscoverItem(info.item as DiscoverStakeInfo, currencyInfo)
},
[currencyConfigMap, isPortfolioSelected, renderPortfolioItem, renderDiscoverItem, styles.loader, theme.primaryText]
)

return (
<EdgeAnim key={stakePolicy.stakePolicyId} enter={fadeInUp20}>
<EarnOptionCard currencyInfo={currencyInfo} stakePolicy={stakePolicy} isOpenPosition onPress={handlePress} />
</EdgeAnim>
)
}
const handleScroll = useSceneScrollHandler()

return (
<SceneWrapper scroll padding={theme.rem(0.5)}>
<EdgeSwitch labelA={lstrings.staking_discover} labelB={lstrings.staking_portfolio} onSelectA={handleSelectEarn} onSelectB={handleSelectPortfolio} />
<SectionHeader leftTitle={lstrings.staking_earning_pools} />
{isPortfolioSelected &&
Object.values(portfolioMap).map(info => renderPortfolioItem(info, currencyConfigMap[info.stakePolicy.stakeAssets[0].pluginId].currencyInfo))}
{!isPortfolioSelected &&
Object.values(discoverMap).map(info => renderDiscoverItem(info, currencyConfigMap[info.stakePolicy.stakeAssets[0].pluginId].currencyInfo))}
{((isLoadingDiscover && !isPortfolioSelected) || (isLoadingPortfolio && isPortfolioSelected)) && (
<ActivityIndicator style={styles.loader} size="large" color={theme.primaryText} />
<SceneWrapper avoidKeyboard renderFooter={renderFooter} footerHeight={footerHeight}>
{({ insetStyle, undoInsetStyle }) => (
<>
<EdgeSwitch labelA={lstrings.staking_discover} labelB={lstrings.staking_portfolio} onSelectA={handleSelectEarn} onSelectB={handleSelectPortfolio} />
<SectionHeader leftTitle={lstrings.staking_earning_pools} />
<View style={{ ...undoInsetStyle, marginTop: 0 }}>
<Animated.FlatList
contentContainerStyle={{ ...insetStyle, ...styles.list }}
data={isPortfolioSelected ? filteredPortfolioItems : filteredDiscoverItems}
keyExtractor={(item, index) => item?.stakePolicy.stakePolicyId ?? `loader-${index}`}
onScroll={handleScroll}
renderItem={renderItem}
scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX}
/>
</View>
</>
)}
</SceneWrapper>
)
Expand All @@ -332,6 +448,7 @@ const getStyles = cacheStyles((theme: Theme) => ({
marginBottom: theme.rem(3)
},
list: {
marginBottom: theme.rem(1)
paddingTop: 0,
marginHorizontal: theme.rem(0.5)
}
}))
Loading

0 comments on commit 28f9306

Please sign in to comment.