Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-08 19:51:04 +04:00
parent f57a7ecac9
commit f6b0bf8dd5
24 changed files with 80 additions and 124 deletions

View file

@ -1,8 +1,10 @@
package com.tangem.common.ui.navigationButtons
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed class NavigationButtonsState {
data object Empty : NavigationButtonsState()

View file

@ -8,11 +8,11 @@ import com.tangem.data.staking.store.StakeKitBalancesStore
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier
import com.tangem.domain.staking.single.SingleStakingBalanceProducer
import com.tangem.domain.staking.single.SingleStakingBalanceSupplier
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -54,7 +54,7 @@ internal object StakingBalanceSupplierModule {
fun provideSingleStakingBalanceSupplier(
factory: SingleStakingBalanceProducer.Factory,
): SingleStakingBalanceSupplier {
return object : SingleStakingBalanceSupplier(
return SingleStakingBalanceSupplier(
factory = factory,
keyCreator = { params ->
listOf(
@ -65,15 +65,15 @@ internal object StakingBalanceSupplierModule {
)
.joinToString(separator = "_")
},
) {}
)
}
@Provides
@Singleton
fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier {
return object : MultiStakingBalanceSupplier(
return MultiStakingBalanceSupplier(
factory = factory,
keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" },
) {}
)
}
}

View file

@ -1,13 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -&gt; raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -&gt; Unit.right() } return@either }</ID>
<ID>MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending &amp;&amp; action.amount &lt; it.amount &amp;&amp; it.type == BalanceType.STAKED &amp;&amp; it.validatorAddress == action.validatorAddress }</ID>
<ID>NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId)</ID>
<ID>UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier</ID>
<ID>UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier</ID>
<ID>UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf()</ID>
<ID>UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: ""</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -23,9 +23,9 @@ class FetchStakingYieldBalanceUseCase(
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
)
.getOrElse {
when (it) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it"))
.getOrElse { error ->
when (error) {
is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$error"))
StakingIdFactory.Error.UnsupportedCurrency -> Unit.right()
}

View file

@ -20,7 +20,12 @@ class GetConstructedStakingTransactionUseCase(
amount: Amount,
transactionId: String,
): Either<StakingError, Pair<StakingTransaction, TransactionData.Compiled>> = Either.catch {
stakeKitRepository.constructTransaction(networkId, fee, amount, transactionId)
stakeKitRepository.constructTransaction(
networkId = networkId,
fee = fee,
amount = amount,
transactionId = transactionId,
)
}.mapLeft {
stakingErrorResolver.resolve(it)
}

View file

@ -100,7 +100,7 @@ class InvalidatePendingTransactionsUseCase(
type = BalanceType.STAKED,
amount = action.amount,
rawCurrencyId = null,
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "",
validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0).orEmpty(),
date = null,
pendingActions = emptyList(),
pendingActionsConstraints = emptyList(),
@ -149,10 +149,10 @@ class InvalidatePendingTransactionsUseCase(
}
private fun findPartialUnstake(balances: MutableList<BalanceItem>, action: StakingAction): Pair<Int, BigDecimal> {
val index = balances.indexOfFirst {
!it.isPending && action.amount < it.amount &&
it.type == BalanceType.STAKED &&
it.validatorAddress == action.validatorAddress
val index = balances.indexOfFirst { balance ->
!balance.isPending && action.amount < balance.amount &&
balance.type == BalanceType.STAKED &&
balance.validatorAddress == action.validatorAddress
}
return index to action.amount
}

View file

@ -8,7 +8,7 @@ import com.tangem.domain.models.staking.action.StakingActionType
sealed class StakingAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(
category = "Staking",
event = event,

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
*
[REDACTED_AUTHOR]
*/
abstract class MultiStakingBalanceSupplier(
open class MultiStakingBalanceSupplier(
override val factory: FlowProducer.Factory<MultiStakingBalanceProducer.Params, MultiStakingBalanceProducer>,
override val keyCreator: (MultiStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<MultiStakingBalanceProducer, MultiStakingBalanceProducer.Params, Set<StakingBalance>>()

View file

@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance
*
[REDACTED_AUTHOR]
*/
abstract class SingleStakingBalanceSupplier(
open class SingleStakingBalanceSupplier(
override val factory: FlowProducer.Factory<SingleStakingBalanceProducer.Params, SingleStakingBalanceProducer>,
override val keyCreator: (SingleStakingBalanceProducer.Params) -> String,
) : FlowCachingSupplier<SingleStakingBalanceProducer, SingleStakingBalanceProducer.Params, StakingBalance>()

View file

@ -1,25 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount &gt; balance</ID>
<ID>BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean</ID>
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean</ID>
<ID>BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean</ID>
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as</ID>
<ID>CastNullableToNonNullableType:SetApprovalBottomSheetTypeChangeTransformer.kt$SetApprovalBottomSheetTypeChangeTransformer$as</ID>
<ID>MultilineLambdaItParameter:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer${ it is StakingNotification.Error || it is NotificationUM.Error || it is NotificationUM.Warning.NetworkFeeUnreachable || it is StakingNotification.Warning.TransactionInProgress || it is StakingNotification.Warning.InitializeTonAccount }</ID>
<ID>MultilineLambdaItParameter:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true isNetwork &amp;&amp; isCurrency }</ID>
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } }</ID>
<ID>MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } }</ID>
<ID>MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED }</ID>
<ID>MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) }</ID>
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId</ID>
<ID>NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId</ID>
<ID>PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState</ID>
<ID>PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState</ID>
<ID>UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -&gt; Unit</ID>
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) }</ID>
<ID>UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -53,9 +53,9 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
params = MultiWalletCryptoCurrenciesProducer.Params(selectedUserWalletId),
)
.orEmpty()
.firstOrNull {
val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
.firstOrNull { currency ->
val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true)
val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true
isNetwork && isCurrency
}
@ -63,8 +63,8 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor(
TangemLogger.e(
"""
Could not get crypto currency for
|- $NETWORK_ID_KEY: $networkId
|- $TOKEN_ID_KEY: $tokenId
|- $NETWORK_ID_KEY: ${networkId.orEmpty()}
|- $TOKEN_ID_KEY: ${tokenId.orEmpty()}
""".trimIndent(),
)
return@launch

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.model
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
@ -11,6 +12,7 @@ import java.math.BigDecimal
// TODO split this interface to click intents and other interaction events
@Suppress("TooManyFunctions")
@Immutable
internal interface StakingClickIntents : AmountScreenClickIntents {
fun onBackClick()

View file

@ -26,19 +26,19 @@ internal class StakingStateController @Inject constructor(
private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles,
) {
val value: StakingUiState get() = uiState.value
private val mutableUiState: MutableStateFlow<StakingUiState> = MutableStateFlow(value = getInitialState())
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
val value: StakingUiState get() = uiState.value
private val buttonsTransformer = SetButtonsStateTransformer(urlOpener)
private val titleTransformer = SetTitleTransformer
fun initializeWithUserWallet(userWallet: UserWallet) {
mutableUiState.update { state ->
state.copy(
showColdWalletInteractionIcon = userWallet.isColdWallet,
isColdWalletInteractionIconVisible = userWallet.isColdWallet,
shouldShowHoldToConfirmButton = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled &&
userWallet.isHotWallet,
)
@ -89,7 +89,7 @@ internal class StakingStateController @Inject constructor(
actionType = StakingActionCommonType.Enter(skipEnterAmount = false),
buttonsState = NavigationButtonsState.Empty,
balanceState = null,
showColdWalletInteractionIcon = true,
isColdWalletInteractionIconVisible = true,
shouldShowHoldToConfirmButton = false,
)
}

View file

@ -39,22 +39,9 @@ internal data class StakingUiState(
val actionType: StakingActionCommonType,
val buttonsState: NavigationButtonsState,
val balanceState: BalanceState?,
val showColdWalletInteractionIcon: Boolean,
val isColdWalletInteractionIconVisible: Boolean,
val shouldShowHoldToConfirmButton: Boolean,
) {
fun copyWrapped(
initialInfoState: StakingStates.InitialInfoState = this.initialInfoState,
amountState: AmountState = this.amountState,
confirmationState: StakingStates.ConfirmationState = this.confirmationState,
validatorState: StakingStates.ValidatorState = this.validatorState,
): StakingUiState = copy(
initialInfoState = initialInfoState,
amountState = amountState,
confirmationState = confirmationState,
validatorState = validatorState,
)
}
)
internal sealed class StakingStates {
@ -64,7 +51,7 @@ internal sealed class StakingStates {
sealed class InitialInfoState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
val showBanner: Boolean,
val isBannerVisible: Boolean,
val infoItems: ImmutableList<RoundedListWithDividersItemData>,
val onInfoClick: (InfoType) -> Unit,
val yieldBalance: InnerYieldBalanceState,

View file

@ -18,7 +18,7 @@ import kotlinx.collections.immutable.persistentListOf
internal object InitialStakingStatePreview {
val defaultState = StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = true,
showBanner = true,
isBannerVisible = true,
infoItems = persistentListOf(
RoundedListWithDividersItemData(
id = R.string.staking_details_available,

View file

@ -37,7 +37,7 @@ internal class SetButtonsStateTransformer(
return prevState.copy(buttonsState = buttonsState)
}
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton? {
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val innerConfirmState = confirmState?.innerState
@ -52,7 +52,7 @@ internal class SetButtonsStateTransformer(
val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled()
return NavigationButton(
textReference = prevState.getButtonText(),
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon },
iconRes = R.drawable.ic_tangem_24.takeIf { prevState.isColdWalletInteractionIconVisible },
isDimmed = isPrimaryButtonDisabled,
isIconVisible = isIconVisible,
shouldShowProgress = isInProgress,

View file

@ -96,7 +96,7 @@ internal class SetInitialDataStateTransformer(
isPrimaryButtonEnabled = with(status) {
!amount.isNullOrZero() && sources.stakingBalanceSource.isActual() && sources.networkSource.isActual()
},
showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
isBannerVisible = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty,
infoItems = getInfoItems(),
onInfoClick = clickIntents::onInfoClick,
yieldBalance = yieldBalance,

View file

@ -7,11 +7,11 @@ import com.tangem.utils.transformer.Transformer
internal class AmountCurrencyChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val value: Boolean,
private val isFiatValue: Boolean,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, isFiatValue).transform(prevState.amountState),
)
}
}

View file

@ -27,7 +27,7 @@ internal class SetApprovalBottomSheetInProgressTransformer(
),
onCancel = onDismiss,
)
} as TangemBottomSheetConfigContent,
} as? TangemBottomSheetConfigContent ?: return prevState,
),
)
}

View file

@ -16,7 +16,7 @@ internal class SetApprovalBottomSheetTypeChangeTransformer(
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
content = approvalBottomSheetConfig?.copy(
data = approvalBottomSheetConfig.data.copy(approveType = approveType),
) as TangemBottomSheetConfigContent,
) as? TangemBottomSheetConfigContent ?: return prevState,
),
)
}

View file

@ -177,12 +177,12 @@ internal class AddStakingNotificationsTransformer(
}
private fun isPrimaryButtonEnabled(notifications: ImmutableList<NotificationUM>, isActualSources: Boolean) =
notifications.none {
it is StakingNotification.Error ||
it is NotificationUM.Error ||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
it is StakingNotification.Warning.TransactionInProgress ||
it is StakingNotification.Warning.InitializeTonAccount
notifications.none { notification ->
notification is StakingNotification.Error ||
notification is NotificationUM.Error ||
notification is NotificationUM.Warning.NetworkFeeUnreachable ||
notification is StakingNotification.Warning.TransactionInProgress ||
notification is StakingNotification.Warning.InitializeTonAccount
} && isActualSources
private fun MutableList<NotificationUM>.addStakingErrorNotifications(
@ -302,8 +302,8 @@ internal class AddStakingNotificationsTransformer(
val balance = cryptoCurrencyStatus.value.amount.orZero()
if (!isSubtractionAvailable) return
val showNotification = sendingAmount + feeAmount > balance
if (showNotification) {
val isExceedsBalance = sendingAmount + feeAmount > balance
if (isExceedsBalance) {
onNotEnoughFeeNotificationShow()
val notification = if (actionType is StakingActionCommonType.Enter) {
NotificationUM.Error.TotalExceedsBalance

View file

@ -150,10 +150,10 @@ internal class StakingInfoNotificationsFactory(
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isTron = isTron(cryptoCurrencyStatus.currency.network.rawId)
val hasStakedBalance = (cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit)?.balance
?.items?.any {
it.type == BalanceType.PREPARING ||
it.type == BalanceType.STAKED ||
it.type == BalanceType.LOCKED
?.items?.any { item ->
item.type == BalanceType.PREPARING ||
item.type == BalanceType.STAKED ||
item.type == BalanceType.LOCKED
} == true
if (isTron && hasStakedBalance) {
add(

View file

@ -87,7 +87,7 @@ internal fun StakingInitialInfoContent(
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
if (state.showBanner) {
if (state.isBannerVisible) {
item(key = BANNER_BLOCK_KEY) {
Column(
modifier = Modifier.animateItem(),
@ -175,7 +175,7 @@ private fun LazyListScope.activeStakingBlock(
ActiveStakingBlock(
balance = balance,
isBalanceHidden = isBalanceHidden,
onClick = clickIntents::onActiveStake,
onClick = { clickIntents.onActiveStake(balance) },
onAnalytic = clickIntents::onActiveStakeAnalytic,
modifier = Modifier
.animateItem()
@ -288,7 +288,7 @@ private fun StakingRewardBlock(
private fun ActiveStakingBlock(
balance: BalanceState,
isBalanceHidden: Boolean,
onClick: (BalanceState) -> Unit,
onClick: () -> Unit,
onAnalytic: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -304,7 +304,7 @@ private fun ActiveStakingBlock(
enabled = balance.isClickable,
onClick = {
onAnalytic()
onClick(balance)
onClick()
},
)
.padding(TangemTheme.dimens.spacing12),
@ -351,7 +351,6 @@ private fun ActiveStakingBlock(
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
if (balance.formattedCryptoAmount != null) {
Text(
text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(),
style = TangemTheme.typography.caption2,
@ -360,11 +359,10 @@ private fun ActiveStakingBlock(
)
}
}
}
}
@Composable
private fun RowScope.StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
private fun StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) {
if (balance.hasImage() || icon != null) {
StakingTargetIcon(
image = if (balance.hasImage()) balance.target?.image.toImageReference() else null,

View file

@ -109,8 +109,8 @@ private fun BoxScope.FeeLoading(feeState: FeeState) {
targetState = feeState,
label = "Fee Loading State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == FeeState.Loading) {
) { state ->
if (state == FeeState.Loading) {
RectangleShimmer(
radius = TangemTheme.dimens.radius3,
modifier = Modifier.size(
@ -128,8 +128,8 @@ private fun BoxScope.FeeError(feeState: FeeState) {
targetState = feeState,
label = "Fee Error State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == FeeState.Error) {
) { state ->
if (state == FeeState.Error) {
Text(
text = DASH_SIGN,
color = TangemTheme.colors.text.primary1,
@ -151,13 +151,6 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va
private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
override val values: Sequence<FeeState>
get() = sequenceOf(
contentState,
FeeState.Loading,
FeeState.Error,
)
private val fee = Fee.Common(
amount = Amount(
currencySymbol = "MATIC",
@ -174,6 +167,13 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState> {
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
)
override val values: Sequence<FeeState>
get() = sequenceOf(
contentState,
FeeState.Loading,
FeeState.Error,
)
}
// endregion