Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-03 12:22:00 +05:00
parent 2f5b70dfaa
commit 402167c31e
11 changed files with 149 additions and 72 deletions

View file

@ -2,6 +2,7 @@ package com.tangem.features.send.v2.api.entity
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.transaction.error.GetFeeError
@ -17,6 +18,7 @@ sealed class FeeSelectorUM {
data class Error(val error: GetFeeError) : FeeSelectorUM()
data class Content(
val fees: TransactionFee,
val feeItems: ImmutableList<FeeItem>,
val selectedFeeItem: FeeItem,
val isFeeApproximate: Boolean,
@ -37,7 +39,7 @@ data class FeeFiatRateUM(
sealed class FeeItem {
abstract val fee: Fee
fun isSame(other: FeeItem): Boolean {
fun isSameClass(other: FeeItem): Boolean {
return this::class == other::class
}

View file

@ -18,6 +18,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -189,6 +190,7 @@ private fun FeeSelectorBlockContent_Preview() {
displayNonceInput = false,
nonce = null,
onNonceChange = {},
fees = TransactionFee.Single(feeItem.fee),
),
)
}

View file

@ -5,13 +5,11 @@ import com.tangem.features.send.v2.api.entity.FeeItem
internal interface FeeSelectorIntents {
fun onFeeItemSelected(feeItem: FeeItem)
fun onCustomFeeValueChange(index: Int, value: String)
fun onCustomFeeNextClick()
fun onDoneClick()
}
internal class StubFeeSelectorIntents : FeeSelectorIntents {
override fun onFeeItemSelected(feeItem: FeeItem) {}
override fun onCustomFeeValueChange(index: Int, value: String) {}
override fun onCustomFeeNextClick() {}
override fun onDoneClick() {}
}

View file

@ -14,6 +14,7 @@ import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.model.transformers.FeeItemSelectedTransformer
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorCustomValueChangedTransformer
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -89,11 +90,15 @@ internal class FeeSelectorModel @Inject constructor(
}
override fun onCustomFeeValueChange(index: Int, value: String) {
// TODO: [REDACTED_JIRA]
}
override fun onCustomFeeNextClick() {
// TODO: [REDACTED_JIRA]
uiState.update(
FeeSelectorCustomValueChangedTransformer(
index = index,
value = value,
intents = this,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = params.cryptoCurrencyStatus,
),
)
}
override fun onDoneClick() {

View file

@ -4,8 +4,8 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
@ -17,7 +17,7 @@ internal class FeeItemConverter(
private val feeSelectorIntents: FeeSelectorIntents,
private val appCurrency: AppCurrency,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Converter<TransactionFee, ImmutableList<FeeItem>> {
) : Converter<FeeItemConverter.Input, ImmutableList<FeeItem>> {
private val customFeeFieldConverter = FeeSelectorCustomFieldConverter(
feeSelectorIntents = feeSelectorIntents,
@ -26,7 +26,7 @@ internal class FeeItemConverter(
normalFee = normalFee,
)
override fun convert(value: TransactionFee): ImmutableList<FeeItem> {
override fun convert(value: Input): ImmutableList<FeeItem> {
val fees = mutableListOf<FeeItem>()
when (suggestedFeeState) {
@ -38,26 +38,33 @@ internal class FeeItemConverter(
),
)
}
when (value) {
when (value.transactionFee) {
is TransactionFee.Choosable -> {
fees.add(FeeItem.Slow(fee = value.minimum))
fees.add(FeeItem.Market(fee = value.normal))
fees.add(FeeItem.Fast(fee = value.priority))
fees.add(FeeItem.Slow(fee = value.transactionFee.minimum))
fees.add(FeeItem.Market(fee = value.transactionFee.normal))
fees.add(FeeItem.Fast(fee = value.transactionFee.priority))
}
is TransactionFee.Single -> {
fees.add(FeeItem.Market(fee = value.normal))
fees.add(FeeItem.Market(fee = value.transactionFee.normal))
}
}
val customFeeFields = customFeeFieldConverter.convert(normalFee)
if (customFeeFields.isNotEmpty()) {
fees.add(
FeeItem.Custom(
fee = customFeeFieldConverter.convertBack(customFeeFields),
customValues = customFeeFields,
),
)
}
val customFee = value.customFee ?: constructCustomFee()
customFee?.let(fees::add)
return fees.toImmutableList()
}
private fun constructCustomFee(): FeeItem.Custom? {
val customFeeFields = customFeeFieldConverter.convert(normalFee)
if (customFeeFields.isEmpty()) return null
return FeeItem.Custom(
fee = customFeeFieldConverter.convertBack(customFeeFields),
customValues = customFeeFields,
)
}
data class Input(val transactionFee: TransactionFee, val customFee: FeeItem.Custom?)
}

View file

@ -5,12 +5,12 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.utils.converter.TwoWayConverter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -25,7 +25,7 @@ internal class FeeSelectorCustomFieldConverter(
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
EthereumCustomFeeConverter(
onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange,
onNextClick = feeSelectorIntents::onCustomFeeNextClick,
onNextClick = null,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
@ -34,7 +34,7 @@ internal class FeeSelectorCustomFieldConverter(
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
BitcoinCustomFeeConverter(
onCustomFeeValueChange = feeSelectorIntents::onCustomFeeValueChange,
onNextClick = feeSelectorIntents::onCustomFeeNextClick,
onNextClick = null,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
@ -77,38 +77,43 @@ internal class FeeSelectorCustomFieldConverter(
}
}
fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) =
when (val fee = feeSelectorState.fees.normal) {
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
feeValue = fee,
customValues = feeSelectorState.customValues,
index = index,
value = value,
)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
customValues = feeSelectorState.customValues,
index = index,
value = value,
txSize = fee.txSize,
)
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
customValues = feeSelectorState.customValues,
index = index,
value = value,
)
else -> feeSelectorState.customValues
}
fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) {
is TransactionFee.Choosable -> feeSelectorState.fees.minimum
is TransactionFee.Single -> feeSelectorState.fees.normal
}.let {
when (it) {
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
minimumFee = it,
customValues = feeSelectorState.customValues,
)
else -> feeSelectorState.customValues
}
fun onValueChange(
feeSelectorState: FeeSelectorUM.Content,
customValues: ImmutableList<CustomFeeFieldUM>,
index: Int,
value: String,
) = when (val fee = feeSelectorState.fees.normal) {
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
feeValue = fee,
customValues = customValues,
index = index,
value = value,
)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
customValues = customValues,
index = index,
value = value,
txSize = fee.txSize,
)
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
customValues = customValues,
index = index,
value = value,
)
else -> customValues
}
fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content, customValues: ImmutableList<CustomFeeFieldUM>) =
when (val fees = feeSelectorState.fees) {
is TransactionFee.Choosable -> fees.minimum
is TransactionFee.Single -> fees.normal
}.let {
when (it) {
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
minimumFee = it,
customValues = customValues,
)
else -> customValues
}
}
}

View file

@ -0,0 +1,38 @@
package com.tangem.features.send.v2.feeselector.model.transformers
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toImmutableList
internal class FeeSelectorCustomValueChangedTransformer(
private val index: Int,
private val value: String,
private val intents: FeeSelectorIntents,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : Transformer<FeeSelectorUM> {
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
val state = prevState as? FeeSelectorUM.Content ?: return prevState
val customFee = state.feeItems.filterIsInstance<FeeItem.Custom>().firstOrNull() ?: return prevState
val customFeeConverter = FeeSelectorCustomFieldConverter(
feeSelectorIntents = intents,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
normalFee = state.selectedFeeItem.fee,
)
val updatedCustomValues = customFeeConverter.onValueChange(state, customFee.customValues, index, value)
val newCustomFee = customFee.copy(
fee = customFeeConverter.convertBack(updatedCustomValues),
customValues = updatedCustomValues,
)
return state.copy(
feeItems = state.feeItems.map { if (it is FeeItem.Custom) newCustomFee else it }.toImmutableList(),
selectedFeeItem = newCustomFee,
)
}
}

View file

@ -30,16 +30,22 @@ internal class FeeSelectorLoadedTransformer(
)
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(fees)
val prevCustomFee = if (prevState is FeeSelectorUM.Content) {
prevState.feeItems.find { it is FeeItem.Custom } as? FeeItem.Custom
} else {
null
}
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(FeeItemConverter.Input(fees, prevCustomFee))
val selectedFee = when (prevState) {
is FeeSelectorUM.Content -> feeItems.first { it.isSame(prevState.selectedFeeItem) }
is FeeSelectorUM.Content -> feeItems.first { it.isSameClass(prevState.selectedFeeItem) }
is FeeSelectorUM.Error,
FeeSelectorUM.Loading,
-> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market }
}
return FeeSelectorUM.Content(
fees = fees,
feeItems = feeItems,
selectedFeeItem = selectedFee,
isFeeApproximate = isFeeApproximate,

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.atoms.text.EllipsisText
@ -111,7 +112,7 @@ private fun FeeSelectorItems(
Column(modifier = modifier) {
val feeFiatRateUM = state.feeFiatRateUM
state.feeItems.fastForEachIndexed { index, item ->
val isSelected = item.isSame(state.selectedFeeItem)
val isSelected = item.isSameClass(state.selectedFeeItem)
val lastItem = index == state.feeItems.size - 1
val iconTint by animateColorAsState(
targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary,
@ -255,6 +256,7 @@ private fun FeeSelectorItems(
isSelected = isSelected,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
onValueChange = feeSelectorIntents::onCustomFeeValueChange,
displayNonceInput = state.displayNonceInput,
nonce = state.nonce,
onNonceChange = state.onNonceChange,
@ -271,6 +273,7 @@ private fun CustomFeeBlock(
isSelected: Boolean,
iconBackgroundColor: Color,
iconTint: Color,
onValueChange: (Int, String) -> Unit,
displayNonceInput: Boolean,
nonce: BigInteger?,
onNonceChange: (String) -> Unit,
@ -305,7 +308,7 @@ private fun CustomFeeBlock(
) {
ExpandedCustomFeeItems(
customFeeFields = customFee.customValues,
onValueChange = { _, _ -> },
onValueChange = onValueChange,
displayNonceInput = displayNonceInput,
nonce = nonce,
onNonceChange = onNonceChange,
@ -503,6 +506,7 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<
displayNonceInput = true,
onNonceChange = {},
nonce = null,
fees = TransactionFee.Single(customFeeItem.fee),
),
),
)

View file

@ -11,10 +11,10 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.lib.crypto.BlockchainUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -24,7 +24,7 @@ import java.math.RoundingMode
internal class BitcoinCustomFeeConverter(
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
private val onNextClick: (() -> Unit)?,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : CustomFeeConverter<Fee.Bitcoin> {
@ -78,7 +78,13 @@ internal class BitcoinCustomFeeConverter(
},
keyboardType = KeyboardType.Companion.Number,
),
keyboardActions = KeyboardActions(onDone = { onNextClick() }),
keyboardActions = KeyboardActions(
onDone = if (onNextClick != null) {
{ onNextClick() }
} else {
null
},
),
),
)
} else {

View file

@ -18,7 +18,7 @@ import kotlinx.collections.immutable.toImmutableList
internal class EthereumCustomFeeConverter(
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
private val onNextClick: (() -> Unit)?,
private val appCurrency: AppCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : BaseEthereumCustomFeeConverter<Fee.Ethereum> {
@ -112,7 +112,11 @@ internal class EthereumCustomFeeConverter(
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = { onNextClick() },
onDone = if (onNextClick != null) {
{ onNextClick() }
} else {
null
},
),
)
}