Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 14:03:37 +04:00
parent 19fdd19139
commit 078e215ad7
18 changed files with 318 additions and 160 deletions

View file

@ -122,7 +122,7 @@ internal class StakingModel @Inject constructor(
private val shareManager: ShareManager,
@DelayedWork private val coroutineScope: CoroutineScope,
private val innerRouter: InnerStakingRouter,
private val appRouter: AppRouter,
appRouter: AppRouter,
) : Model(), StakingClickIntents {
val uiState: StateFlow<StakingUiState> = stateController.uiState
@ -207,6 +207,7 @@ internal class StakingModel @Inject constructor(
private val transactionsInProgress: CopyOnWriteArrayList<StakingTransaction> = CopyOnWriteArrayList()
private var actionsJobHolder: JobHolder = JobHolder()
private var approvalJobHolder: JobHolder = JobHolder()
private var feeJobHolder: JobHolder = JobHolder()
private var sendTransactionJobHolder = JobHolder()
@ -274,6 +275,7 @@ internal class StakingModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = gasEstimate,
cryptoCurrencyStatus = cryptoCurrencyStatus,
),
)
updateNotifications()
@ -293,6 +295,7 @@ internal class StakingModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = fee,
cryptoCurrencyStatus = cryptoCurrencyStatus,
),
)
updateNotifications()
@ -312,25 +315,26 @@ internal class StakingModel @Inject constructor(
},
onConstructError = { error ->
stakingEventFactory.createStakingErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
},
onSendSuccess = { txUrl ->
stakingAnalyticSender.sendTransactionStakingAnalytics(stateController.value)
transactionsInProgress.clear()
stateController.update(SetConfirmationStateCompletedTransformer(txUrl))
stateController.update(SetConfirmationStateCompletedTransformer(txUrl, cryptoCurrencyStatus))
},
onSendError = { error ->
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stakingEventFactory.createSendTransactionErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus))
},
onFeeIncreased = { increasedFee ->
stateController.updateAll(
SetConfirmationStateResetAssentTransformer,
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus),
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = increasedFee,
cryptoCurrencyStatus = cryptoCurrencyStatus,
),
)
stateController.updateEvent(
@ -355,7 +359,9 @@ internal class StakingModel @Inject constructor(
}
if (!isApprovalInProgress) {
stakingStateRouter.onPrevClick()
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus),
)
}
}
null,
@ -545,6 +551,7 @@ internal class StakingModel @Inject constructor(
stateController.update(SetApprovalBottomSheetTypeChangeTransformer(approveType))
}
@Suppress("LongMethod")
override fun onApprovalClick() {
modelScope.launch {
stateController.update(
@ -581,10 +588,13 @@ internal class StakingModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = TransactionFee.Single(fee),
cryptoCurrencyStatus = cryptoCurrencyStatus,
),
)
stakingEventFactory.createGenericErrorAlert(error.message ?: error.toString())
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus),
)
return@launch
},
ifRight = { it },
@ -603,10 +613,13 @@ internal class StakingModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = TransactionFee.Single(fee),
cryptoCurrencyStatus = cryptoCurrencyStatus,
),
)
stakingEventFactory.createSendTransactionErrorAlert(error)
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus),
)
},
ifRight = {
stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency)
@ -851,10 +864,16 @@ internal class StakingModel @Inject constructor(
},
ifLeft = {
stakingEventFactory.createGenericErrorAlert(it.toString())
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus),
)
},
)
getCurrencyStatusUpdatesUseCase(userWalletId, cryptoCurrencyId, false)
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrencyId,
isSingleWalletWithTokens = false,
)
.conflate()
.distinctUntilChangedBy { it.getOrNull()?.value?.yieldBalance }
.filter { value.currentStep == StakingStep.InitialInfo }
@ -888,12 +907,14 @@ internal class StakingModel @Inject constructor(
setupApprovalNeeded()
setupIsAnyTokenStaked()
checkIfSubtractAvailable()
subscribeOnActionsUpdates()
subscribeOnStepChanges()
subscribeOnActionsUpdates(status)
subscribeOnStepChanges(status)
},
ifLeft = { error ->
stakingEventFactory.createGenericErrorAlert(error.toString())
stateController.update(SetConfirmationStateResetAssentTransformer)
stateController.update(
SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus),
)
},
)
}
@ -923,13 +944,13 @@ internal class StakingModel @Inject constructor(
.launchIn(modelScope)
}
private fun subscribeOnStepChanges() {
private fun subscribeOnStepChanges(status: CryptoCurrencyStatus) {
uiState
.distinctUntilChangedBy { it.currentStep }
.onEach {
when {
isInitState() -> {
updateInitialData()
updateInitialData(status)
balanceUpdater.initialUpdate()
}
isAssentState() -> {
@ -950,32 +971,30 @@ internal class StakingModel @Inject constructor(
.saveIn(stepChangesJobHolder)
}
private fun subscribeOnActionsUpdates() {
getActionsUseCase(
userWalletId = userWalletId,
cryptoCurrencyId = cryptoCurrencyId,
)
private fun subscribeOnActionsUpdates(status: CryptoCurrencyStatus) {
getActionsUseCase(userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrencyId)
.conflate()
.distinctUntilChanged()
.onEach { result ->
result.getOrNull()?.let { actions ->
processingActions = actions
if (isInitState()) {
updateInitialData()
updateInitialData(status)
}
}
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
.saveIn(actionsJobHolder)
}
private fun updateInitialData() {
private fun updateInitialData(status: CryptoCurrencyStatus) {
stateController.updateAll(
SetInitialDataStateTransformer(
clickIntents = this@StakingModel,
yield = yield,
isAnyTokenStaked = isAnyTokenStaked,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
cryptoCurrencyStatus = status,
userWalletProvider = Provider { userWallet },
appCurrencyProvider = Provider { appCurrency },
balancesToShowProvider = Provider { balancesToShow },

View file

@ -21,13 +21,12 @@ import org.joda.time.DateTime
import java.util.Calendar
internal class BalanceItemConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
) : Converter<BalanceItem, BalanceState?> {
override fun convert(value: BalanceItem): BalanceState? {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency

View file

@ -20,13 +20,11 @@ import org.joda.time.DateTime
import java.math.BigDecimal
internal class RewardsValidatorStateConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatus: 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

View file

@ -15,18 +15,17 @@ import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
internal class YieldBalancesConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
private val yield: Yield,
) : Converter<Unit, InnerYieldBalanceState> {
private val balanceItemConverter by lazy(LazyThreadSafetyMode.NONE) {
BalanceItemConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield)
BalanceItemConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
}
override fun convert(value: Unit): InnerYieldBalanceState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
@ -68,7 +67,6 @@ internal class YieldBalancesConverter(
.toPersistentList()
private fun getRewardBlockType(): Pair<RewardBlockType, Boolean> {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val blockchainId = cryptoCurrencyStatus.currency.network.id.value
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
val rewards = yieldBalance?.balance?.items

View file

@ -1,7 +1,7 @@
package com.tangem.features.staking.impl.presentation.state.helpers
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
import com.tangem.domain.staking.FetchActionsUseCase
import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.tokens.FetchPendingTransactionsUseCase

View file

@ -13,6 +13,7 @@ internal class SetConfirmationStateAssentTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: Fee,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -32,7 +33,9 @@ internal class SetConfirmationStateAssentTransformer(
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
isPrimaryButtonEnabled = true,
isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
},
)
} else {
return this

View file

@ -1,12 +1,17 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
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.features.staking.impl.presentation.state.TransactionDoneState
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
internal class SetConfirmationStateCompletedTransformer(
private val txUrl: String,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -18,7 +23,9 @@ internal class SetConfirmationStateCompletedTransformer(
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
return if (this is StakingStates.ConfirmationState.Data) {
copy(
isPrimaryButtonEnabled = true,
isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
},
innerState = InnerConfirmationStakingState.COMPLETED,
footerText = TextReference.EMPTY,
notifications = persistentListOf(),

View file

@ -63,7 +63,9 @@ internal class SetConfirmationStateInitTransformer(
actionType = actionType,
balanceState = balanceState,
confirmationState = StakingStates.ConfirmationState.Data(
isPrimaryButtonEnabled = false,
isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
},
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Loading,
notifications = persistentListOf(),

View file

@ -1,17 +1,22 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
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.transformer.Transformer
internal object SetConfirmationStateResetAssentTransformer : Transformer<StakingUiState> {
internal class SetConfirmationStateResetAssentTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState = prevState.confirmationState
return prevState.copy(
confirmationState = if (confirmationState is StakingStates.ConfirmationState.Data) {
confirmationState.copy(
isPrimaryButtonEnabled = true,
isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
},
innerState = InnerConfirmationStakingState.ASSENT,
)
} else {

View file

@ -5,25 +5,28 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.models.AmountParameters
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.Yield
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.model.StakingClickIntents
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.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType
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.utils.getRewardScheduleText
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
import com.tangem.lib.crypto.BlockchainUtils.isPolkadot
import com.tangem.utils.Provider
import com.tangem.utils.isNullOrZero
@ -37,7 +40,7 @@ internal class SetInitialDataStateTransformer(
private val clickIntents: StakingClickIntents,
private val yield: Yield,
private val isAnyTokenStaked: Boolean,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val userWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val balancesToShowProvider: Provider<List<BalanceItem>>,
@ -46,20 +49,20 @@ internal class SetInitialDataStateTransformer(
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield)
RewardsValidatorStateConverter(cryptoCurrencyStatus, appCurrencyProvider, yield)
}
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalancesConverter(
cryptoCurrencyStatusProvider,
appCurrencyProvider,
balancesToShowProvider,
yield,
cryptoCurrencyStatus = cryptoCurrencyStatus,
appCurrencyProvider = appCurrencyProvider,
balancesToShowProvider = balancesToShowProvider,
yield = yield,
)
}
override fun transform(prevState: StakingUiState): StakingUiState {
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
val cryptoCurrency = cryptoCurrencyStatus.currency
return prevState.copy(
title = TextReference.EMPTY,
cryptoCurrencyName = cryptoCurrency.name,
@ -77,8 +80,12 @@ internal class SetInitialDataStateTransformer(
private fun createInitialInfoState(): StakingStates.InitialInfoState.Data {
val yieldBalance = yieldBalancesConverter.convert(Unit)
val status = cryptoCurrencyStatus.value
return StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(),
isPrimaryButtonEnabled = with(status) {
!amount.isNullOrZero() && sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
},
showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
aprRange = getAprRange(yield.preferredValidators),
infoItems = getInfoItems(),
@ -92,8 +99,6 @@ internal class SetInitialDataStateTransformer(
}
private fun getInfoItems(): PersistentList<RoundedListWithDividersItemData> {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return listOfNotNull(
createAnnualPercentageRateItem(),
createAvailableItem(cryptoCurrencyStatus),
@ -187,7 +192,7 @@ internal class SetInitialDataStateTransformer(
private fun createRewardScheduleItem(): RoundedListWithDividersItemData? {
val endTextReference = getRewardScheduleText(
rewardSchedule = yield.metadata.rewardSchedule,
networkId = cryptoCurrencyStatusProvider().currency.network.id.value,
networkId = cryptoCurrencyStatus.currency.network.id.value,
decapitalize = false,
) ?: return null
@ -200,7 +205,7 @@ internal class SetInitialDataStateTransformer(
}
private fun createInitialAmountState(): AmountState {
val cryptoBalanceValue = cryptoCurrencyStatusProvider().value
val cryptoBalanceValue = cryptoCurrencyStatus.value
val maxEnterAmount = EnterAmountBoundary(
amount = cryptoBalanceValue.amount,
fiatAmount = cryptoBalanceValue.fiatAmount,
@ -208,7 +213,7 @@ internal class SetInitialDataStateTransformer(
)
return AmountStateConverter(
clickIntents = clickIntents,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
appCurrencyProvider = appCurrencyProvider,
iconStateConverter = iconStateConverter,
maxEnterAmount = maxEnterAmount,

View file

@ -14,6 +14,7 @@ internal class SetConfirmationStateAssentApprovalTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: TransactionFee,
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -35,7 +36,9 @@ internal class SetConfirmationStateAssentApprovalTransformer(
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
isPrimaryButtonEnabled = true,
isPrimaryButtonEnabled = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
},
isApprovalNeeded = true,
)
} else {

View file

@ -114,6 +114,10 @@ internal class AddStakingNotificationsTransformer(
)
}.toImmutableList()
val isActualSources = with(cryptoCurrencyStatus.value) {
sources.yieldBalanceSource.isActual() && sources.networkSource.isActual()
}
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = notifications.toImmutableList(),
@ -122,7 +126,7 @@ internal class AddStakingNotificationsTransformer(
it is NotificationUM.Error ||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
it is StakingNotification.Warning.TransactionInProgress
},
} && isActualSources,
),
)
}