Updated on 2026-08-14
This commit is contained in:
parent
d61801a6d4
commit
10d959b0de
18 changed files with 467 additions and 162 deletions
|
|
@ -90,6 +90,7 @@ fun InputRowDefault(
|
|||
bottom = TangemTheme.dimens.spacing10,
|
||||
)
|
||||
.clickable(
|
||||
enabled = onIconClick != null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) { onIconClick?.invoke() },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ object Strings {
|
|||
|
||||
const val STARS = "\u2217\u2217\u2217"
|
||||
const val DOT = "•"
|
||||
const val PLUS = "+"
|
||||
}
|
||||
|
|
@ -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<BalanceState>,
|
||||
val type: BalanceType,
|
||||
val items: ImmutableList<BalanceState>,
|
||||
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?,
|
||||
)
|
||||
)
|
||||
|
||||
enum class BalanceGroupType {
|
||||
ACTIVE,
|
||||
UNSTAKED,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BalanceState>,
|
||||
) : 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,
|
||||
|
|
|
|||
|
|
@ -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<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val yield: Yield,
|
||||
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
|
||||
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<BalanceItem>.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CryptoCurrencyStatus>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
private val yield: Yield,
|
||||
) : Converter<Unit, InnerYieldBalanceState> {
|
||||
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<BalanceItem>.mapBalances(): List<BalanceState> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -27,4 +27,8 @@ object StakingClickIntentsStub : StakingClickIntents {
|
|||
override fun openValidators() {}
|
||||
|
||||
override fun onValidatorSelect(validator: Yield.Validator) {}
|
||||
|
||||
override fun openRewardsValidators() {}
|
||||
|
||||
override fun selectRewardValidator(rewardValue: String) {}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<BalanceGroupedState>) {
|
||||
private fun ActiveStakingBlock(groups: List<BalanceGroupedState>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Int, () -> Unit> {
|
||||
private fun getButtonData(currentState: StakingUiState): Pair<Int, (() -> 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<Int, () -> 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -19,4 +19,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
|
|||
fun openValidators()
|
||||
|
||||
fun onValidatorSelect(validator: Yield.Validator)
|
||||
|
||||
fun openRewardsValidators()
|
||||
|
||||
fun selectRewardValidator(rewardValue: String)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue