Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-23 18:55:55 +05:00
parent 7cc1bb7379
commit ea30176490
14 changed files with 156 additions and 43 deletions

View file

@ -2,10 +2,12 @@ package com.tangem.common.ui.amountScreen.converters
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
import com.tangem.common.ui.amountScreen.utils.getFiatValue
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
@ -51,6 +53,7 @@ class AmountReduceByTransformer(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
error = resourceReference(R.string.send_validation_amount_exceeds_balance),
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(

View file

@ -2,10 +2,12 @@ package com.tangem.common.ui.amountScreen.converters
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
import com.tangem.common.ui.amountScreen.utils.getFiatValue
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
@ -45,6 +47,7 @@ class AmountReduceToTransformer(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
error = resourceReference(R.string.send_validation_amount_exceeds_balance),
cryptoAmount = amountTextField.cryptoAmount.copy(value = value),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(

View file

@ -2,11 +2,13 @@ package com.tangem.common.ui.amountScreen.converters.field
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.utils.checkExceedBalance
import com.tangem.common.ui.amountScreen.utils.getCryptoValue
import com.tangem.common.ui.amountScreen.utils.getFiatValue
import com.tangem.common.ui.amountScreen.utils.getKeyboardAction
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
@ -59,6 +61,7 @@ class AmountFieldChangeTransformer(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
error = resourceReference(R.string.send_validation_amount_exceeds_balance),
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(

View file

@ -1,16 +1,16 @@
package com.tangem.common.ui.amountScreen.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -80,6 +80,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount
Box(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.padding(
top = TangemTheme.dimens.spacing8,
start = TangemTheme.dimens.spacing12,
@ -105,7 +107,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.align(BottomCenter)
.align(TopCenter)
.padding(bottom = TangemTheme.dimens.spacing32),
)
AmountFieldError(
@ -113,7 +115,10 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
error = amountField.error,
modifier = Modifier
.align(BottomCenter)
.padding(bottom = TangemTheme.dimens.spacing12),
.padding(
top = TangemTheme.dimens.spacing20,
bottom = TangemTheme.dimens.spacing12,
),
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class AddressArgumentDTO(
@ -10,7 +11,7 @@ data class AddressArgumentDTO(
@Json(name = "network")
val network: String? = null,
@Json(name = "minimum")
val minimum: Double? = null,
val minimum: BigDecimal? = null,
@Json(name = "maximum")
val maximum: Double? = null,
val maximum: BigDecimal? = null,
)

View file

@ -39,14 +39,18 @@ class YieldConverter(
private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter {
return Yield.Args.Enter(
addresses = convertAddresses(enterDTO.addresses),
args = enterDTO.args.mapValues { convertAddressArgument(it.value) },
args = enterDTO.args
.mapKeys { convertArgType(it.key) }
.mapValues { convertAddressArgument(it.value) },
)
}
private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses {
return Yield.Args.Enter.Addresses(
address = convertAddressArgument(addressesDTO.address),
additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) },
additionalAddresses = addressesDTO.additionalAddresses
?.mapKeys { convertArgType(it.key) }
?.mapValues { convertAddressArgument(it.value) },
)
}
@ -122,4 +126,12 @@ class YieldConverter(
else -> Yield.RewardType.UNKNOWN
}
}
private fun convertArgType(value: String): Yield.Args.ArgType {
return when (value) {
"address" -> Yield.Args.ArgType.ADDRESS
"amount" -> Yield.Args.ArgType.AMOUNT
else -> Yield.Args.ArgType.UNKNOWN
}
}
}

View file

@ -33,15 +33,21 @@ data class Yield(
@Serializable
data class Enter(
val addresses: Addresses,
val args: Map<String, AddressArgument>,
val args: Map<ArgType, AddressArgument>,
) {
@Serializable
data class Addresses(
val address: AddressArgument,
val additionalAddresses: Map<String, AddressArgument>? = null,
val additionalAddresses: Map<ArgType, AddressArgument>? = null,
)
}
enum class ArgType {
ADDRESS,
AMOUNT,
UNKNOWN,
}
}
@Serializable
@ -113,6 +119,6 @@ data class Token(
data class AddressArgument(
val required: Boolean,
val network: String? = null,
val minimum: Double? = null,
val maximum: Double? = null,
val minimum: SerializedBigDecimal? = null,
val maximum: SerializedBigDecimal? = null,
)

View file

@ -127,7 +127,7 @@ internal class SetInitialDataStateTransformer(
startText = TextReference.Res(R.string.staking_details_minimum_requirement),
endText = TextReference.Str(
value = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = yield.args.enter.args[KEY_AMOUNT]?.minimum?.toBigDecimal(),
cryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum,
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
@ -191,6 +191,5 @@ internal class SetInitialDataStateTransformer(
companion object {
private val EQUALITY_THRESHOLD = BigDecimal(1E-10)
private const val KEY_AMOUNT = "amount"
}
}

View file

@ -1,18 +1,33 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class AmountChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yield: Yield,
private val value: String,
) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) {
AmountRequirementStateTransformer(
cryptoCurrencyStatus,
yield,
value,
)
}
override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldChangeTransformer(
cryptoCurrencyStatus,
value,
).transform(prevState.amountState)
return prevState.copy(
amountState = AmountFieldChangeTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
amountState = amountRequirementStateTransformer.transform(updatedAmountState),
)
}
}

View file

@ -1,16 +1,32 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class AmountMaxValueStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yield: Yield,
) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer by lazy(LazyThreadSafetyMode.NONE) {
val value = cryptoCurrencyStatus.value.amount
?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals)
.orEmpty()
AmountRequirementStateTransformer(
cryptoCurrencyStatus,
yield,
value,
)
}
override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState)
return prevState.copy(
amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState),
amountState = amountRequirementStateTransformer.transform(updatedAmountState),
)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.extensions.isZero
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.utils.transformer.Transformer
internal class AmountRequirementStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yield: Yield,
private val value: String,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals)
return if (prevState !is AmountState.Data || amountRequirements == null) {
prevState
} else {
val isAlreadyErrorState = prevState.amountTextField.isError
val isAmountRequired = amountRequirements.required
val isAmountZero = amountDecimal.isZero()
val isExceedsRequirements =
amountRequirements.maximum?.compareTo(amountDecimal) == -1 ||
amountRequirements.minimum?.compareTo(amountDecimal) == 1
val isRequirementError = !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState
if (isRequirementError) {
prevState.copy(
amountTextField = prevState.amountTextField.copy(
isError = true,
error = resourceReference(
R.string.staking_amount_requirement_error,
wrappedList(
BigDecimalFormatter.formatCryptoAmount(
amountRequirements.minimum,
cryptoCurrencyStatus.currency.symbol,
cryptoCurrencyStatus.currency.decimals,
),
),
),
),
)
} else {
prevState
}
}
}
}

View file

@ -67,7 +67,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl
)
}
}
AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") {
AnimatedContent(targetState = state.yieldBalance, label = "Staking yield visibility animation") {
if (it is InnerYieldBalanceState.Data) {
ActiveStakingBlock(it.balance, clickIntents::onActiveStake)
}
@ -168,6 +168,7 @@ private fun StakingRewardBlock(
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
enabled = isRewardsToClaim,
onClick = onRewardsClick,
),
)
@ -192,18 +193,20 @@ private fun ActiveStakingBlock(groups: List<BalanceGroupedState>, onClick: (Bala
) {
group.items.forEachIndexed { index, balance ->
key(balance.validator.address) {
val caption = combinedReference(
if (group.type == BalanceGroupType.UNSTAKED) {
resourceReference(R.string.staking_details_unbonding_period)
val caption = if (group.type == BalanceGroupType.UNSTAKED) {
combinedReference(
resourceReference(R.string.staking_details_unbonding_period),
annotatedReference {
appendSpace()
appendColored(
text = balance.unbondingPeriod.resolveReference(),
color = TangemTheme.colors.text.accent,
)
}
} else {
resourceReference(R.string.app_name)
},
)
} else {
combinedReference(
resourceReference(R.string.app_name),
annotatedReference {
appendSpace()
appendColored(
@ -213,9 +216,9 @@ private fun ActiveStakingBlock(groups: List<BalanceGroupedState>, onClick: (Bala
),
color = TangemTheme.colors.text.accent,
)
}
},
)
},
)
}
InputRowImageInfo(
title = group.title.takeIf { index == 0 },
subtitle = stringReference(balance.validator.name),

View file

@ -148,17 +148,10 @@ private fun getButtonData(currentState: StakingUiState): Int {
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.InitialInfo,
StakingStep.Validators,
StakingStep.Amount,
-> currentState.clickIntents.onNextClick()
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
@ -171,7 +164,6 @@ private fun onPrimaryClick(currentState: StakingUiState) {
currentState.clickIntents.onBackClick()
}
}
StakingStep.Validators -> currentState.clickIntents.onNextClick()
StakingStep.RewardsValidators -> Unit
}
}

View file

@ -196,7 +196,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onAmountValueChange(value: String) {
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value))
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, value))
}
override fun onAmountPasteTriggerDismiss() {
@ -204,7 +204,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onMaxValueClick() {
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus))
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield))
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
@ -223,7 +223,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun selectRewardValidator(rewardValue: String) {
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue))
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, rewardValue))
onNextClick()
}
@ -234,7 +234,7 @@ internal class StakingViewModel @Inject constructor(
RouteType.OTHER
}
stateController.update { it.copy(routeType = routeType) }
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue))
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue))
onNextClick(activeStake.pendingActions)
}