diff --git a/src/components/scenes/Staking/EarnScene.tsx b/src/components/scenes/Staking/EarnScene.tsx index 6ccf322c8e5..757857ff055 100644 --- a/src/components/scenes/Staking/EarnScene.tsx +++ b/src/components/scenes/Staking/EarnScene.tsx @@ -1,3 +1,4 @@ +import { useIsFocused } from '@react-navigation/native' import { EdgeCurrencyInfo, EdgeCurrencyWallet } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator } from 'react-native' @@ -25,25 +26,49 @@ import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext' interface Props extends EdgeAppSceneProps<'earnScene'> {} +export interface EarnSceneParams {} + let USERNAME: string | undefined -let STAKE_POLICY_MAP: StakePolicyMap = {} +let DISCOVER_MAP: DiscoverStakeMap = {} +let PORTFOLIO_MAP: PortfolioStakeMap = {} -export interface EarnSceneParams {} +interface DiscoverStakeInfo { + stakePlugin: StakePlugin + stakePolicy: StakePolicy +} + +interface PortfolioStakeInfo extends DiscoverStakeInfo { + walletStakeInfos: WalletStakeInfo[] +} + +interface DiscoverStakeMap { + [stakePolicyId: string]: DiscoverStakeInfo +} + +interface PortfolioStakeMap { + [stakePolicyId: string]: PortfolioStakeInfo +} interface WalletStakeInfo { wallet: EdgeCurrencyWallet - isPositionOpen: boolean stakePosition: StakePosition } -interface DisplayStakeInfo { - stakePlugin: StakePlugin - stakePolicy: StakePolicy - walletStakeInfos: WalletStakeInfo[] -} +/** Hook to ensure the UI updates on map changes, while retaining cached data + * functionality */ +const useStakeMaps = () => { + const [, forceUpdate] = React.useReducer(x => x + 1, 0) -interface StakePolicyMap { - [pluginId: string]: DisplayStakeInfo[] + const updateMaps = React.useCallback((updates: () => void) => { + updates() + forceUpdate() + }, []) + + return { + discoverMap: DISCOVER_MAP, + portfolioMap: PORTFOLIO_MAP, + updateMaps + } } export const EarnScene = (props: Props) => { @@ -51,110 +76,209 @@ export const EarnScene = (props: Props) => { const theme = useTheme() const styles = getStyles(theme) + const { discoverMap, portfolioMap, updateMaps } = useStakeMaps() + const account = useSelector(state => state.core.account) if (USERNAME !== account.username) { // Reset local variable if user changes USERNAME = account.username - STAKE_POLICY_MAP = {} + DISCOVER_MAP = {} + PORTFOLIO_MAP = {} } const currencyConfigMap = useSelector(state => state.core.account.currencyConfig) - const currencyWallets = useWatch(account, 'currencyWallets') const wallets = Object.values(currencyWallets) const [isPortfolioSelected, setIsPortfolioSelected] = React.useState(false) - const [isLoading, setIsLoading] = React.useState(true) - - const [updateCounter, setUpdateCounter] = React.useState(0) + const [isLoadingDiscover, setIsLoadingDiscover] = React.useState(true) + const [isLoadingPortfolio, setIsLoadingPortfolio] = React.useState(true) + const [isPrevFocused, setIsPrevFocused] = React.useState() const handleSelectEarn = useHandler(() => setIsPortfolioSelected(false)) const handleSelectPortfolio = useHandler(() => setIsPortfolioSelected(true)) + const isFocused = useIsFocused() + useAsyncEffect( async () => { - for (const pluginId of Object.keys(currencyConfigMap)) { + const pluginIds = Object.keys(currencyConfigMap) + + for (const pluginId of pluginIds) { + setIsLoadingDiscover(true) + const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING - if (STAKE_POLICY_MAP[pluginId] != null || !isStakingSupported) continue + if (!isStakingSupported) continue - // Initialize stake policy const stakePlugins = await getStakePlugins(pluginId) - STAKE_POLICY_MAP[pluginId] = [] - const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId) - for (const stakePlugin of stakePlugins) { - const stakePolicies = stakePlugin.getPolicies({ pluginId }) + updateMaps(() => { + for (const stakePlugin of stakePlugins) { + for (const stakePolicy of stakePlugin.getPolicies({ pluginId }).filter(stakePolicy => !stakePolicy.deprecated)) { + DISCOVER_MAP[stakePolicy.stakePolicyId] = { + stakePlugin, + stakePolicy + } + } + } + }) + + console.debug('getStakePlugins', pluginId, 'complete') + setIsLoadingDiscover(false) + } + + setIsLoadingDiscover(false) + return () => {} + }, + [], + 'EarnScene Initialize Discover Items' + ) + + // Refresh stake positions when re-entering the scene or on initial load + useAsyncEffect( + async () => { + if (!isLoadingDiscover || (isFocused && !isPrevFocused)) { + setIsLoadingPortfolio(true) + + const controller = new AbortController() + const signal = controller.signal + + try { + const stakePolicyIds = Object.keys(discoverMap) + for (const stakePolicyId of stakePolicyIds) { + if (signal.aborted) break + + const discoverInfo = discoverMap[stakePolicyId] + const { stakePlugin, stakePolicy } = discoverInfo - for (const stakePolicy of stakePolicies) { - const walletStakePositions = [] - for (const wallet of matchingWallets) { + // Find matching wallets based on the first stake asset's pluginId + const pluginId = stakePolicy.stakeAssets[0].pluginId + const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId) + + const walletStakeInfoPromises = matchingWallets.map(async wallet => { + if (signal.aborted) return null try { - // Determine if a wallet matching this policy has an open position - const stakePosition = await stakePlugin.fetchStakePosition({ stakePolicyId: stakePolicy.stakePolicyId, wallet, account }) + const stakePosition = await stakePlugin.fetchStakePosition({ + stakePolicyId: stakePolicy.stakePolicyId, + wallet, + account + }) const allocations = getPositionAllocations(stakePosition) const { staked, earned, unstaked } = allocations const isPositionOpen = [...staked, ...earned, ...unstaked].some(positionAllocation => !zeroString(positionAllocation.nativeAmount)) - walletStakePositions.push({ wallet, isPositionOpen, stakePosition }) + if (isPositionOpen) { + return { wallet, stakePosition } + } } catch (e) { showDevError(e) } - } - - STAKE_POLICY_MAP[pluginId].push({ - stakePlugin, - stakePolicy, - walletStakeInfos: walletStakePositions + return null }) - // Trigger re-render - setUpdateCounter(prevCounter => prevCounter + 1) + + if (!signal.aborted) { + const walletStakeInfos = (await Promise.all(walletStakeInfoPromises)).filter( + (info: WalletStakeInfo | null): info is WalletStakeInfo => info != null + ) + + updateMaps(() => { + PORTFOLIO_MAP[stakePolicyId] = { + ...discoverInfo, + walletStakeInfos + } + }) + } } + } finally { + if (!signal.aborted) { + setIsLoadingPortfolio(false) + setIsPrevFocused(isFocused) + } + } + + return () => { + controller.abort() } } - setIsLoading(false) }, - [updateCounter], - 'EarnScene' + [isFocused, isLoadingDiscover, updateMaps], + 'EarnScene Refresh Portfolio Data' ) - const renderStakeItems = (displayStakeInfo: DisplayStakeInfo, currencyInfo: EdgeCurrencyInfo) => { - const { stakePlugin, stakePolicy, walletStakeInfos } = displayStakeInfo + 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(bridge => ( + + )) + + if (result?.type === 'wallet') { + walletId = result.walletId + } + } - const openStakePositions = walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen) + // User backed out of the WalletListModal + if (walletId == null) return - if (isPortfolioSelected && openStakePositions.length === 0) { - return null + navigation.push('stakeOverview', { + walletId, + stakePlugin, + stakePolicy, + // 'stakeOverview' scene will fetch the position if one exists. + // No need to know if a position exists at this point. + stakePosition: undefined + }) } + return ( + + + + ) + } + + 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 - if (walletStakeInfos.length === 1 || (isPortfolioSelected && openStakePositions.length === 1)) { - // Only one compatible wallet if on "Discover", or only one open - // position on "Portfolio." Auto-select the wallet. + 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 an existing wallet that matches this policy or create a new one - const allowedAssets = stakePolicy.stakeAssets.map(stakeAsset => ({ pluginId: stakeAsset.pluginId, tokenId: null })) - - // Filter for wallets that have an open position if "Portfolio" is - // selected - const allowedPortfolioWalletIds = isPortfolioSelected - ? walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen).map(walletStakePosition => walletStakePosition.wallet.id) - : undefined + // Select from wallets that have an open position + const allowedWalletIds = walletStakeInfos.map(walletStakePosition => walletStakePosition.wallet.id) const result = await Airship.show(bridge => ( )) @@ -178,7 +302,7 @@ export const EarnScene = (props: Props) => { return ( - + ) } @@ -187,10 +311,13 @@ export const EarnScene = (props: Props) => { - {Object.keys(STAKE_POLICY_MAP).map(pluginId => - STAKE_POLICY_MAP[pluginId].map(displayStakeInfo => renderStakeItems(displayStakeInfo, currencyConfigMap[pluginId].currencyInfo)) + {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)) && ( + )} - {isLoading && } ) }