Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-18 21:22:11 +05:00
parent ef36fbf13e
commit 5e597758b9
13 changed files with 121 additions and 65 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@ -31,6 +32,7 @@ data class BalanceState(
val fiatAmount: TextReference,
val rawCurrencyId: String?,
val unbondingPeriod: TextReference,
val pendingActions: ImmutableList<PendingAction>,
)
enum class BalanceGroupType {

View file

@ -16,7 +16,9 @@ internal class StakingStateRouter(
when (stateController.value.currentStep) {
StakingStep.InitialInfo -> when (stateController.value.routeType) {
RouteType.STAKE -> showAmount()
RouteType.UNSTAKE -> showConfirmation()
RouteType.OTHER,
RouteType.UNSTAKE,
-> showConfirmation()
RouteType.CLAIM -> showRewardsValidators()
}
StakingStep.RewardsValidators,

View file

@ -5,6 +5,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -83,6 +84,7 @@ internal sealed class StakingStates {
val innerState: InnerConfirmationStakingState,
val feeState: FeeState,
val validatorState: ValidatorState,
val pendingActions: ImmutableList<PendingAction>,
val notifications: ImmutableList<StakingNotification>,
val footerText: String,
val transactionDoneState: TransactionDoneState,
@ -106,4 +108,5 @@ enum class RouteType {
STAKE,
UNSTAKE,
CLAIM,
OTHER,
}

View file

@ -4,10 +4,7 @@ import com.tangem.core.ui.extensions.*
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.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceState
@ -15,6 +12,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.PLUS
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
@ -58,6 +56,7 @@ internal class RewardsValidatorStateConverter(
count = unbondingPeriod,
formatArgs = wrappedList(unbondingPeriod),
),
pendingActions = balance.pendingActions.toPersistentList(),
)
}
@ -66,6 +65,7 @@ internal class RewardsValidatorStateConverter(
cryptoValue: BigDecimal,
fiatValue: BigDecimal?,
unbondingPeriod: TextReference,
pendingActions: ImmutableList<PendingAction>,
): BalanceState {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -95,6 +95,7 @@ internal class RewardsValidatorStateConverter(
fiatAmount = fiatAmount,
rawCurrencyId = cryptoCurrency.id.rawCurrencyId,
unbondingPeriod = unbondingPeriod,
pendingActions = pendingActions,
)
}
}

View file

@ -104,6 +104,7 @@ internal class YieldBalancesConverter(
count = unbondingPeriod,
formatArgs = wrappedList(unbondingPeriod),
),
pendingActions = balance.pendingActions.toPersistentList(),
)
}
}

View file

@ -85,5 +85,6 @@ internal object ConfirmationStatePreviewData {
),
),
transactionDoneState = TransactionDoneState.Empty,
pendingActions = persistentListOf(),
)
}

View file

@ -51,6 +51,7 @@ internal object InitialStakingStatePreview {
preferred = false,
),
unbondingPeriod = stringReference("3 days"),
pendingActions = persistentListOf(),
),
),
),

View file

@ -1,15 +1,17 @@
package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
object StakingClickIntentsStub : StakingClickIntents {
override fun onBackClick() {}
override fun onNextClick() {}
override fun onNextClick(pendingActions: ImmutableList<PendingAction>) {}
override fun onPrevClick() {}

View file

@ -1,22 +1,25 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
import com.tangem.blockchain.common.Amount
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import kotlinx.collections.immutable.ImmutableList
@Suppress("UnusedPrivateMember")
internal class SetConfirmationStateAssentTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val stakingGasEstimate: StakingGasEstimate,
private val pendingActionList: ImmutableList<PendingAction>,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -45,6 +48,7 @@ internal class SetConfirmationStateAssentTransformer(
isFeeApproximate = false,
),
validatorState = validatorState.copySealed(isClickable = true),
pendingActions = pendingActionList,
isPrimaryButtonEnabled = true,
)
} else {

View file

@ -2,10 +2,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
@ -37,6 +33,7 @@ internal class SetConfirmationStateLoadingTransformer(
),
footerText = "",
transactionDoneState = TransactionDoneState.Empty,
pendingActions = persistentListOf(),
),
)
}

View file

@ -25,8 +25,6 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
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.StakingUiState
@Composable
internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) {
@ -70,10 +68,8 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
val showTangemIcon = uiState.currentStep == StakingStep.Confirmation &&
(isInProgressInnerState || isInAssentInnerState)
val (buttonTextId, buttonClick) = getButtonData(
currentState = uiState,
)
val isButtonEnabled = isButtonEnabled(uiState)
val buttonTextId = getButtonData(currentState = uiState)
val (isButtonEnabled, isButtonDisplayed) = isButtonEnabled(uiState)
val buttonIcon = if (showTangemIcon) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
@ -101,17 +97,17 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
}
}
AnimatedVisibility(
visible = buttonClick != null,
visible = isButtonDisplayed,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemButton(
text = stringResource(buttonTextId),
icon = buttonIcon,
enabled = isButtonEnabled && buttonClick != null,
enabled = isButtonEnabled && isButtonDisplayed,
onClick = {
if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (buttonClick != null) buttonClick()
onPrimaryClick(uiState)
},
showProgress = isInProgressInnerState,
modifier = Modifier.fillMaxWidth(),
@ -121,37 +117,62 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
}
}
private fun getButtonData(currentState: StakingUiState): Pair<Int, (() -> Unit)?> {
private fun getButtonData(currentState: StakingUiState): Int {
return when (currentState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
val click = if (initialState.isStakeMoreAvailable) {
currentState.clickIntents::onNextClick
} else {
null
}
R.string.staking_stake_more to click
R.string.staking_stake_more
} else {
R.string.common_next to currentState.clickIntents::onNextClick
R.string.common_next
}
}
StakingStep.Amount,
-> R.string.common_next to currentState.clickIntents::onNextClick
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
R.string.common_close to currentState.clickIntents::onBackClick
R.string.common_close
} else {
R.string.common_stake to currentState.clickIntents::onNextClick
R.string.common_stake
}
} else {
R.string.common_close to currentState.clickIntents::onBackClick
R.string.common_close
}
}
StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick
StakingStep.RewardsValidators -> R.string.common_next to null
StakingStep.Validators -> R.string.common_continue
StakingStep.Amount,
StakingStep.RewardsValidators,
-> R.string.common_next
}
}
private fun onPrimaryClick(currentState: StakingUiState) {
when (currentState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
if (initialState.isStakeMoreAvailable) {
currentState.clickIntents.onNextClick()
}
} else {
currentState.clickIntents.onNextClick()
}
}
StakingStep.Amount -> currentState.clickIntents.onNextClick()
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
currentState.clickIntents.onBackClick()
} else {
currentState.clickIntents.onNextClick()
}
} else {
currentState.clickIntents.onBackClick()
}
}
StakingStep.Validators -> currentState.clickIntents.onNextClick()
StakingStep.RewardsValidators -> Unit
}
}
@ -165,12 +186,16 @@ private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) {
-> true
}
private fun isButtonEnabled(uiState: StakingUiState): Boolean {
private fun isButtonEnabled(uiState: StakingUiState): Pair<Boolean, Boolean> {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled
StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled
StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.Validators -> true
StakingStep.InitialInfo -> {
val initialState = uiState.initialInfoState as? StakingStates.InitialInfoState.Data
val isDisplayed = initialState?.isStakeMoreAvailable == true
uiState.initialInfoState.isPrimaryButtonEnabled to isDisplayed
}
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled to true
StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled to true
StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled to false
StakingStep.Validators -> true to true
}
}

View file

@ -1,15 +1,18 @@
package com.tangem.features.staking.impl.presentation.viewmodel
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal interface StakingClickIntents : AmountScreenClickIntents {
fun onBackClick()
fun onNextClick()
fun onNextClick(pendingActions: ImmutableList<PendingAction> = persistentListOf())
fun onPrevClick()

View file

@ -15,6 +15,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.staking.*
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
@ -38,13 +39,14 @@ import com.tangem.features.staking.impl.presentation.state.transformers.validato
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@Suppress("LargeClass", "LongParameterList")
@HiltViewModel
internal class StakingViewModel @Inject constructor(
private val stateController: StakingStateController,
@ -98,11 +100,11 @@ internal class StakingViewModel @Inject constructor(
stakingStateRouter.onBackClick()
}
override fun onNextClick() {
override fun onNextClick(pendingActions: ImmutableList<PendingAction>) {
handleOnNextConfirmationClick()
stakingStateRouter.onNextClick()
if (isAssentState()) {
initStaking()
estimateGas(pendingActions)
}
}
@ -115,16 +117,11 @@ internal class StakingViewModel @Inject constructor(
value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state")
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("No validator provided")
val actionType = when (value.routeType) {
RouteType.STAKE -> StakingActionCommonType.ENTER
RouteType.UNSTAKE -> StakingActionCommonType.EXIT
RouteType.CLAIM -> TODO()
}
val pendingActions = confirmationState.pendingActions
val stakingTransaction = getStakingTransactionUseCase(
params = ActionParams(
actionCommonType = actionType,
actionCommonType = getStakingCommonType(),
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
@ -132,6 +129,8 @@ internal class StakingViewModel @Inject constructor(
?: error("No available address"),
validatorAddress = validatorState.chosenValidator.address,
token = yield.token,
passthrough = pendingActions.firstOrNull()?.passthrough,
type = pendingActions.firstOrNull()?.type,
),
).getOrElse {
error(it)
@ -141,37 +140,35 @@ internal class StakingViewModel @Inject constructor(
sendStakingTransaction(
transactionId = stakingTransaction.id,
gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"),
TransactionData.Compiled(value = it.hexToBytes()),
txData = TransactionData.Compiled(value = it.hexToBytes()),
pendingActions = pendingActions,
)
} ?: error("No unsigned transaction available")
}
}
}
private fun initStaking() {
private fun estimateGas(pendingActions: ImmutableList<PendingAction>) {
viewModelScope.launch {
stateController.update(
SetConfirmationStateLoadingTransformer(
yield = yield,
),
)
val actionType = when (value.routeType) {
RouteType.STAKE -> StakingActionCommonType.ENTER
RouteType.UNSTAKE -> StakingActionCommonType.EXIT
RouteType.CLAIM -> TODO()
}
val cryptoCurrencyValue = cryptoCurrencyStatus.value
val stakingGasEstimate = estimateGasUseCase(
params = ActionParams(
actionCommonType = actionType,
actionCommonType = getStakingCommonType(),
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"),
token = yield.token,
passthrough = pendingActions.firstOrNull()?.passthrough,
type = pendingActions.firstOrNull()?.type,
),
).getOrElse { error("Can't get fee info") }
@ -180,6 +177,7 @@ internal class StakingViewModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
),
)
}
@ -230,9 +228,14 @@ internal class StakingViewModel @Inject constructor(
}
override fun onActiveStake(activeStake: BalanceState) {
stateController.update { it.copy(routeType = RouteType.UNSTAKE) }
val routeType = if (activeStake.pendingActions.isEmpty()) {
RouteType.UNSTAKE
} else {
RouteType.OTHER
}
stateController.update { it.copy(routeType = routeType) }
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue))
onNextClick()
onNextClick(activeStake.pendingActions)
}
override fun onExploreClick() {
@ -314,6 +317,7 @@ internal class StakingViewModel @Inject constructor(
transactionId: String,
gasEstimate: StakingGasEstimate,
txData: TransactionData,
pendingActions: ImmutableList<PendingAction>,
) {
sendTransactionUseCase(
txData = txData,
@ -327,8 +331,10 @@ internal class StakingViewModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = gasEstimate,
pendingActionList = pendingActions,
),
)
// todo add error dialog
},
ifRight = { txHash ->
submitHash(transactionId, txHash)
@ -370,4 +376,12 @@ internal class StakingViewModel @Inject constructor(
(value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState ==
InnerConfirmationStakingState.ASSENT
}
private fun getStakingCommonType() = when (value.routeType) {
RouteType.STAKE -> StakingActionCommonType.ENTER
RouteType.UNSTAKE -> StakingActionCommonType.EXIT
RouteType.CLAIM,
RouteType.OTHER,
-> StakingActionCommonType.PENDING
}
}