From a895ae90f653d407720ea8a3c1c90938a8922dbd Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 29 Oct 2024 10:52:03 -0700 Subject: [PATCH 1/4] Revert `StakingReturnsCard,` Create `EarnOptionCard` --- src/components/cards/EarnOptionCard.tsx | 95 ++++++++ src/components/cards/StakingReturnsCard.tsx | 215 ++++++++++++------ src/components/scenes/Staking/EarnScene.tsx | 4 +- .../scenes/Staking/StakeOverviewScene.tsx | 22 +- 4 files changed, 254 insertions(+), 82 deletions(-) create mode 100644 src/components/cards/EarnOptionCard.tsx diff --git a/src/components/cards/EarnOptionCard.tsx b/src/components/cards/EarnOptionCard.tsx new file mode 100644 index 00000000000..40c9624cfdf --- /dev/null +++ b/src/components/cards/EarnOptionCard.tsx @@ -0,0 +1,95 @@ +import { EdgeCurrencyWallet } from 'edge-core-js' +import * as React from 'react' +import { View } from 'react-native' +import { sprintf } from 'sprintf-js' + +import { toPercentString } from '../../locales/intl' +import { lstrings } from '../../locales/strings' +import { StakePolicy } from '../../plugins/stake-plugins/types' +import { getPolicyIconUris } from '../../util/stakeUtils' +import { getUkCompliantString } from '../../util/ukComplianceUtils' +import { PairIcons } from '../icons/PairIcons' +import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' +import { TitleText } from '../text/TitleText' +import { EdgeText } from '../themed/EdgeText' +import { EdgeCard } from './EdgeCard' + +interface Props { + stakePolicy: StakePolicy + wallet: EdgeCurrencyWallet + + countryCode?: string + /** If false, show "Stake"/"Earn" + * If true, show "Staked"/"Earned" */ + isOpenPosition?: boolean + onPress?: () => void +} + +export function EarnOptionCard(props: Props) { + const theme = useTheme() + const styles = getStyles(theme) + + const { stakePolicy, wallet, isOpenPosition, countryCode, onPress } = props + const { apy, yieldType, stakeProviderInfo } = stakePolicy + + const { stakeAssets, rewardAssets } = stakePolicy + const stakeCurrencyCodes = stakeAssets.map(asset => asset.currencyCode).join(' + ') + const rewardCurrencyCodes = rewardAssets.map(asset => asset.currencyCode).join(', ') + + const stakeText = sprintf(isOpenPosition ? lstrings.stake_staked_1s : lstrings.stake_stake_1s, stakeCurrencyCodes) + const rewardText = isOpenPosition + ? sprintf(lstrings.stake_earning_1s, rewardCurrencyCodes) + : getUkCompliantString(countryCode, 'stake_earn_1s', rewardCurrencyCodes) + + const policyIcons = getPolicyIconUris(wallet.currencyInfo, stakePolicy) + + const variablePrefix = yieldType === 'stable' ? '' : '~ ' + const apyText = apy == null || apy <= 0 ? lstrings.stake_variable_apy : variablePrefix + sprintf(lstrings.stake_apy_1s, toPercentString(apy / 100)) + + return ( + + + + {stakeText} + {rewardText} + {apyText} + {`${lstrings.plugin_powered_by_space}${stakeProviderInfo.displayName}`} + + + + + + ) +} + +const getStyles = cacheStyles((theme: Theme) => ({ + contentContainer: { + flexDirection: 'row', + alignItems: 'center' + }, + textContainer: { + flexGrow: 1, + flexShrink: 1, + justifyContent: 'center', + padding: theme.rem(0.5) + }, + rewardText: { + fontSize: theme.rem(0.8), + marginTop: theme.rem(1), + marginBottom: theme.rem(0.15) + }, + apyText: { + fontSize: theme.rem(0.8), + color: theme.positiveText, + marginVertical: theme.rem(0.15) + }, + providerIcon: { + width: theme.rem(1), + height: theme.rem(1), + marginRight: theme.rem(0.25) + }, + providerText: { + fontSize: theme.rem(0.75), + color: theme.secondaryText + } +})) diff --git a/src/components/cards/StakingReturnsCard.tsx b/src/components/cards/StakingReturnsCard.tsx index 556acea8d61..8a922a30160 100644 --- a/src/components/cards/StakingReturnsCard.tsx +++ b/src/components/cards/StakingReturnsCard.tsx @@ -1,95 +1,168 @@ -import { EdgeCurrencyWallet } from 'edge-core-js' +import { toFixed } from 'biggystring' import * as React from 'react' -import { View } from 'react-native' +import { View, ViewStyle } from 'react-native' +import FastImage from 'react-native-fast-image' import { sprintf } from 'sprintf-js' -import { toPercentString } from '../../locales/intl' import { lstrings } from '../../locales/strings' -import { StakePolicy } from '../../plugins/stake-plugins/types' -import { getPolicyIconUris } from '../../util/stakeUtils' -import { getUkCompliantString } from '../../util/ukComplianceUtils' +import { StakeProviderInfo } from '../../plugins/stake-plugins/types' +import { getStakeProviderIcon } from '../../util/CdnUris' import { PairIcons } from '../icons/PairIcons' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' -import { TitleText } from '../text/TitleText' import { EdgeText } from '../themed/EdgeText' -import { EdgeCard } from './EdgeCard' -interface Props { - stakePolicy: StakePolicy - wallet: EdgeCurrencyWallet - - countryCode?: string - /** If false, show "Stake"/"Earn" - * If true, show "Staked"/"Earned" */ - isOpenPosition?: boolean - onPress?: () => void +interface StakingReturnsCardParams { + fromCurrencyLogos: string[] + toCurrencyLogos: string[] + apy?: number + stakeProviderInfo?: StakeProviderInfo } -export function StakingReturnsCard(props: Props) { +export function StakingReturnsCard({ fromCurrencyLogos, toCurrencyLogos, apy, stakeProviderInfo }: StakingReturnsCardParams) { const theme = useTheme() const styles = getStyles(theme) - const { stakePolicy, wallet, isOpenPosition, countryCode, onPress } = props - const { apy, yieldType, stakeProviderInfo } = stakePolicy - - const { stakeAssets, rewardAssets } = stakePolicy - const stakeCurrencyCodes = stakeAssets.map(asset => asset.currencyCode).join(' + ') - const rewardCurrencyCodes = rewardAssets.map(asset => asset.currencyCode).join(', ') - - const stakeText = sprintf(isOpenPosition ? lstrings.stake_staked_1s : lstrings.stake_stake_1s, stakeCurrencyCodes) - const rewardText = isOpenPosition - ? sprintf(lstrings.stake_earning_1s, rewardCurrencyCodes) - : getUkCompliantString(countryCode, 'stake_earn_1s', rewardCurrencyCodes) + const renderArrow = () => { + return ( + + + + + + ) + } - const policyIcons = getPolicyIconUris(wallet.currencyInfo, stakePolicy) + const renderEstimatedReturn = () => { + if (apy == null || apy <= 0) return null + const estimatedReturnMsg = toFixed(apy.toString(), 1, 1) + '% APR' + return {sprintf(lstrings.stake_estimated_return, estimatedReturnMsg)} + } - const variablePrefix = yieldType === 'stable' ? '' : '~ ' - const apyText = apy == null || apy <= 0 ? lstrings.stake_variable_apy : variablePrefix + sprintf(lstrings.stake_apy_1s, toPercentString(apy / 100)) + const renderStakeProvider = () => { + if (stakeProviderInfo == null) return null + const { displayName, pluginId, stakeProviderId } = stakeProviderInfo + const swapProviderIcon = getStakeProviderIcon(pluginId, stakeProviderId, theme) + return ( + + {swapProviderIcon ? : null} + {displayName} + + ) + } return ( - - + + + + + + + + {renderArrow()} + + + + - {stakeText} - {rewardText} - {apyText} - {`${lstrings.plugin_powered_by_space}${stakeProviderInfo.displayName}`} + {renderEstimatedReturn()} + {renderStakeProvider()} - - - + + ) } -const getStyles = cacheStyles((theme: Theme) => ({ - contentContainer: { - flexDirection: 'row', - alignItems: 'center' - }, - textContainer: { - flexGrow: 1, - flexShrink: 1, - justifyContent: 'center', - padding: theme.rem(0.5) - }, - rewardText: { - fontSize: theme.rem(0.8), - marginTop: theme.rem(1), - marginBottom: theme.rem(0.15) - }, - apyText: { - fontSize: theme.rem(0.8), - color: theme.positiveText, - marginVertical: theme.rem(0.15) - }, - providerIcon: { - width: theme.rem(1), - height: theme.rem(1), - marginRight: theme.rem(0.25) - }, - providerText: { - fontSize: theme.rem(0.75), - color: theme.secondaryText +const getStyles = cacheStyles((theme: Theme) => { + const commonCap: ViewStyle = { + borderColor: theme.lineDivider, + borderBottomWidth: theme.thinLineWidth, + borderTopWidth: theme.thinLineWidth, + width: theme.rem(1) + } + const commonArrow: ViewStyle = { + position: 'absolute', + width: theme.thinLineWidth * 2, + height: theme.rem(0.625), + right: 0 + theme.thinLineWidth * 1.5, + borderRadius: theme.thinLineWidth, + backgroundColor: theme.icon + } + return { + container: { + flexDirection: 'row', + minWidth: theme.rem(10), + marginTop: theme.rem(1.5) + }, + iconsContainer: { + flexDirection: 'row', + position: 'absolute' + }, + textContainer: { + alignItems: 'center', + paddingHorizontal: theme.rem(1), + paddingTop: theme.rem(2), + paddingBottom: theme.rem(1), + borderBottomWidth: theme.thinLineWidth, + borderColor: theme.lineDivider, + minWidth: theme.rem(15) + }, + icon: { + top: theme.rem(-1.5), + flexDirection: 'row', + alignItems: 'center' + }, + middleLine: { + flex: 1, + borderTopWidth: theme.thinLineWidth, + borderColor: theme.lineDivider + }, + leftCap: { + ...commonCap, + borderLeftWidth: theme.thinLineWidth, + borderRightWidth: 0, + borderBottomLeftRadius: theme.rem(0.5), + borderTopLeftRadius: theme.rem(0.5) + }, + rightCap: { + ...commonCap, + borderLeftWidth: 0, + borderRightWidth: theme.thinLineWidth, + borderBottomRightRadius: theme.rem(0.5), + borderTopRightRadius: theme.rem(0.5) + }, + swapProvider: { + marginTop: theme.rem(0.25), + flexDirection: 'row', + alignItems: 'center' + }, + swapProviderIcon: { + width: theme.rem(0.625), + height: theme.rem(0.625), + marginRight: theme.rem(0.5) + }, + swapProviderText: { + fontSize: theme.rem(0.75), + color: theme.secondaryText + }, + arrowContainer: { + flexDirection: 'row' + }, + arrowBase: { + width: theme.rem(3), + height: theme.thinLineWidth * 2, + borderRadius: theme.thinLineWidth, + backgroundColor: theme.icon + }, + arrowTopLine: { + ...commonArrow, + bottom: 0 - theme.thinLineWidth * 1.325, + transform: [{ rotateZ: '-45deg' }] + }, + arrowBottomLine: { + ...commonArrow, + top: 0 - theme.thinLineWidth * 1.325, + transform: [{ rotateZ: '45deg' }] + } } -})) +}) diff --git a/src/components/scenes/Staking/EarnScene.tsx b/src/components/scenes/Staking/EarnScene.tsx index 83b6c121d43..4e71fa524c9 100644 --- a/src/components/scenes/Staking/EarnScene.tsx +++ b/src/components/scenes/Staking/EarnScene.tsx @@ -15,7 +15,7 @@ import { EdgeAppSceneProps } from '../../../types/routerTypes' import { getPluginFromPolicy, getPositionAllocations } from '../../../util/stakeUtils' import { zeroString } from '../../../util/utils' import { EdgeSwitch } from '../../buttons/EdgeSwitch' -import { StakingReturnsCard } from '../../cards/StakingReturnsCard' +import { EarnOptionCard } from '../../cards/EarnOptionCard' import { EdgeAnim, fadeInUp20 } from '../../common/EdgeAnim' import { SceneWrapper } from '../../common/SceneWrapper' import { SectionHeader } from '../../common/SectionHeader' @@ -119,7 +119,7 @@ export const EarnScene = (props: Props) => { return ( - + ) })} diff --git a/src/components/scenes/Staking/StakeOverviewScene.tsx b/src/components/scenes/Staking/StakeOverviewScene.tsx index d405c31bf62..7faa6c73198 100644 --- a/src/components/scenes/Staking/StakeOverviewScene.tsx +++ b/src/components/scenes/Staking/StakeOverviewScene.tsx @@ -4,16 +4,15 @@ import { View } from 'react-native' import { FlatList } from 'react-native-gesture-handler' import { sprintf } from 'sprintf-js' -import { getFirstOpenInfo } from '../../../actions/FirstOpenActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../../constants/constantSettings' import { useAsyncEffect } from '../../../hooks/useAsyncEffect' import { lstrings } from '../../../locales/strings' import { ChangeQuoteRequest, PositionAllocation, StakePlugin, StakePolicy, StakePosition } from '../../../plugins/stake-plugins/types' import { selectDisplayDenomByCurrencyCode } from '../../../selectors/DenominationSelectors' import { useDispatch, useSelector } from '../../../types/reactRedux' -import { EdgeAppSceneProps } from '../../../types/routerTypes' +import { EdgeSceneProps } from '../../../types/routerTypes' import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' -import { getAllocationLocktimeMessage, getPolicyTitleName, getPositionAllocations } from '../../../util/stakeUtils' +import { getAllocationLocktimeMessage, getPolicyIconUris, getPolicyTitleName, getPositionAllocations } from '../../../util/stakeUtils' import { StyledButtonContainer } from '../../buttons/ButtonsView' import { StakingReturnsCard } from '../../cards/StakingReturnsCard' import { SceneWrapper } from '../../common/SceneWrapper' @@ -26,7 +25,7 @@ import { MainButton } from '../../themed/MainButton' import { SceneHeader } from '../../themed/SceneHeader' import { CryptoFiatAmountTile } from '../../tiles/CryptoFiatAmountTile' -interface Props extends EdgeAppSceneProps<'stakeOverview'> { +interface Props extends EdgeSceneProps<'stakeOverview'> { wallet: EdgeCurrencyWallet } @@ -55,13 +54,13 @@ const StakeOverviewSceneComponent = (props: Props) => { denomMap[asset.currencyCode] = dispatch((_, getState) => selectDisplayDenomByCurrencyCode(getState(), wallet.currencyConfig, asset.currencyCode)) return denomMap }, {}) + const policyIcons = getPolicyIconUris(wallet.currencyInfo, stakePolicy) // Hooks const [stakeAllocations, setStakeAllocations] = React.useState([]) const [rewardAllocations, setRewardAllocations] = React.useState([]) const [unstakedAllocations, setUnstakedAllocations] = React.useState([]) const [stakePosition, setStakePosition] = React.useState(startingStakePosition) - const [countryCode, setCountryCode] = React.useState() // Background loop to force fetchStakePosition updates const [updateCounter, setUpdateCounter] = React.useState(0) @@ -75,8 +74,6 @@ const StakeOverviewSceneComponent = (props: Props) => { useAsyncEffect( async () => { - setCountryCode((await getFirstOpenInfo()).countryCode) - let sp: StakePosition try { if (stakePosition == null) { @@ -101,7 +98,7 @@ const StakeOverviewSceneComponent = (props: Props) => { // Handlers const handleModifyPress = (modification: ChangeQuoteRequest['action'] | 'unstakeAndClaim') => () => { const sceneTitleMap = { - stake: getPolicyTitleName(stakePolicy, countryCode), + stake: getPolicyTitleName(stakePolicy), claim: lstrings.stake_claim_rewards, unstake: lstrings.stake_unstake, unstakeAndClaim: lstrings.stake_unstake_claim, @@ -150,7 +147,14 @@ const StakeOverviewSceneComponent = (props: Props) => { return ( - + + + {stakePosition == null ? ( <> From 74b8580133e9d709fb8a8222c733de4e9e6d1843 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 29 Oct 2024 10:53:22 -0700 Subject: [PATCH 2/4] Update `EarnOptionCard` to take `currencyInfo` instead of a `wallet` --- src/components/cards/EarnOptionCard.tsx | 8 ++++---- src/components/scenes/Staking/EarnScene.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/cards/EarnOptionCard.tsx b/src/components/cards/EarnOptionCard.tsx index 40c9624cfdf..7a45a0b503e 100644 --- a/src/components/cards/EarnOptionCard.tsx +++ b/src/components/cards/EarnOptionCard.tsx @@ -1,4 +1,4 @@ -import { EdgeCurrencyWallet } from 'edge-core-js' +import { EdgeCurrencyInfo } from 'edge-core-js' import * as React from 'react' import { View } from 'react-native' import { sprintf } from 'sprintf-js' @@ -15,8 +15,8 @@ import { EdgeText } from '../themed/EdgeText' import { EdgeCard } from './EdgeCard' interface Props { + currencyInfo: EdgeCurrencyInfo stakePolicy: StakePolicy - wallet: EdgeCurrencyWallet countryCode?: string /** If false, show "Stake"/"Earn" @@ -29,7 +29,7 @@ export function EarnOptionCard(props: Props) { const theme = useTheme() const styles = getStyles(theme) - const { stakePolicy, wallet, isOpenPosition, countryCode, onPress } = props + const { stakePolicy, currencyInfo, isOpenPosition, countryCode, onPress } = props const { apy, yieldType, stakeProviderInfo } = stakePolicy const { stakeAssets, rewardAssets } = stakePolicy @@ -41,7 +41,7 @@ export function EarnOptionCard(props: Props) { ? sprintf(lstrings.stake_earning_1s, rewardCurrencyCodes) : getUkCompliantString(countryCode, 'stake_earn_1s', rewardCurrencyCodes) - const policyIcons = getPolicyIconUris(wallet.currencyInfo, stakePolicy) + const policyIcons = getPolicyIconUris(currencyInfo, stakePolicy) const variablePrefix = yieldType === 'stable' ? '' : '~ ' const apyText = apy == null || apy <= 0 ? lstrings.stake_variable_apy : variablePrefix + sprintf(lstrings.stake_apy_1s, toPercentString(apy / 100)) diff --git a/src/components/scenes/Staking/EarnScene.tsx b/src/components/scenes/Staking/EarnScene.tsx index 4e71fa524c9..fbd0ea5924b 100644 --- a/src/components/scenes/Staking/EarnScene.tsx +++ b/src/components/scenes/Staking/EarnScene.tsx @@ -119,7 +119,7 @@ export const EarnScene = (props: Props) => { return ( - + ) })} From 51824adb1c912e0346cd2b31d59a543c01f93b94 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Tue, 29 Oct 2024 15:38:15 -0700 Subject: [PATCH 3/4] Restyle `StakingReturnsCard/StakeOverviewScene` to the new style --- src/components/cards/StakingReturnsCard.tsx | 68 ++++----------- .../scenes/Staking/StakeOverviewScene.tsx | 82 +++++++++++-------- 2 files changed, 61 insertions(+), 89 deletions(-) diff --git a/src/components/cards/StakingReturnsCard.tsx b/src/components/cards/StakingReturnsCard.tsx index 8a922a30160..5c86b4d75ac 100644 --- a/src/components/cards/StakingReturnsCard.tsx +++ b/src/components/cards/StakingReturnsCard.tsx @@ -10,6 +10,7 @@ import { getStakeProviderIcon } from '../../util/CdnUris' import { PairIcons } from '../icons/PairIcons' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' +import { EdgeCard } from './EdgeCard' interface StakingReturnsCardParams { fromCurrencyLogos: string[] @@ -51,35 +52,21 @@ export function StakingReturnsCard({ fromCurrencyLogos, toCurrencyLogos, apy, st } return ( - - - - - - - - {renderArrow()} - - - - - - {renderEstimatedReturn()} - {renderStakeProvider()} - + + + + {renderArrow()} + - - + + {renderEstimatedReturn()} + {renderStakeProvider()} + + ) } const getStyles = cacheStyles((theme: Theme) => { - const commonCap: ViewStyle = { - borderColor: theme.lineDivider, - borderBottomWidth: theme.thinLineWidth, - borderTopWidth: theme.thinLineWidth, - width: theme.rem(1) - } const commonArrow: ViewStyle = { position: 'absolute', width: theme.thinLineWidth * 2, @@ -96,41 +83,16 @@ const getStyles = cacheStyles((theme: Theme) => { }, iconsContainer: { flexDirection: 'row', - position: 'absolute' + marginVertical: theme.rem(0.5), + alignItems: 'center', + justifyContent: 'center' }, textContainer: { alignItems: 'center', - paddingHorizontal: theme.rem(1), - paddingTop: theme.rem(2), - paddingBottom: theme.rem(1), - borderBottomWidth: theme.thinLineWidth, + margin: theme.rem(0.5), borderColor: theme.lineDivider, minWidth: theme.rem(15) }, - icon: { - top: theme.rem(-1.5), - flexDirection: 'row', - alignItems: 'center' - }, - middleLine: { - flex: 1, - borderTopWidth: theme.thinLineWidth, - borderColor: theme.lineDivider - }, - leftCap: { - ...commonCap, - borderLeftWidth: theme.thinLineWidth, - borderRightWidth: 0, - borderBottomLeftRadius: theme.rem(0.5), - borderTopLeftRadius: theme.rem(0.5) - }, - rightCap: { - ...commonCap, - borderLeftWidth: 0, - borderRightWidth: theme.thinLineWidth, - borderBottomRightRadius: theme.rem(0.5), - borderTopRightRadius: theme.rem(0.5) - }, swapProvider: { marginTop: theme.rem(0.25), flexDirection: 'row', diff --git a/src/components/scenes/Staking/StakeOverviewScene.tsx b/src/components/scenes/Staking/StakeOverviewScene.tsx index 7faa6c73198..1fc256ae287 100644 --- a/src/components/scenes/Staking/StakeOverviewScene.tsx +++ b/src/components/scenes/Staking/StakeOverviewScene.tsx @@ -4,6 +4,7 @@ import { View } from 'react-native' import { FlatList } from 'react-native-gesture-handler' import { sprintf } from 'sprintf-js' +import { getFirstOpenInfo } from '../../../actions/FirstOpenActions' import { SCROLL_INDICATOR_INSET_FIX } from '../../../constants/constantSettings' import { useAsyncEffect } from '../../../hooks/useAsyncEffect' import { lstrings } from '../../../locales/strings' @@ -13,7 +14,7 @@ import { useDispatch, useSelector } from '../../../types/reactRedux' import { EdgeSceneProps } from '../../../types/routerTypes' import { getTokenIdForced } from '../../../util/CurrencyInfoHelpers' import { getAllocationLocktimeMessage, getPolicyIconUris, getPolicyTitleName, getPositionAllocations } from '../../../util/stakeUtils' -import { StyledButtonContainer } from '../../buttons/ButtonsView' +import { SceneButtons } from '../../buttons/SceneButtons' import { StakingReturnsCard } from '../../cards/StakingReturnsCard' import { SceneWrapper } from '../../common/SceneWrapper' import { withWallet } from '../../hoc/withWallet' @@ -21,8 +22,7 @@ import { FillLoader } from '../../progress-indicators/FillLoader' import { Shimmer } from '../../progress-indicators/Shimmer' import { showError } from '../../services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext' -import { MainButton } from '../../themed/MainButton' -import { SceneHeader } from '../../themed/SceneHeader' +import { SceneHeaderUi4 } from '../../themed/SceneHeaderUi4' import { CryptoFiatAmountTile } from '../../tiles/CryptoFiatAmountTile' interface Props extends EdgeSceneProps<'stakeOverview'> { @@ -57,6 +57,8 @@ const StakeOverviewSceneComponent = (props: Props) => { const policyIcons = getPolicyIconUris(wallet.currencyInfo, stakePolicy) // Hooks + + const [countryCode, setCountryCode] = React.useState() const [stakeAllocations, setStakeAllocations] = React.useState([]) const [rewardAllocations, setRewardAllocations] = React.useState([]) const [unstakedAllocations, setUnstakedAllocations] = React.useState([]) @@ -74,6 +76,8 @@ const StakeOverviewSceneComponent = (props: Props) => { useAsyncEffect( async () => { + setCountryCode((await getFirstOpenInfo()).countryCode) + let sp: StakePosition try { if (stakePosition == null) { @@ -98,7 +102,7 @@ const StakeOverviewSceneComponent = (props: Props) => { // Handlers const handleModifyPress = (modification: ChangeQuoteRequest['action'] | 'unstakeAndClaim') => () => { const sceneTitleMap = { - stake: getPolicyTitleName(stakePolicy), + stake: getPolicyTitleName(stakePolicy, countryCode), claim: lstrings.stake_claim_rewards, unstake: lstrings.stake_unstake, unstakeAndClaim: lstrings.stake_unstake_claim, @@ -146,15 +150,13 @@ const StakeOverviewSceneComponent = (props: Props) => { return ( - - - - + + {stakePosition == null ? ( <> @@ -173,34 +175,42 @@ const StakeOverviewSceneComponent = (props: Props) => { } scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} /> - - - {stakePolicy.hideClaimAction ? null : ( - - )} - {stakePolicy.hideUnstakeAndClaimAction ? null : ( - - )} - {stakePolicy.hideUnstakeAction ? null : ( - - )} - + ) } const getStyles = cacheStyles((theme: Theme) => ({ - card: { - alignItems: 'center', - justifyContent: 'flex-start', - padding: theme.rem(0.5) - }, shimmer: { height: theme.rem(3), marginLeft: theme.rem(1), From 14be004e4c943ef5e06f6ea0714826127a653923 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 30 Oct 2024 15:17:54 -0700 Subject: [PATCH 4/4] Refactor `EarnScene` - Initialization of `stakePolicyMap` to be based on all supported instead of only enabled wallet `pluginIds.` This also limits the display to one card per policy instead of one card per wallet. - Add wallet picker logic based on "Discover/Portfolio" state and number of open positions - Only initialize `stakePolicyMap` once, regardless of if we re-navigate to the scene --- CHANGELOG.md | 4 + src/components/scenes/Staking/EarnScene.tsx | 180 ++++++++++++-------- 2 files changed, 117 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e22ff0a4853..c066be6df92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - added: Add TON - added: Log swap errors to Sentry. - added: Tracking for unexpected fiat provider errors. +- changed: Redesign `StakingReturnsCard,` specifically for `StakeOverviewScene` +- changed: `EarnScene` shows all possible stake options, instead of only those for enabled wallets +- changed: `EarnScene` shows one card per stake option if multiple wallets have stake positions on that stake option +- changed: `EarnScene` only intializes stake options once, regardless of re-navigation to the scene - changed: `FiatProviderError` messages now include `FiatProviderQuoteError` info. - changed: Add explicit gas limit for Kiln staking. - changed: Various strings updated to UK compliance spec diff --git a/src/components/scenes/Staking/EarnScene.tsx b/src/components/scenes/Staking/EarnScene.tsx index fbd0ea5924b..a1fcf032b50 100644 --- a/src/components/scenes/Staking/EarnScene.tsx +++ b/src/components/scenes/Staking/EarnScene.tsx @@ -1,4 +1,4 @@ -import { EdgeCurrencyWallet } from 'edge-core-js' +import { EdgeCurrencyInfo, EdgeCurrencyWallet } from 'edge-core-js' import * as React from 'react' import { ActivityIndicator } from 'react-native' @@ -11,28 +11,36 @@ import { lstrings } from '../../../locales/strings' import { getStakePlugins } from '../../../plugins/stake-plugins/stakePlugins' import { StakePlugin, StakePolicy, StakePosition } from '../../../plugins/stake-plugins/types' import { useSelector } from '../../../types/reactRedux' -import { EdgeAppSceneProps } from '../../../types/routerTypes' -import { getPluginFromPolicy, getPositionAllocations } from '../../../util/stakeUtils' +import { EdgeAppSceneProps, NavigationBase } from '../../../types/routerTypes' +import { getPositionAllocations } from '../../../util/stakeUtils' import { zeroString } from '../../../util/utils' import { EdgeSwitch } from '../../buttons/EdgeSwitch' import { EarnOptionCard } from '../../cards/EarnOptionCard' import { EdgeAnim, fadeInUp20 } from '../../common/EdgeAnim' import { SceneWrapper } from '../../common/SceneWrapper' import { SectionHeader } from '../../common/SectionHeader' -import { showDevError } from '../../services/AirshipInstance' +import { WalletListModal, WalletListResult } from '../../modals/WalletListModal' +import { Airship, showDevError } from '../../services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../../services/ThemeContext' interface Props extends EdgeAppSceneProps<'earnScene'> {} export interface EarnSceneParams {} -interface StakePolicyPosition { - stakePolicy: StakePolicy +interface WalletStakeInfo { + wallet: EdgeCurrencyWallet + isPositionOpen: boolean stakePosition: StakePosition } +interface DisplayStakeInfo { + stakePlugin: StakePlugin + stakePolicy: StakePolicy + walletStakeInfos: WalletStakeInfo[] +} + interface StakePolicyMap { - [walletId: string]: { stakePolicyPositions: StakePolicyPosition[]; stakePlugins: StakePlugin[] } + [pluginId: string]: DisplayStakeInfo[] } export const EarnScene = (props: Props) => { @@ -41,101 +49,139 @@ export const EarnScene = (props: Props) => { const styles = getStyles(theme) const account = useSelector(state => state.core.account) + + const currencyConfigMap = useSelector(state => state.core.account.currencyConfig) + const currencyWallets = useWatch(account, 'currencyWallets') const wallets = Object.values(currencyWallets) - const [stakePolicyMap, setStakePolicyMap] = React.useState() - const [positionWallets, setPositionWallets] = React.useState([]) const [isPortfolioSelected, setIsPortfolioSelected] = React.useState(false) const [isLoading, setIsLoading] = React.useState(true) - // Filter wallets based on isPortfolioSelected - const displayWallets = !stakePolicyMap ? [] : isPortfolioSelected ? positionWallets : wallets + // Store `stakePolicyMap` in a ref and manage re-renders manually to avoid + // re-initializing it every time we enter the scene. + const [updateCounter, setUpdateCounter] = React.useState(0) + const stakePolicyMapRef = React.useRef({}) + const stakePolicyMap = stakePolicyMapRef.current const handleSelectEarn = useHandler(() => setIsPortfolioSelected(false)) const handleSelectPortfolio = useHandler(() => setIsPortfolioSelected(true)) useAsyncEffect( async () => { - if (stakePolicyMap != null) return - - const positionWallets = [] - const policyMap: StakePolicyMap = {} - - for (const wallet of wallets) { - // Get all available stake policies - const { pluginId } = wallet.currencyInfo - if (SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING) { - // For each wallet - const stakePolicyPositions: StakePolicyPosition[] = [] - - try { - const stakePlugins = await getStakePlugins(pluginId) - for (const stakePlugin of stakePlugins) { - const stakePolicies = stakePlugin.getPolicies({ wallet, currencyCode: wallet.currencyInfo.currencyCode }) - - // Check if there's open positions - for (const stakePolicy of stakePolicies) { + for (const pluginId of Object.keys(currencyConfigMap)) { + const isStakingSupported = SPECIAL_CURRENCY_INFO[pluginId]?.isStakingSupported === true && ENV.ENABLE_STAKING + if (stakePolicyMap[pluginId] != null || !isStakingSupported) continue + + // Initialize stake policy + try { + const stakePlugins = await getStakePlugins(pluginId) + stakePolicyMap[pluginId] = [] + + for (const stakePlugin of stakePlugins) { + const stakePolicies = stakePlugin.getPolicies({ currencyCode: currencyConfigMap[pluginId].currencyInfo.currencyCode }) + const matchingWallets = wallets.filter((wallet: EdgeCurrencyWallet) => wallet.currencyInfo.pluginId === pluginId) + + for (const stakePolicy of stakePolicies) { + const walletStakePositions = [] + for (const wallet of matchingWallets) { + // Determine if a wallet matching this policy has an open position const stakePosition = await stakePlugin.fetchStakePosition({ stakePolicyId: stakePolicy.stakePolicyId, wallet, account }) - stakePolicyPositions.push({ stakePolicy, stakePosition }) - const allocations = getPositionAllocations(stakePosition) const { staked, earned, unstaked } = allocations - if ([...staked, ...earned, ...unstaked].some(positionAllocation => !zeroString(positionAllocation.nativeAmount))) { - positionWallets.push(wallet) - } + const isPositionOpen = [...staked, ...earned, ...unstaked].some(positionAllocation => !zeroString(positionAllocation.nativeAmount)) + + walletStakePositions.push({ wallet, isPositionOpen, stakePosition }) } - } - policyMap[wallet.id] = { stakePolicyPositions, stakePlugins } - setStakePolicyMap({ ...policyMap }) - setPositionWallets(positionWallets) - } catch (e) { - showDevError(e) + stakePolicyMap[pluginId].push({ + stakePlugin, + stakePolicy, + walletStakeInfos: walletStakePositions + }) + // Trigger re-render + setUpdateCounter(prevCounter => prevCounter + 1) + } } + } catch (e) { + showDevError(e) } } setIsLoading(false) }, - [], + [updateCounter], 'EarnScene' ) - const renderStakeItems = (wallet: EdgeCurrencyWallet) => { - if (stakePolicyMap == null) return null + const renderStakeItems = (displayStakeInfo: DisplayStakeInfo, currencyInfo: EdgeCurrencyInfo) => { + const { stakePlugin, stakePolicy, walletStakeInfos } = displayStakeInfo + + const handlePress = async () => { + let walletId: string | undefined + let stakePosition + + const openStakePositions = walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen) + + 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 { 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 allowedWalletIds = isPortfolioSelected + ? walletStakeInfos.filter(walletStakeInfo => walletStakeInfo.isPositionOpen).map(walletStakePosition => walletStakePosition.wallet.id) + : undefined + + const result = await Airship.show(bridge => ( + + )) + + 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) return - const { stakePolicyPositions, stakePlugins } = stakePolicyMap[wallet.id] ?? { stakePolicyPositions: [], stakePlugins: [] } + navigation.push('stakeOverview', { + walletId, + stakePlugin, + stakePolicy, + stakePosition + }) + } return ( - <> - {stakePolicyPositions.map((stakePolicyPosition: { stakePolicy: StakePolicy; stakePosition: StakePosition }, index: number) => { - const { stakePolicy, stakePosition } = stakePolicyPosition - - if (stakePolicy == null) return null - const stakePlugin = getPluginFromPolicy(stakePlugins, stakePolicy) - - const handlePress = - stakePlugin == null ? undefined : () => navigation.push('stakeOverview', { stakePlugin, walletId: wallet.id, stakePolicy, stakePosition }) - - return ( - - - - ) - })} - + + + ) } return ( - // TODO: Address "VirtualizedLists should never be nested inside plain - // ScrollViews with the same orientation because it can break windowing and - // other functionality - use another VirtualizedList-backed container - // instead." somehow, while retaining the bottom loader positioning... - {displayWallets.map(wallet => renderStakeItems(wallet))} + {Object.keys(stakePolicyMap).map(pluginId => + stakePolicyMap[pluginId].map(displayStakeInfo => renderStakeItems(displayStakeInfo, currencyConfigMap[pluginId].currencyInfo)) + )} {isLoading && } )