Updated on 2026-08-14
This commit is contained in:
parent
7cc1bb7379
commit
ea30176490
14 changed files with 156 additions and 43 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue