From 10d959b0de0783e11be19cfeb62979da6606a91a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 18:14:36 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../ui/components/inputrow/InputRowDefault.kt | 1 + .../components/inputrow/InputRowImageInfo.kt | 2 +- .../src/main/java/com/tangem/utils/Strings.kt | 1 + .../state/InnerYieldBalanceState.kt | 14 +- .../state/StakingStateController.kt | 1 + .../presentation/state/StakingStateRouter.kt | 5 + .../impl/presentation/state/StakingUiState.kt | 14 ++ .../RewardsValidatorStateConverter.kt | 100 +++++++++++++ .../converters/YieldBalancesConverter.kt | 138 ++++++++++++++++++ .../previewdata/InitialStakingStatePreview.kt | 6 +- .../state/stub/StakingClickIntentsStub.kt | 4 + .../SetInitialDataStateTransformer.kt | 112 +++----------- .../ui/StakingClaimRewardsValidatorContent.kt | 73 +++++++++ .../ui/StakingInitialInfoContent.kt | 73 +++++---- .../ui/StakingNavigationButtons.kt | 40 +++-- .../impl/presentation/ui/StakingScreen.kt | 12 +- .../viewmodel/StakingClickIntents.kt | 4 + .../viewmodel/StakingViewModel.kt | 29 ++-- 18 files changed, 467 insertions(+), 162 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 3b1e657dda..1eff068d9f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -90,6 +90,7 @@ fun InputRowDefault( bottom = TangemTheme.dimens.spacing10, ) .clickable( + enabled = onIconClick != null, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), ) { onIconClick?.invoke() }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index 81a7e01218..c4fb398c6e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -36,13 +36,13 @@ import com.tangem.core.ui.res.TangemThemePreview @Suppress("LongParameterList") @Composable fun InputRowImageInfo( - title: TextReference?, subtitle: TextReference, caption: TextReference, infoTitle: TextReference, infoSubtitle: TextReference, imageUrl: String, modifier: Modifier = Modifier, + title: TextReference? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, ) { diff --git a/core/utils/src/main/java/com/tangem/utils/Strings.kt b/core/utils/src/main/java/com/tangem/utils/Strings.kt index f6cc72a41a..b40470e852 100644 --- a/core/utils/src/main/java/com/tangem/utils/Strings.kt +++ b/core/utils/src/main/java/com/tangem/utils/Strings.kt @@ -4,4 +4,5 @@ object Strings { const val STARS = "\u2217\u2217\u2217" const val DOT = "•" + const val PLUS = "+" } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index f50e8ec598..61015e2357 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -1,8 +1,8 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield +import kotlinx.collections.immutable.ImmutableList sealed class InnerYieldBalanceState { data class Data( @@ -16,15 +16,21 @@ sealed class InnerYieldBalanceState { } data class BalanceGroupedState( - val items: List, - val type: BalanceType, + val items: ImmutableList, val footer: TextReference?, val title: TextReference, ) data class BalanceState( val validator: Yield.Validator, + val cryptoValue: String, val cryptoAmount: TextReference, val fiatAmount: TextReference, val rawCurrencyId: String?, -) \ No newline at end of file +) + +enum class BalanceGroupType { + ACTIVE, + UNSTAKED, + UNKNOWN, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index bef915818c..d62526b9a8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -39,6 +39,7 @@ internal class StakingStateController @Inject constructor() { currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), + rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmStakingState = StakingStates.ConfirmStakingState.Empty(), isBalanceHidden = false, event = consumedEvent(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index eef49189ea..921430000b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -28,6 +28,7 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.uiState.value.currentStep) { StakingStep.InitialInfo -> showAmount() + StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Amount, -> showConfirm() @@ -50,6 +51,10 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } + fun showRewardsValidators() { + stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } + } + fun showAmount() { stateController.update { it.copy(currentStep = StakingStep.Amount) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index b1758a8d42..8d952f02e4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -19,6 +19,7 @@ internal data class StakingUiState( val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, + val rewardsValidatorsState: StakingStates.RewardsValidatorsState, val confirmStakingState: StakingStates.ConfirmStakingState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, @@ -61,6 +62,18 @@ internal sealed class StakingStates { ) : InitialInfoState() } + /** Select validator to claim rewards state */ + sealed class RewardsValidatorsState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val rewards: ImmutableList, + ) : RewardsValidatorsState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : RewardsValidatorsState() + } + /** Confirm state */ sealed class ConfirmStakingState : StakingStates() { data class Data( @@ -81,6 +94,7 @@ internal sealed class StakingStates { enum class StakingStep { InitialInfo, + RewardsValidators, Amount, Confirm, Validators, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt new file mode 100644 index 0000000000..7ebb9058e7 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -0,0 +1,100 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.BalanceItem +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.utils.Provider +import com.tangem.utils.Strings.PLUS +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addOrReplace +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class RewardsValidatorStateConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: Unit): StakingStates.RewardsValidatorsState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + return if (yieldBalance is YieldBalance.Data) { + val balances = yieldBalance.balance.items + StakingStates.RewardsValidatorsState.Data( + isPrimaryButtonEnabled = true, + rewards = balances + // todo remove when real data is available + .addOrReplace( + item = balances.first().copy( + type = BalanceType.REWARDS, + ), + predicate = { true }, + ) + .filter { it.type == BalanceType.REWARDS } + .mapRewardBalances(cryptoCurrencyStatus) + .toPersistentList(), + ) + } else { + StakingStates.RewardsValidatorsState.Empty() + } + } + + private fun List.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) = + this.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true) + } + val cryptoValue = balance.amount.times(balance.pricePerShare) + val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) + + validator?.toBalanceState( + cryptoCurrencyStatus = cryptoCurrencyStatus, + cryptoValue = cryptoValue, + fiatValue = fiatValue, + ) + } + + private fun Yield.Validator.toBalanceState( + cryptoCurrencyStatus: CryptoCurrencyStatus, + cryptoValue: BigDecimal, + fiatValue: BigDecimal?, + ): BalanceState { + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + val cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoValue, + cryptoCurrency = cryptoCurrency, + ), + ) + val fiatAmount = combinedReference( + stringReference(PLUS), + stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + ) + + return BalanceState( + validator = this, + cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), + cryptoAmount = cryptoAmount, + fiatAmount = fiatAmount, + rawCurrencyId = cryptoCurrency.id.rawCurrencyId, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt new file mode 100644 index 0000000000..28ede76079 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -0,0 +1,138 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.* +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.BalanceGroupType +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.toPersistentList + +internal class YieldBalancesConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: Unit): InnerYieldBalanceState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + + val cryptoCurrency = cryptoCurrencyStatus.currency + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + + return if (yieldBalance is YieldBalance.Data) { + val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() + val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) + val groupedBalances = getGroupedBalance(yieldBalance.balance) + + InnerYieldBalanceState.Data( + rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoRewardsValue, + cryptoCurrency = cryptoCurrency, + ), + rewardsFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRewardsValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), + balance = groupedBalances, + ) + } else { + InnerYieldBalanceState.Empty + } + } + + private fun getGroupedBalance(balance: YieldBalanceItem) = balance.items + // todo remove when real data is available + .addOrReplace( + item = balance.items.first().copy( + type = BalanceType.REWARDS, + ), + predicate = { true }, + ) + .groupBy { it.type.toGroup() } + .mapNotNull { item -> + val (title, footer) = getGroupTitle(item.key) + title?.let { + BalanceGroupedState( + items = item.value.mapBalances().toPersistentList(), + footer = footer, + title = it, + ) + } + } + + private fun List.mapBalances(): List { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + return this.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + balance.validatorAddress?.contains(it.address, ignoreCase = true) == true + } + val cryptoAmount = balance.amount * balance.pricePerShare + val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) + + validator?.let { + BalanceState( + validator = validator, + cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), + cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + rawCurrencyId = balance.rawCurrencyId, + ) + } + } + } + + private fun BalanceType.toGroup() = when (this) { + BalanceType.PREPARING, + BalanceType.STAKED, + BalanceType.REWARDS, + BalanceType.AVAILABLE, + BalanceType.LOCKED, + -> BalanceGroupType.ACTIVE + BalanceType.UNSTAKING, + BalanceType.UNLOCKING, + BalanceType.UNSTAKED, + -> BalanceGroupType.UNSTAKED + BalanceType.UNKNOWN, + -> BalanceGroupType.UNKNOWN + } + + private fun getGroupTitle(type: BalanceGroupType) = when (type) { + BalanceGroupType.ACTIVE -> resourceReference( + R.string.staking_active, + ) to resourceReference( + R.string.staking_active_footer, + ) + BalanceGroupType.UNSTAKED -> resourceReference( + R.string.staking_unstaked, + ) to resourceReference( + R.string.staking_unstaked_footer, + ) + BalanceGroupType.UNKNOWN -> null to null + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index f49e7535d0..4eadd4916a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -1,12 +1,12 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates +import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { val defaultState = StakingStates.InitialInfoState.Data( @@ -30,11 +30,11 @@ internal object InitialStakingStatePreview { isRewardsToClaim = false, balance = listOf( BalanceGroupedState( - type = BalanceType.STAKED, title = stringReference("Staked"), footer = null, - items = listOf( + items = persistentListOf( BalanceState( + cryptoValue = "100", cryptoAmount = stringReference("100 SOL"), fiatAmount = stringReference("100 $"), rawCurrencyId = null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 8a120d3f9a..6320dce75c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -27,4 +27,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun openValidators() {} override fun onValidatorSelect(validator: Yield.Validator) {} + + override fun openRewardsValidators() {} + + override fun selectRewardValidator(rewardValue: String) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 0438982aff..9e1341b01c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -9,19 +9,21 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.YieldBalance -import com.tangem.domain.staking.model.YieldBalanceItem import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter +import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero -import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -45,6 +47,14 @@ internal class SetInitialDataStateTransformer( ) } + private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { + RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + + private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { + YieldBalancesConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( clickIntents = clickIntents, @@ -52,6 +62,8 @@ internal class SetInitialDataStateTransformer( initialInfoState = createInitialInfoState(), amountState = createInitialAmountState(), confirmStakingState = createInitialConfirmationState(), + rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit), + bottomSheetConfig = null, ) } @@ -78,7 +90,7 @@ internal class SetInitialDataStateTransformer( warmupPeriod = yield.metadata.warmupPeriod.days.toString(), rewardSchedule = yield.metadata.rewardSchedule, onInfoClick = clickIntents::onInfoClick, - yieldBalance = getStakedBalances(cryptoCurrencyStatus), + yieldBalance = yieldBalancesConverter.convert(Unit), ) } @@ -116,96 +128,6 @@ internal class SetInitialDataStateTransformer( return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr)) } - private fun getStakedBalances(cryptoCurrencyStatus: CryptoCurrencyStatus): InnerYieldBalanceState { - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance - return if (yieldBalance is YieldBalance.Data) { - val appCurrency = appCurrencyProvider() - val cryptoCurrency = cryptoCurrencyStatus.currency - val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() - val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) - val groupedBalances = getGroupedBalance(yieldBalance.balance, cryptoCurrencyStatus, appCurrency) - - InnerYieldBalanceState.Data( - rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoRewardsValue, - cryptoCurrency = cryptoCurrency, - ), - rewardsFiat = BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatRewardsValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), - balance = groupedBalances, - ) - } else { - InnerYieldBalanceState.Empty - } - } - - private fun getGroupedBalance( - balance: YieldBalanceItem, - cryptoCurrencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ) = balance.items - .sortedByDescending { it.type } - .groupBy { it.type } - .mapNotNull { item -> - val (title, footer) = getValidatorBalanceInto(item.key) - - val balances = item.value.mapNotNull { balance -> - val validator = yield.validators.firstOrNull { - balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } - val cryptoAmount = balance.amount * balance.pricePerShare - val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - - validator?.let { - BalanceState( - validator = validator, - cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = balance.amount, - cryptoCurrency = cryptoCurrencyStatus.currency, - ), - ), - fiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ), - rawCurrencyId = balance.rawCurrencyId, - ) - } - } - - title?.let { - BalanceGroupedState( - items = balances, - type = item.key, - footer = footer, - title = it, - ) - } - } - - private fun getValidatorBalanceInto(type: BalanceType) = when (type) { - BalanceType.PREPARING, - BalanceType.STAKED, - -> resourceReference(R.string.staking_active) to resourceReference(R.string.staking_active_footer) - BalanceType.UNSTAKING, - BalanceType.UNLOCKING, - BalanceType.UNSTAKED, - -> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer) - BalanceType.AVAILABLE, - BalanceType.LOCKED, - BalanceType.UNKNOWN, - BalanceType.REWARDS, - -> null to null - } - companion object { private val EQUALITY_THRESHOLD = BigDecimal(1E-10) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt new file mode 100644 index 0000000000..64a2f51b56 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -0,0 +1,73 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.inputrow.InputRowImageInfo +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.extensions.orZero + +@Composable +internal fun StakingClaimRewardsValidatorContent( + state: StakingStates.RewardsValidatorsState, + clickIntents: StakingClickIntents, + modifier: Modifier = Modifier, +) { + if (state !is StakingStates.RewardsValidatorsState.Data) return + Column( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing12) + .verticalScroll(rememberScrollState()), + ) { + state.rewards.forEachIndexed { index, item -> + key(item.validator.address) { + InputRowImageInfo( + subtitle = stringReference(item.validator.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + appendSpace() + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent( + percent = item.validator.apr.orZero(), + useAbsoluteValue = true, + ), + ) + } + }, + ), + ), + infoTitle = item.fiatAmount, + infoSubtitle = item.cryptoAmount, + imageUrl = item.validator.image.orEmpty(), + modifier = modifier + .roundedShapeItemDecoration(index, state.rewards.lastIndex, false) + .background(TangemTheme.colors.background.action) + .clickable( + onClick = { + clickIntents.selectRewardValidator(item.cryptoValue) + }, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 81730a1913..5ca7d71e8e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -4,13 +4,18 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -34,12 +39,14 @@ import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Strings.DOT import com.tangem.utils.extensions.orZero @Composable -internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { +internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) { if (state !is StakingStates.InitialInfoState.Data) return Column( @@ -63,6 +70,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { rewardCrypto = it.rewardsCrypto, rewardFiat = it.rewardsFiat, isRewardsToClaim = it.isRewardsToClaim, + onRewardsClick = clickIntents::openRewardsValidators, ) } } @@ -188,7 +196,12 @@ internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { } @Composable -private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewardsToClaim: Boolean) { +private fun StakingRewardBlock( + rewardCrypto: String, + rewardFiat: String, + isRewardsToClaim: Boolean, + onRewardsClick: () -> Unit, +) { val (text, textColor) = if (isRewardsToClaim) { annotatedReference( buildAnnotatedString { @@ -211,37 +224,46 @@ private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewar textColor = textColor, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action), + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onRewardsClick, + ), ) } @Composable -private fun ActiveStakingBlock(groupes: List) { +private fun ActiveStakingBlock(groups: List) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - groupes.forEach { group -> - FooterContainer( - footer = group.footer?.resolveReference(), - modifier = Modifier, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action), + groups.forEach { group -> + key(group.title) { + FooterContainer( + footer = group.footer?.resolveReference(), + modifier = Modifier, ) { - group.items.forEachIndexed { index, balance -> - InputRowImageInfo( - title = group.title.takeIf { index == 0 }, - subtitle = stringReference(balance.validator.name), - caption = stringReference( - BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), - ), - infoTitle = balance.fiatAmount, - infoSubtitle = balance.cryptoAmount, - imageUrl = balance.validator.image.orEmpty(), - ) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + group.items.forEachIndexed { index, balance -> + key(balance.validator.address) { + InputRowImageInfo( + title = group.title.takeIf { index == 0 }, + subtitle = stringReference(balance.validator.name), + caption = stringReference( + BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), + ), + infoTitle = balance.fiatAmount, + infoSubtitle = balance.cryptoAmount, + imageUrl = balance.validator.image.orEmpty(), + ) + } + } } } } @@ -278,6 +300,7 @@ private fun StakingInitialInfoContent_Preview( TangemThemePreview { StakingInitialInfoContent( state = feeState, + clickIntents = StakingClickIntentsStub, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index e9c29e3cb1..1ee7aca2ac 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -1,8 +1,6 @@ package com.tangem.features.staking.impl.presentation.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -84,22 +82,29 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier SpacerW12() } } - TangemButton( - text = stringResource(buttonTextId), - icon = buttonIcon, - enabled = isButtonEnabled, - onClick = { - if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - buttonClick() - }, - showProgress = false, - modifier = Modifier.fillMaxWidth(), - colors = TangemButtonsDefaults.primaryButtonColors, - ) + + AnimatedVisibility( + visible = buttonClick != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled && buttonClick != null, + onClick = { + if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + if (buttonClick != null) buttonClick() + }, + showProgress = false, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } } } -private fun getButtonData(currentState: StakingUiState): Pair Unit> { +private fun getButtonData(currentState: StakingUiState): Pair Unit)?> { return when (currentState.currentStep) { StakingStep.InitialInfo -> { val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data @@ -114,11 +119,13 @@ private fun getButtonData(currentState: StakingUiState): Pair Unit> { StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick + else -> R.string.common_next to null } } private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { StakingStep.InitialInfo, + StakingStep.RewardsValidators, StakingStep.Confirm, StakingStep.Success, -> false @@ -132,6 +139,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled StakingStep.Success -> true StakingStep.Validators -> true } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index d53e68d30a..1b92f7fcea 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -65,6 +65,7 @@ private fun SendAppBar(uiState: StakingUiState) { val titleRes = when (uiState.currentStep) { StakingStep.Amount -> stringResource(id = R.string.send_amount_label) StakingStep.InitialInfo, + StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Confirm, -> stringResource(id = R.string.common_stake) @@ -78,7 +79,9 @@ private fun SendAppBar(uiState: StakingUiState) { -> { R.drawable.ic_close_24 } - StakingStep.InitialInfo -> { + StakingStep.RewardsValidators, + StakingStep.InitialInfo, + -> { R.drawable.ic_back_24 } } @@ -137,7 +140,14 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M when (state) { StakingStep.InitialInfo -> StakingInitialInfoContent( state = uiState.initialInfoState, + clickIntents = uiState.clickIntents, ) + StakingStep.RewardsValidators -> { + StakingClaimRewardsValidatorContent( + state = uiState.rewardsValidatorsState, + clickIntents = uiState.clickIntents, + ) + } StakingStep.Amount -> AmountScreenContent( amountState = uiState.amountState, isBalanceHiding = uiState.isBalanceHidden, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index d89142ec38..62f5fc6c5c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -19,4 +19,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun openValidators() fun onValidatorSelect(validator: Yield.Validator) + + fun openRewardsValidators() + + fun selectRewardValidator(rewardValue: String) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index ef08dd78bb..23bdae1705 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -21,9 +21,9 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter -import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer @@ -89,22 +89,8 @@ internal class StakingViewModel @Inject constructor( override fun onNextClick() { stakingStateRouter.onNextClick() - when (value.currentStep) { - StakingStep.Confirm -> { - initStaking() - } - StakingStep.InitialInfo -> { - // TODO staking - } - StakingStep.Amount -> { - // TODO staking - } - StakingStep.Success -> { - // TODO staking - } - StakingStep.Validators -> { - // TODO staking - } + if (value.currentStep == StakingStep.Confirm) { + initStaking() } } @@ -175,6 +161,15 @@ internal class StakingViewModel @Inject constructor( stateController.update(ValidatorSelectChangeTransformer(validator)) } + override fun openRewardsValidators() { + stakingStateRouter.showRewardsValidators() + } + + override fun selectRewardValidator(rewardValue: String) { + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue)) + stakingStateRouter.onNextClick() + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter