diff --git a/src/components/scenes/Staking/EarnScene.tsx b/src/components/scenes/Staking/EarnScene.tsx index 85eefe6dbd0..757857ff055 100644 --- a/src/components/scenes/Staking/EarnScene.tsx +++ b/src/components/scenes/Staking/EarnScene.tsx @@ -26,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) + + const updateMaps = React.useCallback((updates: () => void) => { + updates() + forceUpdate() + }, []) -interface StakePolicyMap { - [pluginId: string]: DisplayStakeInfo[] + return { + discoverMap: DISCOVER_MAP, + portfolioMap: PORTFOLIO_MAP, + updateMaps + } } export const EarnScene = (props: Props) => { @@ -52,174 +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 [isLoadingDiscover, setIsLoadingDiscover] = React.useState(true) const [isLoadingPortfolio, setIsLoadingPortfolio] = React.useState(true) - const [isPrevFocused, setIsPrevFocused] = React.useState(true) + const [isPrevFocused, setIsPrevFocused] = React.useState() const handleSelectEarn = useHandler(() => setIsPortfolioSelected(false)) const handleSelectPortfolio = useHandler(() => setIsPortfolioSelected(true)) const isFocused = useIsFocused() - const refreshStakePositions = async (pluginId: string): Promise => { - const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING - if (!isStakingSupported || STAKE_POLICY_MAP[pluginId] == null) return [] + useAsyncEffect( + async () => { + const pluginIds = Object.keys(currencyConfigMap) - const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId) - const updatedDisplayStakeInfos = [] - for (const displayStakeInfo of STAKE_POLICY_MAP[pluginId]) { - const { stakePlugin, stakePolicy } = displayStakeInfo + for (const pluginId of pluginIds) { + setIsLoadingDiscover(true) - const walletStakePositions = [] - for (const wallet of matchingWallets) { - try { - // Determine if a wallet matching this policy has an open position - 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 }) - } catch (e) { - showDevError(e) - } - } + const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING + if (!isStakingSupported) continue - // Create a new displayStakeInfo object - updatedDisplayStakeInfos.push({ - stakePlugin, - stakePolicy, - walletStakeInfos: walletStakePositions - }) - } + const stakePlugins = await getStakePlugins(pluginId) - return updatedDisplayStakeInfos - } + 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 () => { - for (const pluginId of Object.keys(currencyConfigMap)) { - const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING - if (STAKE_POLICY_MAP[pluginId] != null || !isStakingSupported) continue + if (!isLoadingDiscover || (isFocused && !isPrevFocused)) { + setIsLoadingPortfolio(true) - // Initialize stake policy - const stakePlugins = await getStakePlugins(pluginId) - STAKE_POLICY_MAP[pluginId] = [] + const controller = new AbortController() + const signal = controller.signal - const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId) - for (const stakePlugin of stakePlugins) { - const stakePolicies = stakePlugin.getPolicies({ pluginId }).filter(stakePolicy => !stakePolicy.deprecated) + try { + const stakePolicyIds = Object.keys(discoverMap) + for (const stakePolicyId of stakePolicyIds) { + if (signal.aborted) break - for (const stakePolicy of stakePolicies) { - const walletStakePositions = [] - for (const wallet of matchingWallets) { + const discoverInfo = discoverMap[stakePolicyId] + const { stakePlugin, stakePolicy } = discoverInfo + + // 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 }) + + 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() } } - setIsLoadingPortfolio(false) - setIsLoadingDiscover(false) }, - [], - 'EarnScene Initialize STAKE_POLICY_MAP' + [isFocused, isLoadingDiscover, updateMaps], + 'EarnScene Refresh Portfolio Data' ) - // Refresh stake positions when re-entering the scene - useAsyncEffect( - async () => { - if (isFocused && !isPrevFocused) { - setIsLoadingPortfolio(true) + const renderDiscoverItem = (discoverStakeInfo: DiscoverStakeInfo, currencyInfo: EdgeCurrencyInfo) => { + const { stakePlugin, stakePolicy } = discoverStakeInfo - for (const pluginId of Object.keys(currencyConfigMap)) { - const newDisplayStakeInfos = await refreshStakePositions(pluginId) - STAKE_POLICY_MAP[pluginId] = newDisplayStakeInfos - } + const handlePress = async () => { + let walletId: string | undefined - setIsLoadingPortfolio(false) - } + 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 })) - setIsPrevFocused(isFocused) - }, - [isFocused], - 'EarnScene Refresh Stake Positions' - ) + const result = await Airship.show(bridge => ( + + )) - const renderStakeItems = (displayStakeInfo: DisplayStakeInfo, currencyInfo: EdgeCurrencyInfo) => { - const { stakePlugin, stakePolicy, walletStakeInfos } = displayStakeInfo + 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 const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === currencyInfo.pluginId) if (matchingWallets.length === 1) { - // Only one compatible wallet if on "Discover", or only one open - // position on "Portfolio." Auto-select the wallet. - const { wallet, stakePosition: existingStakePosition } = walletStakeInfos[0] ?? { - // It's possible that the wallet was created on this scene previously, - // and when re-navigating back here, the STAKE_POLICY_MAP has not - // finished updating. The `StakeOverviewScene` will handle refreshing - // the position, if any. - wallet: matchingWallets[0], - stakePosition: undefined - } - + // 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 => ( )) @@ -243,7 +302,7 @@ export const EarnScene = (props: Props) => { return ( - + ) } @@ -252,9 +311,10 @@ 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)) && ( )}