Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-06 17:19:24 +03:00
parent f81e51f4c4
commit 3baec6cc7e
27 changed files with 580 additions and 22 deletions

View file

@ -17,6 +17,7 @@ import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -323,4 +324,12 @@ internal object TransactionDomainModule {
getMultiCryptoCurrencyStatusUseCase = getMultiCryptoCurrencyStatusUseCase,
)
}
@Provides
@Singleton
fun provideIsGaslessFeeSupportedForNetwork(
gaslessTransactionRepository: GaslessTransactionRepository,
): IsGaslessFeeSupportedForNetwork {
return IsGaslessFeeSupportedForNetwork(gaslessTransactionRepository)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -52,6 +53,10 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
is TokenFiatAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
is TokenFiatAmountState.Empty -> {
// Empty box for proper measurements
Box(modifier)
}
null -> Unit
}
}

View file

@ -243,6 +243,8 @@ sealed class TokenItemState {
data object Loading : FiatAmountState()
data object Locked : FiatAmountState()
data object Empty : FiatAmountState()
}
@Immutable

View file

@ -0,0 +1,16 @@
package com.tangem.domain.transaction.usecase.gasless
import com.tangem.domain.models.network.Network
import com.tangem.domain.transaction.GaslessTransactionRepository
/**
* Use case to check if gasless fee is supported for a given network.
*/
class IsGaslessFeeSupportedForNetwork(
private val gaslessTransactionRepository: GaslessTransactionRepository,
) {
operator fun invoke(network: Network): Boolean {
return gaslessTransactionRepository.isNetworkSupported(network)
}
}

View file

@ -63,6 +63,7 @@ data class FeeExtraInfo(
val isFeeConvertibleToFiat: Boolean,
val isTronToken: Boolean,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
val availableFeeCurrencies: ImmutableList<CryptoCurrencyStatus>? = null,
)
sealed class FeeNonce {

View file

@ -5,6 +5,7 @@ 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.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents
@ -13,6 +14,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM
sealed class FeeSelectorParams {
abstract val state: FeeSelectorUM
abstract val userWalletId: UserWalletId
abstract val onLoadFeeExtended: (suspend () -> Either<GetFeeError, TransactionFeeExtended>)?
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
@ -24,6 +26,7 @@ sealed class FeeSelectorParams {
data class FeeSelectorBlockParams(
override val state: FeeSelectorUM,
override val userWalletId: UserWalletId,
override val onLoadFeeExtended: (suspend () -> Either<GetFeeError, TransactionFeeExtended>)? = null,
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
@ -36,6 +39,7 @@ sealed class FeeSelectorParams {
data class FeeSelectorDetailsParams(
override val state: FeeSelectorUM,
override val userWalletId: UserWalletId,
override val onLoadFeeExtended: (suspend () -> Either<GetFeeError, TransactionFeeExtended>)? = null,
override val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,

View file

@ -54,6 +54,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWalletId,
),
onDismiss = {
model.feeSelectorBottomSheet.dismiss()

View file

@ -17,6 +17,7 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
import com.tangem.features.send.v2.feeselector.component.extended.FeeExtendedSelectorComponent
import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorComponent
import com.tangem.features.send.v2.feeselector.component.token.FeeTokenSelectorComponent
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet
@ -28,8 +29,9 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
@Assisted private val onDismiss: () -> Unit,
private val feeExtendedSelectorComponent: FeeExtendedSelectorComponent.Factory,
private val feeExtendedSelectorComponentFactory: FeeExtendedSelectorComponent.Factory,
private val feeSpeedSelectorComponentFactory: FeeSpeedSelectorComponent.Factory,
private val feeTokenSelectorComponentFactory: FeeTokenSelectorComponent.Factory,
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
private val model: FeeSelectorModel = getOrCreateModel(params = params)
@ -86,7 +88,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
)
return when (config) {
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponent.create(
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
@ -94,7 +96,10 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
appComponentContext = appComponentContext,
params = componentParams,
)
FeeSelectorRoute.ChooseToken -> TODO()
FeeSelectorRoute.ChooseToken -> feeTokenSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
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.extended.entity.FeeExtendedSelectorUM
import com.tangem.features.send.v2.feeselector.component.extended.entity.SelectedTokenItemConverter
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.feeselector.component.extended.entity
package com.tangem.features.send.v2.feeselector.component.extended.model
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.icons.IconTint

View file

@ -0,0 +1,42 @@
package com.tangem.features.send.v2.feeselector.component.token
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
import com.tangem.features.send.v2.feeselector.component.token.model.FeeTokenSelectorModel
import com.tangem.features.send.v2.feeselector.component.token.ui.FeeTokenSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class FeeTokenSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: FeeSelectorComponentParams,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: FeeTokenSelectorModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
FeeTokenSelectorContent(
state = state,
intents = model,
modifier = modifier,
)
}
@AssistedFactory
interface Factory {
fun create(
appComponentContext: AppComponentContext,
params: FeeSelectorComponentParams,
): FeeTokenSelectorComponent
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.send.v2.feeselector.component.token
import androidx.compose.runtime.Stable
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents
@Stable
internal interface FeeTokenSelectorIntents : FeeSelectorIntents {
fun onLearnMoreClick()
}
@Stable
internal class StubFeeTokenSelectorIntents : FeeTokenSelectorIntents, FeeSelectorIntents by StubFeeSelectorIntents() {
override fun onLearnMoreClick() {}
}

View file

@ -0,0 +1,11 @@
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(
val parent: FeeSelectorUM.Content,
val selectedToken: TokenItemState,
val tokens: ImmutableList<TokenItemState>,
)

View file

@ -0,0 +1,55 @@
package com.tangem.features.send.v2.feeselector.component.token.model
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
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.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
internal class FeeTokenForListConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val onTokenClick: () -> Unit,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
if (value.value !is CryptoCurrencyStatus.Loaded) {
return TokenItemState.Loading(id = value.currency.id.value)
}
val appCurrency = appCurrencyProvider()
val tokenItemConverter = TokenItemStateConverter(
appCurrency = appCurrencyProvider(),
onItemClick = { _, _ -> onTokenClick() },
)
val state = tokenItemConverter.convert(value)
return when (state) {
is TokenItemState.Content -> {
val balance = value.value.fiatAmount.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
}
state.copy(
fiatAmountState = TokenItemState.FiatAmountState.Empty,
subtitleState = TokenItemState.SubtitleState.TextContent(
value = resourceReference(
R.string.common_balance,
wrappedList(balance),
),
isAvailable = false,
),
subtitle2State = null,
)
}
else -> state
}
}
}

View file

@ -0,0 +1,93 @@
package com.tangem.features.send.v2.feeselector.component.token.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.TangemBlogUrlBuilder
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.FeeTokenSelectorUM
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class FeeTokenSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model(), FeeTokenSelectorIntents,
FeeSelectorIntents by paramsContainer.require<FeeSelectorComponentParams>().intents {
private val params = paramsContainer.require<FeeSelectorComponentParams>()
private var appCurrency = AppCurrency.Default
val uiState: StateFlow<FeeTokenSelectorUM>
field = MutableStateFlow(getInitialState())
init {
initAppCurrency()
params.state
.onEach { uiState.value = stateFromParent(it as FeeSelectorUM.Content) }
.launchIn(modelScope)
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun getInitialState(): FeeTokenSelectorUM {
val parentState = params.state.value as FeeSelectorUM.Content
return stateFromParent(parentState)
}
private fun stateFromParent(newParentState: FeeSelectorUM.Content): FeeTokenSelectorUM {
val parentState = newParentState
val selectedCurrencyStatus = parentState.feeExtraInfo.feeCryptoCurrencyStatus
val selectedTokenUM = convertToken(selectedCurrencyStatus)
return FeeTokenSelectorUM(
parent = parentState,
selectedToken = selectedTokenUM,
tokens = parentState.feeExtraInfo.availableFeeCurrencies?.map { status ->
convertToken(status)
}?.toImmutableList() ?: persistentListOf(selectedTokenUM),
)
}
private fun convertToken(status: CryptoCurrencyStatus): TokenItemState {
return FeeTokenForListConverter(
appCurrencyProvider = Provider { appCurrency },
onTokenClick = { onTokenSelected(status) },
).convert(status)
}
override fun onLearnMoreClick() {
modelScope.launch {
urlOpener.openUrl(
TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.WhatIsTransactionFee),
)
}
}
}

View file

@ -0,0 +1,226 @@
package com.tangem.features.send.v2.feeselector.component.token.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
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.core.ui.components.SpacerH8
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.selectedBorder
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
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.FeeTokenSelectorUM
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@Composable
internal fun FeeTokenSelectorContent(
state: FeeTokenSelectorUM,
intents: FeeTokenSelectorIntents,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.padding(
bottom = 16.dp,
start = 16.dp,
end = 16.dp,
),
) {
Title(onLearnMoreClick = intents::onLearnMoreClick)
SpacerH8()
TokenListContent(state)
}
}
@Composable
private fun Title(modifier: Modifier = Modifier, onLearnMoreClick: () -> Unit) {
val linkText = stringResourceSafe(R.string.common_learn_more)
val fullString = stringResourceSafe(R.string.fee_selector_choose_token_description, linkText)
val linkTextPosition = fullString.length - linkText.length
val defaultColor = TangemTheme.colors.text.secondary
val linkColor = TangemTheme.colors.text.accent
val annotatedString = remember(defaultColor, linkColor, onLearnMoreClick) {
buildAnnotatedString {
withStyle(SpanStyle(defaultColor)) {
append(fullString.substring(0, linkTextPosition))
}
withLink(
link = LinkAnnotation.Clickable(
tag = "learn_more",
linkInteractionListener = { onLearnMoreClick() },
),
block = {
withStyle(SpanStyle(color = linkColor, textDecoration = TextDecoration.None)) {
append(
fullString.substring(linkTextPosition, fullString.length).replace(
' ',
StringsSigns.NON_BREAKING_SPACE,
),
)
}
},
)
}
}
Text(
modifier = modifier.padding(horizontal = 32.dp),
text = annotatedString,
style = TangemTheme.typography.caption2,
textAlign = TextAlign.Center,
)
}
@Composable
internal fun TokenListContent(state: FeeTokenSelectorUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
state.tokens.fastForEach { item ->
key(item.id) {
TokenItem(
modifier = Modifier
.selectedBorder(isSelected = state.selectedToken.id == item.id)
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.action),
state = item,
isBalanceHidden = false,
)
}
}
}
}
@Suppress("LongMethod")
@Preview
@Composable
private fun Preview() {
val cryptoCurrencyStatus = CryptoCurrencyStatus(
currency = CryptoCurrency.Coin(
id = CryptoCurrency.ID.fromValue("coin⟨BITCOIN⟩bitcoin"),
network = Network(
id = Network.ID(
value = "bitcoin",
derivationPath = Network.DerivationPath.None,
),
backendId = "bitcoin",
name = "Bitcoin",
currencySymbol = "BTC",
derivationPath = Network.DerivationPath.None,
isTestnet = false,
standardType = Network.StandardType.Unspecified("bitcoin"),
hasFiatFeeRate = false,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "Bitcoin",
symbol = "BTC",
decimals = 8,
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/medium/bitcoin.png",
isCustom = false,
),
value = CryptoCurrencyStatus.Loading,
)
val parentUM = FeeSelectorUM.Content(
isPrimaryButtonEnabled = false,
feeItems = persistentListOf(
FeeItem.Market(
fee = Fee.Common(
Amount(
value = BigDecimal("0.02"),
blockchain = Blockchain.Ethereum,
),
),
),
),
selectedFeeItem = FeeItem.Slow(
fee = Fee.Common(
Amount(
value = BigDecimal("0.01"),
blockchain = Blockchain.Ethereum,
),
),
),
feeExtraInfo = FeeExtraInfo(
isFeeApproximate = true,
isFeeConvertibleToFiat = true,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal.TEN,
appCurrency = AppCurrency.Default,
),
feeNonce = FeeNonce.None,
fees = TransactionFee.Single(
Fee.Common(
Amount(
value = BigDecimal("0.01"),
blockchain = Blockchain.Ethereum,
),
),
),
)
val tokenItemState = TokenItemState.Content(
id = "1",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
fiatAmountState = TokenItemState.FiatAmountState.Empty,
subtitle2State = null,
subtitleState = TokenItemState.SubtitleState.TextContent(
value = stringReference("Balance: 0,35853044 BTC"),
isAvailable = false,
),
onItemClick = {},
onItemLongClick = {},
)
val state = FeeTokenSelectorUM(
parent = parentUM,
selectedToken = tokenItemState,
tokens = persistentListOf(tokenItemState, tokenItemState.copy(id = "2")),
)
TangemThemePreview {
FeeTokenSelectorContent(
state = state,
intents = StubFeeTokenSelectorIntents(),
modifier = Modifier
.background(TangemTheme.colors.background.tertiary),
)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.feeselector.component.extended.model.FeeExtendedSelectorModel
import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel
import com.tangem.features.send.v2.feeselector.component.speed.model.FeeSpeedSelectorModel
import com.tangem.features.send.v2.feeselector.component.token.model.FeeTokenSelectorModel
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
import dagger.Binds
import dagger.Module
@ -33,5 +34,10 @@ internal interface FeeSelectorModelModule {
@Binds
@IntoMap
@ClassKey(FeeSpeedSelectorModel::class)
fun bindFeeSelectorSpeedModel(model: FeeSpeedSelectorModel): Model
fun bindFeeSpeedSelectorModel(model: FeeSpeedSelectorModel): Model
@Binds
@IntoMap
@ClassKey(FeeTokenSelectorModel::class)
fun bindFeeTokenSelectorModel(model: FeeTokenSelectorModel): Model
}

View file

@ -1,11 +1,13 @@
package com.tangem.features.send.v2.feeselector.model
import androidx.compose.runtime.Stable
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeItem
@Stable
internal interface FeeSelectorIntents {
fun onFeeItemSelected(feeItem: FeeItem)
fun onTokenSelected(status: CryptoCurrencyStatus)
fun onCustomFeeValueChange(index: Int, value: String)
fun onNonceChange(value: String)
fun onDoneClick()
@ -13,6 +15,7 @@ internal interface FeeSelectorIntents {
internal class StubFeeSelectorIntents : FeeSelectorIntents {
override fun onFeeItemSelected(feeItem: FeeItem) {}
override fun onTokenSelected(status: CryptoCurrencyStatus) {}
override fun onCustomFeeValueChange(index: Int, value: String) {}
override fun onNonceChange(value: String) {}
override fun onDoneClick() {}

View file

@ -2,6 +2,7 @@ package com.tangem.features.send.v2.feeselector.model
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.raise.either
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -12,6 +13,8 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted
import com.tangem.features.send.v2.api.entity.FeeItem
@ -32,6 +35,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@Suppress("LongParameterList")
@ -47,6 +51,8 @@ internal class FeeSelectorLogic @AssistedInject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getAvailableFeeTokensUseCase: GetAvailableFeeTokensUseCase,
) : FeeSelectorIntents {
private var appCurrency: AppCurrency = AppCurrency.Default
@ -105,6 +111,20 @@ internal class FeeSelectorLogic @AssistedInject constructor(
uiState.update(FeeItemSelectedTransformer(feeItem))
}
override fun onTokenSelected(status: CryptoCurrencyStatus) {
uiState.update { state ->
if (state is FeeSelectorUM.Content) {
state.copy(
feeExtraInfo = state.feeExtraInfo.copy(
feeCryptoCurrencyStatus = status,
),
)
} else {
state
}
}
}
override fun onCustomFeeValueChange(index: Int, value: String) {
uiState.update(
FeeSelectorCustomValueChangedTransformer(
@ -208,20 +228,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
}
},
ifRight = { fee ->
Either.Right(
LoadedFeeResult.Extended(
fee = fee,
selectedToken = if (params.feeCryptoCurrencyStatus.currency.id != fee.feeTokenId) {
getSingleCryptoCurrencyStatusUseCase
.invokeMultiWalletSync(
userWalletId = TODO(),
cryptoCurrencyId = fee.feeTokenId,
).getOrNull()
} else {
params.feeCryptoCurrencyStatus
},
),
)
populateExtendedFee(fee)
},
)
} else {
@ -229,10 +236,42 @@ internal class FeeSelectorLogic @AssistedInject constructor(
}
}
private suspend fun populateExtendedFee(
fee: TransactionFeeExtended,
): Either<GetFeeError, LoadedFeeResult.Extended> = either {
val selectedToken = if (params.feeCryptoCurrencyStatus.currency.id != fee.feeTokenId) {
getSingleCryptoCurrencyStatusUseCase
.invokeMultiWalletSync(
userWalletId = params.userWalletId,
cryptoCurrencyId = fee.feeTokenId,
).getOrElse {
raise(GetFeeError.DataError(IllegalStateException("No token found for id: ${fee.feeTokenId}")))
}
} else {
params.feeCryptoCurrencyStatus
}
val userWallet = getUserWalletUseCase(params.userWalletId).mapLeft {
GetFeeError.DataError(IllegalStateException("No wallet found for id: ${params.userWalletId}"))
}.bind()
val availableTokens = getAvailableFeeTokensUseCase.invoke(
userWallet = userWallet,
network = params.cryptoCurrencyStatus.currency.network,
).bind()
LoadedFeeResult.Extended(
fee = fee,
selectedToken = selectedToken,
availableTokens = availableTokens,
)
}
sealed class LoadedFeeResult {
data class Extended(
val fee: TransactionFeeExtended,
val selectedToken: CryptoCurrencyStatus?,
val availableTokens: List<CryptoCurrencyStatus>,
) : LoadedFeeResult()
data class Basic(val fee: TransactionFee) : LoadedFeeResult()

View file

@ -7,6 +7,8 @@ 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.decompose.utils.stack
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
@ -24,6 +26,7 @@ internal class FeeSelectorModel @Inject constructor(
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val sendFeatureToggles: SendFeatureToggles,
private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork,
) : Model(), FeeSelectorIntents {
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorDetailsParams>()
@ -54,6 +57,11 @@ internal class FeeSelectorModel @Inject constructor(
}
}
override fun onTokenSelected(status: CryptoCurrencyStatus) {
feeSelectorLogic.onTokenSelected(status)
stackNavigation.pop()
}
override fun onCustomFeeValueChange(index: Int, value: String) {
feeSelectorLogic.onCustomFeeValueChange(index, value)
}
@ -85,7 +93,10 @@ internal class FeeSelectorModel @Inject constructor(
return FeeSelectorRoute.ChooseSpeed
}
// TODO: Add logic to choose initial route based on other conditions
return FeeSelectorRoute.NetworkFee
return if (isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network)) {
FeeSelectorRoute.NetworkFee
} else {
FeeSelectorRoute.ChooseSpeed
}
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.features.send.v2.feeselector.model.FeeSelectorLogic.LoadedFeeR
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Suppress("LongParameterList")
internal class FeeSelectorLoadedTransformer(
@ -61,6 +62,7 @@ internal class FeeSelectorLoadedTransformer(
isTronToken = cryptoCurrencyStatus.currency is CryptoCurrency.Token &&
isTron(cryptoCurrencyStatus.currency.network.rawId),
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
availableFeeCurrencies = getAvailableFeeCurrencies(),
),
feeFiatRateUM = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
FeeFiatRateUM(
@ -78,4 +80,9 @@ internal class FeeSelectorLoadedTransformer(
},
)
}
private fun getAvailableFeeCurrencies(): ImmutableList<CryptoCurrencyStatus>? {
if (fees !is LoadedFeeResult.Extended) return null
return fees.availableTokens.toImmutableList()
}
}

View file

@ -92,6 +92,7 @@ internal class SendConfirmComponent(
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWallet.walletId,
),
onResult = model::onFeeResult,
)

View file

@ -78,6 +78,7 @@ internal class NFTSendConfirmComponent @AssistedInject constructor(
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWallet.walletId,
),
onResult = model::onFeeResult,
)

View file

@ -96,6 +96,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor(
feeDisplaySource = FeeDisplaySource.Screen,
analyticsCategoryName = params.analyticsCategoryName,
analyticsSendSource = params.analyticsSendSource,
userWalletId = params.userWallet.walletId,
),
onResult = model::onFeeResult,
)

View file

@ -57,6 +57,7 @@ internal fun getWcCommonScreen(
feeDisplaySource = FeeDisplaySource.BottomSheet,
analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME,
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.WalletConnect,
userWalletId = model.userWallet.walletId,
),
)
}

View file

@ -76,6 +76,7 @@ internal class WcSendTransactionComponent(
feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet,
analyticsCategoryName = WcAnalyticEvents.WC_CATEGORY_NAME,
analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.WalletConnect,
userWalletId = model.userWallet.walletId,
),
onResult = model::updateFee,
)

View file

@ -105,6 +105,9 @@ internal class WcSendTransactionModel @Inject constructor(
private val securityStatusState =
MutableStateFlow<Lce<Throwable, BlockAidTransactionCheck.Result>>(Lce.Loading(partialContent = null))
val userWallet
get() = useCase.wallet
init {
@Suppress("UnusedPrivateMember")
modelScope.launch {