Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-11 10:29:08 +05:00
commit ffbda0faf1
16 changed files with 122 additions and 74 deletions

View file

@ -66,6 +66,7 @@ internal class DefaultTransactionRepository(
override suspend fun createApprovalTransaction(
amount: Amount,
approvalAmount: Amount?,
fee: Fee,
contractAddress: String,
spenderAddress: String,
@ -83,7 +84,7 @@ internal class DefaultTransactionRepository(
val approvalData = approver.getApproveData(
spenderAddress = spenderAddress,
value = amount,
value = approvalAmount,
)
val extras = createTransactionDataExtras(

View file

@ -31,6 +31,7 @@ interface TransactionRepository {
@Suppress("LongParameterList")
suspend fun createApprovalTransaction(
amount: Amount,
approvalAmount: Amount?,
fee: Fee,
contractAddress: String,
spenderAddress: String,

View file

@ -16,14 +16,15 @@ class CreateApprovalTransactionUseCase(
suspend operator fun invoke(
cryptoCurrency: CryptoCurrency.Token,
userWalletId: UserWalletId,
amount: BigDecimal,
amount: BigDecimal?,
fee: Fee,
contractAddress: String,
spenderAddress: String,
hash: String? = null,
) = Either.catch {
transactionRepository.createApprovalTransaction(
amount = amount.convertToSdkAmount(cryptoCurrency),
amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrency),
approvalAmount = amount?.convertToSdkAmount(cryptoCurrency),
contractAddress = contractAddress,
spenderAddress = spenderAddress,
userWalletId = userWalletId,

View file

@ -15,6 +15,7 @@ internal sealed class InnerYieldBalanceState {
val rewardsCrypto: String,
val rewardsFiat: String,
val rewardBlockType: RewardBlockType,
val isActionable: Boolean,
val balance: ImmutableList<BalanceState>,
) : InnerYieldBalanceState()

View file

@ -39,7 +39,7 @@ internal class YieldBalancesConverter(
} else {
null
}
val (type, isActionable) = getRewardBlockType()
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoRewardsValue,
@ -50,7 +50,8 @@ internal class YieldBalancesConverter(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
rewardBlockType = getRewardBlockType(),
rewardBlockType = type,
isActionable = isActionable,
balance = balanceToShowItems.mapBalances(),
)
} else {
@ -65,20 +66,21 @@ internal class YieldBalancesConverter(
.sortedBy { it.type.order }
.toPersistentList()
private fun getRewardBlockType(): RewardBlockType {
private fun getRewardBlockType(): Pair<RewardBlockType, Boolean> {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
val isRewardsClaimable = yieldBalance?.balance?.items
val rewards = yieldBalance?.balance?.items
?.filter { it.type == BalanceType.REWARDS }
?.any { it.pendingActions.isNotEmpty() }
?: false
val isActionable = rewards?.all { it.pendingActions.isNotEmpty() } == true
val isRewardsClaimable = rewards?.isNotEmpty() == true
val isSolana = isSolana(cryptoCurrencyStatus.currency.network.id.value)
return when {
isSolana -> RewardBlockType.RewardUnavailable
isRewardsClaimable -> RewardBlockType.Rewards
else -> RewardBlockType.NoRewards
isSolana -> RewardBlockType.RewardUnavailable to false
isRewardsClaimable -> RewardBlockType.Rewards to isActionable
else -> RewardBlockType.NoRewards to false
}
}
}

View file

@ -67,6 +67,7 @@ internal object InitialStakingStatePreview {
rewardsFiat = "100 $",
rewardsCrypto = "100 SOL",
rewardBlockType = RewardBlockType.RewardUnavailable,
isActionable = true,
balance = persistentListOf(
BalanceState(
groupId = "groupId",

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
@ -21,7 +22,8 @@ internal object StakingClickIntentsStub : StakingClickIntents {
pendingAction: PendingAction?,
pendingActions: ImmutableList<PendingAction>?,
balanceState: BalanceState?,
) { }
) {
}
override fun onActionClick() {}
@ -53,6 +55,8 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun showApprovalBottomSheet() {}
override fun onApproveTypeChange(approveType: ApproveType) {}
override fun onApprovalClick() {}
override fun onExploreClick() {}

View file

@ -119,7 +119,8 @@ internal class AddStakingNotificationsTransformer(
isPrimaryButtonEnabled = notifications.none {
it is StakingNotification.Error ||
it is NotificationUM.Error ||
it is NotificationUM.Warning.NetworkFeeUnreachable
it is NotificationUM.Warning.NetworkFeeUnreachable ||
it is StakingNotification.Warning.TransactionInProgress
},
),
)

View file

@ -0,0 +1,23 @@
package com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class SetApprovalBottomSheetTypeChangeTransformer(
private val approveType: ApproveType,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val approvalBottomSheetConfig = prevState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
return prevState.copy(
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
content = approvalBottomSheetConfig?.copy(
data = approvalBottomSheetConfig.data.copy(approveType = approveType),
) as TangemBottomSheetConfigContent,
),
)
}
}

View file

@ -22,6 +22,7 @@ internal object SetApprovalInProgressTransformer : Transformer<StakingUiState> {
val updatedConfirmationState = state?.copy(
notifications = notifications.toPersistentList(),
isPrimaryButtonEnabled = false,
) ?: prevState.confirmationState
return prevState.copy(

View file

@ -72,6 +72,7 @@ internal class ShowApprovalBottomSheetTransformer(
formatArgs = wrappedList(cryptoCurrency.symbol),
),
dialogText = resourceReference(R.string.give_permission_staking_footer),
onChangeApproveType = prevState.clickIntents::onApproveTypeChange,
),
onCancel = onDismiss,
),

View file

@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
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.TransactionDoneState
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
@ -42,6 +43,7 @@ internal fun StakingConfirmationContent(
val isEnterAction = type == StakingActionCommonType.ENTER
val showValidatorBlock = isEnterAction || state.pendingAction?.type == StakingActionType.VOTE_LOCKED
val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED
val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress }
Column(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
@ -61,12 +63,16 @@ internal fun StakingConfirmationContent(
}
AmountBlock(
amountState = amountState,
isClickDisabled = !isEnterAction || isTransactionSent,
isClickDisabled = !isEnterAction || isTransactionSent || isTransactionInProgress,
isEditingDisabled = !isEnterAction && state.innerState != InnerConfirmationStakingState.COMPLETED,
onClick = clickIntents::onPrevClick,
)
if (showValidatorBlock) {
ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators)
ValidatorBlock(
validatorState = state.validatorState,
isClickable = !isTransactionInProgress,
onClick = clickIntents::openValidators,
)
}
StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent)
NotificationsBlock(notifications = state.notifications)

View file

@ -1,7 +1,6 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -9,7 +8,6 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
@ -65,7 +63,7 @@ private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock"
private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock"
private const val STAKE_PRIMARY_BUTTON_KEY = "StakePrimaryButton"
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun StakingInitialInfoContent(
state: StakingStates.InitialInfoState,
@ -104,21 +102,6 @@ internal fun StakingInitialInfoContent(
hideEndText = isBalanceHidden,
)
if (state.yieldBalance is InnerYieldBalanceState.Data) {
item(key = STAKING_REWARD_BLOCK_KEY) {
Column(modifier = Modifier.animateItem()) {
StakingRewardBlock(
rewardCrypto = state.yieldBalance.rewardsCrypto,
rewardFiat = state.yieldBalance.rewardsFiat,
rewardBlockType = state.yieldBalance.rewardBlockType,
onRewardsClick = clickIntents::openRewardsValidators,
isBalanceHidden = isBalanceHidden,
)
SpacerH12()
}
}
}
activeStakingBlock(
state = state,
clickIntents = clickIntents,
@ -144,30 +127,39 @@ private fun LazyListScope.activeStakingBlock(
clickIntents: StakingClickIntents,
isBalanceHidden: Boolean,
) {
if (state.yieldBalance is InnerYieldBalanceState.Data) {
if (state.yieldBalance.balance.isNotEmpty()) {
item(ACTIVE_STAKING_BLOCK_KEY) {
Text(
text = stringResource(id = R.string.staking_your_stakes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = 1 + state.yieldBalance.balance.lastIndex,
addDefaultPadding = false,
)
.fillMaxWidth()
.background(TangemTheme.colors.background.action)
.padding(
top = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing4,
),
val balances = (state.yieldBalance as? InnerYieldBalanceState.Data)?.balance
if (!balances.isNullOrEmpty()) {
item(key = STAKING_REWARD_BLOCK_KEY) {
Column(modifier = Modifier.animateItem()) {
StakingRewardBlock(
yieldBalanceState = state.yieldBalance,
onRewardsClick = clickIntents::openRewardsValidators,
isBalanceHidden = isBalanceHidden,
)
SpacerH12()
}
}
item(ACTIVE_STAKING_BLOCK_KEY) {
Text(
text = stringResource(id = R.string.staking_your_stakes),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = 0,
lastIndex = 1 + state.yieldBalance.balance.lastIndex,
addDefaultPadding = false,
)
.fillMaxWidth()
.background(TangemTheme.colors.background.action)
.padding(
top = TangemTheme.dimens.spacing12,
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing4,
),
)
}
itemsIndexed(
items = state.yieldBalance.balance,
key = { _, balance ->
@ -225,20 +217,18 @@ private fun BannerBlock(onClick: () -> Unit) {
@Composable
private fun StakingRewardBlock(
rewardCrypto: String,
rewardFiat: String,
rewardBlockType: RewardBlockType,
yieldBalanceState: InnerYieldBalanceState.Data,
onRewardsClick: () -> Unit,
isBalanceHidden: Boolean,
) {
val (text, textColor) = when (rewardBlockType) {
val (text, textColor) = when (yieldBalanceState.rewardBlockType) {
RewardBlockType.Rewards -> {
annotatedReference {
append(rewardFiat.orMaskWithStars(isBalanceHidden))
append(yieldBalanceState.rewardsFiat.orMaskWithStars(isBalanceHidden))
appendSpace()
append(DOT)
appendSpace()
append(rewardCrypto.orMaskWithStars(isBalanceHidden))
append(yieldBalanceState.rewardsCrypto.orMaskWithStars(isBalanceHidden))
} to TangemTheme.colors.text.primary1
}
RewardBlockType.RewardUnavailable -> {
@ -249,10 +239,11 @@ private fun StakingRewardBlock(
resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary
}
}
val isShowIcon = yieldBalanceState.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable
InputRowDefault(
title = resourceReference(R.string.staking_rewards),
text = text,
iconRes = R.drawable.ic_chevron_right_24.takeIf { rewardBlockType == RewardBlockType.Rewards },
iconRes = R.drawable.ic_chevron_right_24.takeIf { isShowIcon },
textColor = textColor,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -260,7 +251,7 @@ private fun StakingRewardBlock(
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
enabled = rewardBlockType == RewardBlockType.Rewards,
enabled = yieldBalanceState.isActionable,
onClick = onRewardsClick,
),
)

View file

@ -20,14 +20,14 @@ import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholde
import com.tangem.utils.extensions.orZero
@Composable
internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) {
internal fun ValidatorBlock(validatorState: ValidatorState, isClickable: Boolean, onClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(
enabled = validatorState.isClickable,
enabled = validatorState.isClickable && isClickable,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = onClick,

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.viewmodel
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
@ -51,6 +52,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun showApprovalBottomSheet()
fun onApproveTypeChange(approveType: ApproveType)
fun onApprovalClick()
fun onAmountReduceByClick(

View file

@ -11,6 +11,8 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -59,10 +61,7 @@ import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeTra
import com.tangem.features.staking.impl.presentation.state.helpers.StakingTransactionSender
import com.tangem.features.staking.impl.presentation.state.transformers.*
import com.tangem.features.staking.impl.presentation.state.transformers.amount.*
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.*
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw
import com.tangem.utils.Provider
@ -365,10 +364,16 @@ internal class StakingViewModel @Inject constructor(
override fun onPrevClick() {
if (value.currentStep == StakingStep.Confirmation) {
when ((value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState) {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
when (confirmationState?.innerState) {
InnerConfirmationStakingState.ASSENT -> {
stateController.update(SetConfirmationStateResetAssentTransformer)
stakingStateRouter.onPrevClick()
val isApprovalInProgress = confirmationState.notifications.any {
it is StakingNotification.Warning.TransactionInProgress
}
if (!isApprovalInProgress) {
stakingStateRouter.onPrevClick()
stateController.update(SetConfirmationStateResetAssentTransformer)
}
}
null,
InnerConfirmationStakingState.IN_PROGRESS,
@ -556,6 +561,10 @@ internal class StakingViewModel @Inject constructor(
)
}
override fun onApproveTypeChange(approveType: ApproveType) {
stateController.update(SetApprovalBottomSheetTypeChangeTransformer(approveType))
}
override fun onApprovalClick() {
viewModelScope.launch {
stateController.update(
@ -567,15 +576,17 @@ internal class StakingViewModel @Inject constructor(
val tokenCryptoCurrency =
cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: error("No token currency")
val amountValue = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
?: error("No confirmation state")
val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided")
val approval = stakingApproval as? StakingApproval.Needed ?: error("No staking approve spender address")
val approvalBottomSheetConfig = value.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
val isLimitedApproval = approvalBottomSheetConfig?.data?.approveType == ApproveType.LIMITED
val approvalTransaction = createApprovalTransactionUseCase(
amount = amountValue,
amount = amountValue.takeIf { isLimitedApproval },
contractAddress = tokenCryptoCurrency.contractAddress,
spenderAddress = approval.spenderAddress,
fee = fee,