Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-18 13:28:32 +03:00
parent 8690dc97da
commit c024ba2836
9 changed files with 109 additions and 40 deletions

View file

@ -101,8 +101,7 @@ fun TangemTokenRow(
.testTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
},
modifier = modifier
.tokenClickable(tokenRowUM = tokenRowUM),
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
)
}

View file

@ -14,7 +14,6 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.transaction.GaslessTransactionRepository
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.raiseIllegalStateError
import java.math.BigDecimal
class GetAvailableFeeTokensUseCase(
private val gaslessTransactionRepository: GaslessTransactionRepository,
@ -88,9 +87,7 @@ class GetAvailableFeeTokensUseCase(
.filter { it.currency.network.id == network.id }
.filter { currencyStatus ->
val token = currencyStatus.currency
token is CryptoCurrency.Token &&
currencyStatus.value.amount?.let { amount -> amount > BigDecimal.ZERO } == true &&
supportedGaslessTokens.contains(token.contractAddress.lowercase())
token is CryptoCurrency.Token && supportedGaslessTokens.contains(token.contractAddress.lowercase())
}
.toList()
}

View file

@ -75,6 +75,7 @@ internal fun FeeSpeedSelectorContent(
start = 16.dp,
end = 16.dp,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Title(onLearnMoreClick = intents::onLearnMoreClick)
SpacerH8()

View file

@ -0,0 +1,8 @@
package com.tangem.features.send.v2.feeselector.component.token.entity
import com.tangem.core.ui.components.token.state.TokenItemState
internal data class FeeTokenItemState(
val state: TokenItemState,
val isAvailable: Boolean = true,
)

View file

@ -1,11 +1,10 @@
package com.tangem.features.send.v2.feeselector.component.token.entity
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import kotlinx.collections.immutable.ImmutableList
data class FeeTokenSelectorUM(
internal data class FeeTokenSelectorUM(
val parent: FeeSelectorUM.Content,
val selectedToken: TokenItemState,
val tokens: ImmutableList<TokenItemState>,
val selectedToken: FeeTokenItemState,
val tokens: ImmutableList<FeeTokenItemState>,
)

View file

@ -8,29 +8,36 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenItemState
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
internal class FeeTokenForListConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val onTokenClick: () -> Unit,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
) : Converter<CryptoCurrencyStatus, FeeTokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
override fun convert(value: CryptoCurrencyStatus): FeeTokenItemState {
if (value.value !is CryptoCurrencyStatus.Loaded) {
return TokenItemState.Loading(id = value.currency.id.value)
return FeeTokenItemState(TokenItemState.Loading(id = value.currency.id.value))
}
val appCurrency = appCurrencyProvider()
val isBalanceZero = value.value.amount?.isZero() == true
val tokenItemConverter = TokenItemStateConverter(
appCurrency = appCurrencyProvider(),
onItemClick = { _, _ -> onTokenClick() },
onItemClick = if (isBalanceZero.not()) {
{ _, _ -> onTokenClick() }
} else {
null
},
)
val state = tokenItemConverter.convert(value)
return when (state) {
val updatedState = when (state) {
is TokenItemState.Content -> {
val balance = value.value.fiatAmount.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
@ -46,10 +53,14 @@ internal class FeeTokenForListConverter(
isAvailable = false,
),
subtitle2State = null,
)
}
else -> state
}
return FeeTokenItemState(
state = updatedState,
isAvailable = isBalanceZero.not(),
)
}
}

View file

@ -7,13 +7,13 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
import com.tangem.features.send.v2.feeselector.component.token.FeeTokenSelectorIntents
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenItemState
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenSelectorUM
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.utils.Provider
@ -78,7 +78,7 @@ internal class FeeTokenSelectorModel @Inject constructor(
)
}
private fun convertToken(status: CryptoCurrencyStatus): TokenItemState {
private fun convertToken(status: CryptoCurrencyStatus): FeeTokenItemState {
return FeeTokenForListConverter(
appCurrencyProvider = Provider { appCurrency },
onTokenClick = { onTokenSelected(status) },

View file

@ -9,8 +9,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
@ -37,10 +39,12 @@ import com.tangem.domain.models.network.Network
import com.tangem.features.send.v2.api.entity.*
import com.tangem.features.send.v2.feeselector.component.token.FeeTokenSelectorIntents
import com.tangem.features.send.v2.feeselector.component.token.StubFeeTokenSelectorIntents
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenItemState
import com.tangem.features.send.v2.feeselector.component.token.entity.FeeTokenSelectorUM
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@Composable
@ -55,6 +59,7 @@ internal fun FeeTokenSelectorContent(
start = 16.dp,
end = 16.dp,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Title(onLearnMoreClick = intents::onLearnMoreClick)
SpacerH8()
@ -101,17 +106,27 @@ private fun Title(modifier: Modifier = Modifier, onLearnMoreClick: () -> Unit) {
)
}
@Suppress("MagicNumber")
@Composable
internal fun TokenListContent(state: FeeTokenSelectorUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
state.tokens.fastForEach { item ->
val (availableTokens, notAvailableTokens) = remember(state.tokens) {
state.tokens.partition { it.isAvailable }.let { pair ->
Pair(
first = pair.first.map { it.state }.toImmutableList(),
second = pair.second.map { it.state }.toImmutableList(),
)
}
}
availableTokens.fastForEach { item ->
key(item.id) {
TokenItem(
modifier = Modifier
.selectedBorder(isSelected = state.selectedToken.id == item.id)
.selectedBorder(isSelected = state.selectedToken.state.id == item.id)
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.action),
state = item,
@ -119,6 +134,28 @@ internal fun TokenListContent(state: FeeTokenSelectorUM, modifier: Modifier = Mo
)
}
}
if (notAvailableTokens.isNotEmpty()) {
Text(
modifier = Modifier.padding(start = 12.dp, top = 8.dp),
text = stringResourceSafe(R.string.common_not_available),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
notAvailableTokens.fastForEach { item ->
key(item.id) {
TokenItem(
modifier = Modifier
.graphicsLayer { alpha = 0.5f }
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.action),
state = item,
isBalanceHidden = false,
)
}
}
}
}
}
@ -209,10 +246,17 @@ private fun Preview() {
onItemLongClick = {},
)
val feeTokenItemState = FeeTokenItemState(tokenItemState)
val state = FeeTokenSelectorUM(
parent = parentUM,
selectedToken = tokenItemState,
tokens = persistentListOf(tokenItemState, tokenItemState.copy(id = "2")),
selectedToken = feeTokenItemState,
tokens = persistentListOf(
feeTokenItemState,
feeTokenItemState.copy(state = tokenItemState.copy(id = "2")),
feeTokenItemState.copy(state = tokenItemState.copy(id = "3"), isAvailable = false),
feeTokenItemState.copy(state = tokenItemState.copy(id = "4"), isAvailable = false),
),
)
TangemThemePreview {

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.feeselector.model
import arrow.core.Either
import arrow.core.flatMap
import arrow.core.getOrElse
import arrow.core.raise.either
import com.tangem.blockchain.common.AmountType
@ -209,25 +210,34 @@ internal class FeeSelectorLogic @AssistedInject constructor(
@Suppress("UnreachableCode")
private suspend fun callLoadFee(): Either<GetFeeError, LoadedFeeResult> {
val extended = params.onLoadFeeExtended
return if (extended != null && sendFeatureToggles.isGaslessTransactionsEnabled) {
val selectedToken = (uiState.value as? FeeSelectorUM.Content)?.feeExtraInfo?.feeCryptoCurrencyStatus
?.takeIf { it.currency is CryptoCurrency.Token }
extended(selectedToken).fold(
ifLeft = { error ->
if (error is GetFeeError.GaslessError) {
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
} else {
Either.Left(error)
}
},
ifRight = { fee ->
populateExtendedFee(fee)
},
)
} else {
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
if (extended == null || !sendFeatureToggles.isGaslessTransactionsEnabled) {
return params.onLoadFee().map { LoadedFeeResult.Basic(it) }
}
val selectedTokenOrNull = (uiState.value as? FeeSelectorUM.Content)?.feeExtraInfo?.feeCryptoCurrencyStatus
if (selectedTokenOrNull?.currency is CryptoCurrency.Coin) {
return params.onLoadFee().flatMap { loadedFee ->
val feeExtended = TransactionFeeExtended(
transactionFee = loadedFee,
feeTokenId = selectedTokenOrNull.currency.id,
)
populateExtendedFee(feeExtended)
}
}
return extended(selectedTokenOrNull).fold(
ifLeft = { error ->
if (error is GetFeeError.GaslessError) {
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
} else {
Either.Left(error)
}
},
ifRight = { fee ->
populateExtendedFee(fee)
},
)
}
private suspend fun populateExtendedFee(