Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 15:39:32 +05:00
parent 72d9b99348
commit de3bdb3cb0
18 changed files with 138 additions and 49 deletions

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.buttons.common.contentColor
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
@ -73,12 +74,18 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier
} else {
TangemButtonIconPosition.None
}
val color = if (button.isDimmed) {
TangemButtonsDefaults.secondaryButtonColors
.copy(contentColor = TangemTheme.colors.text.tertiary)
} else {
TangemButtonsDefaults.primaryButtonColors
}
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
showProgress = button.showProgress,
colors = TangemButtonsDefaults.primaryButtonColors,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
icon = icon,
modifier = Modifier.fillMaxWidth(),

View file

@ -16,6 +16,17 @@ sealed class NavigationButtonsState {
) : NavigationButtonsState()
}
/**
* @property textReference text
* @property iconRes icon resource id
* @property isSecondary should set secondary color scheme
* @property isIconVisible determines whether icon is visible
* @property showProgress indicates progress state of button
* @property isEnabled enabled
* @property isDimmed determines whether the button content will be dimmed.
* This property will be ignored if [isEnabled] is `false`.
* @property onClick lambda be invoked when action component is clicked
*/
data class NavigationButton(
val textReference: TextReference,
@DrawableRes val iconRes: Int? = null,
@ -23,5 +34,6 @@ data class NavigationButton(
val isIconVisible: Boolean = false,
val showProgress: Boolean = false,
val isEnabled: Boolean = true,
val isDimmed: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -861,6 +861,7 @@
<string name="staking_rewards">Rewards</string>
<string name="staking_stake_locked">Stake locked</string>
<string name="staking_stake_more">Stake more</string>
<string name="staking_stake_more_button_unavailability_reason">When staking %1$s, your entire %2$s balance is staked. Any additional %2$s you deposit to your Tangem Wallet will also be automatically staked.</string>
<string name="staking_staked_amount">Staked amount</string>
<string name="staking_summary_description_text">You stake %1$s and will be receiving your reward %2$s</string>
<string name="staking_tap_to_unlock">Tap to unlock</string>

View file

@ -18,6 +18,7 @@ internal class StakeKitErrorConverter(
message = stakeKitErrorResponse.message,
code = stakeKitErrorResponse.code,
methodName = stakeKitErrorResponse.path,
details = stakeKitErrorResponse.details?.let(StakeKitErrorDetailsConverter::convert),
)
} catch (e: Exception) {
StakingError.StakeKitUnknownError(value)

View file

@ -0,0 +1,15 @@
package com.tangem.data.staking.converters.error
import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorDetailsDTO
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.utils.converter.Converter
internal object StakeKitErrorDetailsConverter :
Converter<StakeKitErrorDetailsDTO, StakingError.StakeKitApiError.ErrorDetails> {
override fun convert(value: StakeKitErrorDetailsDTO): StakingError.StakeKitApiError.ErrorDetails {
return StakingError.StakeKitApiError.ErrorDetails(
amount = value.amount?.toBigDecimalOrNull(),
)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.domain.staking.model.stakekit
import java.math.BigDecimal
sealed class StakingError {
// region stakekit errors
@ -7,8 +9,14 @@ sealed class StakingError {
data class StakeKitApiError(
val message: String?,
val code: Int?,
val details: ErrorDetails?,
val methodName: String?,
) : StakingError()
) : StakingError() {
data class ErrorDetails(
val amount: BigDecimal?,
)
}
data class StakeKitUnknownError(
val jsonString: String? = null,
@ -17,4 +25,13 @@ sealed class StakingError {
// endregion
data class DomainError(val message: String?) : StakingError()
}
sealed class StakingErrors {
abstract val message: String
data object MinimumAmountNotReachedError : StakingErrors() {
override val message: String = "MinimumAmountNotReachedError"
}
}

View file

@ -50,9 +50,8 @@ data class Yield(
val isPartialAmountDisabled: Boolean
get() {
val enterAmount = args[ArgType.AMOUNT] ?: return false
val min = enterAmount.minimum ?: return false
val max = enterAmount.maximum ?: return false
return min.signum() == -1 && max.signum() == -1
return max.signum() == -1
}
}

View file

@ -65,4 +65,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onFailedTxEmailClick(errorMessage: String)
fun openTokenDetails(cryptoCurrency: CryptoCurrency)
fun showPrimaryClickAlert()
}

View file

@ -295,13 +295,13 @@ internal class StakingModel @Inject constructor(
)
updateNotifications()
},
onStakingFeeError = {
stateController.update(AddStakingErrorTransformer())
updateNotifications(GetFeeError.UnknownError)
onStakingFeeError = { stakingFeeError ->
stateController.update(AddStakingErrorTransformer)
updateNotifications(stakingError = stakingFeeError)
},
onFeeError = { error ->
analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError)
stateController.update(AddStakingErrorTransformer())
stateController.update(AddStakingErrorTransformer)
updateNotifications(error)
},
onApprovalFee = { fee ->
@ -646,7 +646,7 @@ internal class StakingModel @Inject constructor(
}.saveIn(approvalJobHolder)
}
private fun updateNotifications(feeError: GetFeeError? = null) {
private fun updateNotifications(feeError: GetFeeError? = null, stakingError: StakingError? = null) {
modelScope.launch {
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
val feeState = confirmationState?.feeState as? FeeState.Content
@ -687,6 +687,7 @@ internal class StakingModel @Inject constructor(
currencyCheck = currencyStatus,
isSubtractAvailable = isAmountSubtractAvailable,
feeError = feeError,
stakingError = stakingError,
yield = yield,
),
)
@ -839,6 +840,14 @@ internal class StakingModel @Inject constructor(
innerRouter.openTokenDetails(userWalletId, cryptoCurrency)
}
override fun showPrimaryClickAlert() {
stateController.updateEvent(
StakingEvent.ShowAlert(
StakingAlertUM.StakeMoreClickUnavailable(cryptoCurrencyStatus.currency),
),
)
}
private suspend fun setupApprovalNeeded() {
stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).fold(
ifRight = { approval ->

View file

@ -20,20 +20,20 @@ internal object StakingNotification {
buttonState = buttonState,
onCloseClick = onCloseClick,
) {
data class StakedPositionNotFoundError(val message: String) : StakingNotification.Error(
data class StakedPositionNotFoundError(val message: String) : Error(
title = stringReference(message),
subtitle = stringReference(message),
)
data class Common(val subtitle: TextReference) : StakingNotification.Error(
data class Common(val subtitle: TextReference) : Error(
title = resourceReference(R.string.common_error),
subtitle = subtitle,
)
data class CardanoMinimumBalance(
data class MinimumAmountNotReachedError(
val title: TextReference,
val subtitle: TextReference,
) : StakingNotification.Error(
) : Error(
title = title,
subtitle = subtitle,
)
@ -54,9 +54,9 @@ internal object StakingNotification {
data class TransactionInProgress(
val title: TextReference,
val description: TextReference,
) : StakingNotification.Warning(title = title, subtitle = description)
) : Warning(title = title, subtitle = description)
data object LowStakedBalance : StakingNotification.Warning(
data object LowStakedBalance : Warning(
title = resourceReference(R.string.staking_notification_low_staked_balance_title),
subtitle = resourceReference(R.string.staking_notification_low_staked_balance_text),
)
@ -75,12 +75,12 @@ internal object StakingNotification {
) {
data class EarnRewards(
val subtitleText: TextReference,
) : StakingNotification.Info(
) : Info(
title = resourceReference(R.string.staking_notification_earn_rewards_title),
subtitle = subtitleText,
)
data object StakeEntireBalance : StakingNotification.Info(
data object StakeEntireBalance : Info(
title = resourceReference(R.string.common_network_fee_title),
subtitle = resourceReference(R.string.staking_notification_stake_entire_balance_text),
)
@ -88,7 +88,7 @@ internal object StakingNotification {
data class Unstake(
val cooldownPeriodDays: Int,
@StringRes val subtitleRes: Int,
) : StakingNotification.Info(
) : Info(
title = resourceReference(R.string.common_unstake),
subtitle = resourceReference(
subtitleRes,
@ -105,7 +105,7 @@ internal object StakingNotification {
data class Ordinary(
val title: TextReference,
val text: TextReference,
) : StakingNotification.Info(
) : Info(
title = title,
subtitle = text,
)

View file

@ -5,6 +5,7 @@ import com.tangem.common.ui.alerts.models.AlertUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.features.staking.impl.R
@Immutable
@ -48,4 +49,16 @@ internal sealed class StakingAlertUM : AlertUM {
override val message: TextReference = resourceReference(id = R.string.staking_error_no_validators_message)
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
}
data class StakeMoreClickUnavailable(
val cryptoCurrency: CryptoCurrency,
) : StakingAlertUM() {
override val onConfirmClick: (() -> Unit)? = null
override val title: TextReference? = null
override val message: TextReference = resourceReference(
id = R.string.staking_stake_more_button_unavailability_reason,
wrappedList(cryptoCurrency.name, cryptoCurrency.symbol),
)
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
}
}

View file

@ -74,4 +74,6 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {}
override fun onActiveStakeAnalytic() {}
override fun showPrimaryClickAlert() {}
}

View file

@ -1,39 +1,20 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.features.staking.impl.presentation.state.FeeState
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.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class AddStakingErrorTransformer(
private val error: StakingError? = null,
) : Transformer<StakingUiState> {
internal object AddStakingErrorTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState =
prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val notifications = buildList {
addAll(confirmationState.notifications)
error?.let { add(convertToNotification(it)) }
}.toPersistentList()
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = notifications,
feeState = FeeState.Error,
),
)
}
private fun convertToNotification(error: StakingError): NotificationUM {
return StakingNotification.Error.Common(
subtitle = stringReference(error.toString()),
)
}
}

View file

@ -47,15 +47,22 @@ internal class SetButtonsStateTransformer(
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
val isIconVisible = isConfirmation && !isCompleted
val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled()
return NavigationButton(
textReference = prevState.getButtonText(),
iconRes = R.drawable.ic_tangem_24,
isSecondary = false,
isDimmed = isPrimaryButtonDisabled,
isIconVisible = isIconVisible,
showProgress = isInProgress,
isEnabled = prevState.isButtonEnabled(),
onClick = { prevState.onPrimaryClick() },
).takeIf { prevState.isPrimaryButtonVisible() }
onClick = {
if (isPrimaryButtonDisabled) {
prevState.clickIntents.showPrimaryClickAlert()
} else {
prevState.onPrimaryClick()
}
},
)
}
private fun getPrevButton(prevState: StakingUiState): NavigationButton? {
@ -186,12 +193,12 @@ internal class SetButtonsStateTransformer(
-> true
}
private fun StakingUiState.isPrimaryButtonVisible(): Boolean {
private fun StakingUiState.isPrimaryButtonDisabled(): Boolean {
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
val hasNotStaking = initialState?.yieldBalance == InnerYieldBalanceState.Empty
val isCardano = BlockchainUtils.isCardano(cryptoCurrencyBlockchainId)
return hasNotStaking || !(isCardano && currentStep == StakingStep.InitialInfo)
return !hasNotStaking && isCardano && currentStep == StakingStep.InitialInfo
}
private fun StakingUiState.isButtonEnabled(): Boolean {

View file

@ -121,7 +121,7 @@ internal class AmountRequirementStateTransformer(
val isExceedsMaxRequirement = if (maximum?.isPositive() == true) {
maximum?.compareTo(amount) == -1
} else {
cryptoCurrencyStatus.value.amount?.compareTo(amount) == 1
cryptoCurrencyStatus.value.amount?.compareTo(amount) == -1
}
val errorText = when {

View file

@ -11,7 +11,10 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionN
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.model.stakekit.StakingErrors
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.tokens.model.CryptoCurrency
@ -39,6 +42,7 @@ internal class AddStakingNotificationsTransformer(
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val currencyWarning: CryptoCurrencyWarning?,
private val feeError: GetFeeError?,
private val stakingError: StakingError?,
private val currencyCheck: CryptoCurrencyCheck,
private val isSubtractAvailable: Boolean,
private val yield: Yield,
@ -93,6 +97,9 @@ internal class AddStakingNotificationsTransformer(
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
addStakingErrorNotifications(
stakingError = stakingError,
)
// warnings
addWarningNotifications(
prevState = prevState,
@ -128,6 +135,22 @@ internal class AddStakingNotificationsTransformer(
)
}
private fun MutableList<NotificationUM>.addStakingErrorNotifications(stakingError: StakingError?) {
when (stakingError) {
is StakingError.StakeKitApiError -> {
if (stakingError.message != StakingErrors.MinimumAmountNotReachedError.message) {
add(
StakingNotification.Error.Common(subtitle = stringReference(stakingError.toString())),
)
}
}
null -> Unit
else -> add(
StakingNotification.Error.Common(subtitle = stringReference(stakingError.toString())),
)
}
}
private fun MutableList<NotificationUM>.addErrorNotifications(
prevState: StakingUiState,
onReload: () -> Unit,
@ -193,7 +216,7 @@ internal class AddStakingNotificationsTransformer(
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
cryptoCurrencyStatus.currency
addRentExemptionNotification(
rentWarning = currencyCheck.rentWarning,

View file

@ -175,7 +175,7 @@ internal class StakingInfoNotificationsFactory(
val balance = cryptoCurrencyStatus.value.amount.orZero()
if (isCardano && balance - feeValue < MINIMUM_STAKE_BALANCE) {
add(
StakingNotification.Error.CardanoMinimumBalance(
StakingNotification.Error.MinimumAmountNotReachedError(
title = resourceReference(R.string.staking_notification_minimum_balance_title),
subtitle = resourceReference(R.string.staking_notification_minimum_stake_ada_text),
),
@ -189,7 +189,7 @@ internal class StakingInfoNotificationsFactory(
val balance = cryptoCurrencyStatus.value.amount.orZero()
if (isCardano && balance - feeValue < MINIMUM_RESTAKE_BALANCE) {
add(
StakingNotification.Error.CardanoMinimumBalance(
StakingNotification.Error.MinimumAmountNotReachedError(
title = resourceReference(R.string.staking_notification_minimum_restake_ada_title),
subtitle = resourceReference(R.string.staking_notification_minimum_restake_ada_text),
),

View file

@ -29,7 +29,7 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t
StakingActionType.REVOTE -> resourceReference(R.string.staking_revote)
StakingActionType.REBOND -> resourceReference(R.string.staking_rebond)
StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate)
StakingActionType.STAKE -> resourceReference(R.string.common_stake)
StakingActionType.STAKE -> resourceReference(R.string.staking_restake)
StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake)
StakingActionType.UNKNOWN -> TextReference.EMPTY
null -> TextReference.EMPTY