Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-07 15:45:42 +03:00
parent 8564f21d01
commit 7740d6f453
18 changed files with 167 additions and 50 deletions

View file

@ -47,6 +47,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
params = FeeSelectorParams.FeeSelectorDetailsParams(
state = model.uiState.value,
onLoadFee = params.onLoadFee,
onLoadFeeExtended = params.onLoadFeeExtended,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
callback = model,

View file

@ -29,10 +29,6 @@ class FeeExtendedSelectorModel @Inject constructor(
private val params = paramsContainer.require<FeeSelectorComponentParams>()
private var appCurrency: AppCurrency = AppCurrency.Default
private val tokenConverter = SelectedTokenItemConverter(
appCurrencyProvider = Provider { appCurrency },
onTokenClick = { router.push(FeeSelectorRoute.ChooseToken) },
)
init {
initAppCurrency()
@ -44,19 +40,13 @@ class FeeExtendedSelectorModel @Inject constructor(
init {
params.state
.filterIsInstance<FeeSelectorUM.Content>()
.onEach(::onParentStateUpdated)
.onEach { state -> uiState.value = convertState(state) }
.launchIn(modelScope)
}
private fun getInitialState(): FeeExtendedSelectorUM {
val parentContentState = params.state.value as FeeSelectorUM.Content
return FeeExtendedSelectorUM(
parent = parentContentState,
token = tokenConverter.convert(parentContentState.feeExtraInfo.feeCryptoCurrencyStatus),
fee = parentContentState.selectedFeeItem,
onFeeClick = { router.push(FeeSelectorRoute.ChooseSpeed) },
)
return convertState(parentContentState)
}
private fun initAppCurrency() {
@ -65,13 +55,18 @@ class FeeExtendedSelectorModel @Inject constructor(
}
}
private fun onParentStateUpdated(state: FeeSelectorUM.Content) {
uiState.update { current ->
current.copy(
parent = state,
token = tokenConverter.convert(state.feeExtraInfo.feeCryptoCurrencyStatus),
fee = state.selectedFeeItem,
)
}
private fun convertState(state: FeeSelectorUM.Content): FeeExtendedSelectorUM {
val tokenConverter = SelectedTokenItemConverter(
appCurrencyProvider = Provider { appCurrency },
onTokenClick = { router.push(FeeSelectorRoute.ChooseToken) },
isTokenSelectionAvailable = state.feeExtraInfo.availableFeeCurrencies.orEmpty().size > 1,
)
return FeeExtendedSelectorUM(
parent = state,
token = tokenConverter.convert(state.feeExtraInfo.feeCryptoCurrencyStatus),
fee = state.selectedFeeItem,
onFeeClick = { router.push(FeeSelectorRoute.ChooseSpeed) },
)
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.utils.converter.Converter
internal class SelectedTokenItemConverter(
private val appCurrencyProvider: Provider<AppCurrency>,
private val onTokenClick: () -> Unit,
private val isTokenSelectionAvailable: Boolean,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
@ -24,9 +25,14 @@ internal class SelectedTokenItemConverter(
}
val appCurrency = appCurrencyProvider()
val tokenItemConverter = TokenItemStateConverter(
appCurrency = appCurrencyProvider(),
onItemClick = { _, _ -> onTokenClick() },
onItemClick = if (isTokenSelectionAvailable) {
{ _, _ -> onTokenClick() }
} else {
null
},
)
val state = tokenItemConverter.convert(value)
@ -38,10 +44,14 @@ internal class SelectedTokenItemConverter(
}
state.copy(
fiatAmountState = TokenItemState.FiatAmountState.Icon(
iconRes = R.drawable.ic_select_18_24,
tint = IconTint.Informative,
),
fiatAmountState = if (isTokenSelectionAvailable) {
TokenItemState.FiatAmountState.Icon(
iconRes = R.drawable.ic_select_18_24,
tint = IconTint.Informative,
)
} else {
TokenItemState.FiatAmountState.Empty
},
subtitleState = TokenItemState.SubtitleState.TextContent(
value = resourceReference(
R.string.common_balance,
@ -50,7 +60,6 @@ internal class SelectedTokenItemConverter(
isAvailable = false,
),
subtitle2State = null,
)
}
else -> state

View file

@ -22,6 +22,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
@ -61,12 +62,15 @@ fun FeeExtendedSelectorContent(state: FeeExtendedSelectorUM, modifier: Modifier
val feeFiatRateUM = state.parent.feeFiatRateUM
val fee = state.fee.fee
val isChooseSpeedAvailable = state.parent.feeItems.size > 1
RegularFeeItemContent(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.clickable(onClick = state.onFeeClick)
.conditional(isChooseSpeedAvailable) {
clickable(onClick = state.onFeeClick)
}
.background(
TangemTheme.colors.background.action,
RoundedCornerShape(TangemTheme.dimens.radius14),
@ -93,7 +97,8 @@ fun FeeExtendedSelectorContent(state: FeeExtendedSelectorUM, modifier: Modifier
null
},
ellipsizeOffset = fee.amount.currencySymbol.length,
showSelectorIcon = true,
showSelectorIcon = isChooseSpeedAvailable,
isLoading = state.fee.isLoading(),
)
}
}

View file

@ -38,6 +38,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.inputrow.InputRowEnter
@ -325,6 +326,7 @@ internal fun RegularFeeItemContent(
postDot: TextReference? = null,
ellipsizeOffset: Int? = null,
showSelectorIcon: Boolean = false,
isLoading: Boolean = false,
) {
Column(modifier = modifier) {
Row(
@ -349,6 +351,7 @@ internal fun RegularFeeItemContent(
preDot = preDot,
postDot = postDot,
ellipsizeOffset = ellipsizeOffset,
isLoading = isLoading,
)
if (showSelectorIcon) {
@ -371,6 +374,7 @@ private fun FeeDescription(
preDot: TextReference? = null,
postDot: TextReference? = null,
ellipsizeOffset: Int? = null,
isLoading: Boolean = false,
) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
@ -379,8 +383,12 @@ private fun FeeDescription(
style = TangemTheme.typography.subtitle2,
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE),
)
if (preDot != null) {
FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset)
when {
isLoading -> TextShimmer(
modifier = Modifier.width(122.dp),
style = TangemTheme.typography.caption1,
)
preDot != null -> FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset)
}
}
}

View file

@ -22,6 +22,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@ -47,7 +48,8 @@ internal class FeeTokenSelectorModel @Inject constructor(
initAppCurrency()
params.state
.onEach { uiState.value = stateFromParent(it as FeeSelectorUM.Content) }
.filterIsInstance<FeeSelectorUM.Content>()
.onEach { state -> uiState.value = stateFromParent(state) }
.launchIn(modelScope)
}

View file

@ -92,6 +92,7 @@ internal class FeeSelectorAlertFactory @Inject constructor(
is FeeItem.Slow -> newTransactionFee.minimum
is FeeItem.Market -> newTransactionFee.normal
is FeeItem.Fast -> newTransactionFee.priority
FeeItem.Loading -> newTransactionFee.normal
is FeeItem.Custom -> return
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
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.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.transaction.error.GetFeeError
@ -31,6 +32,7 @@ import com.tangem.utils.transformer.update
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
@ -56,6 +58,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
) : FeeSelectorIntents {
private var appCurrency: AppCurrency = AppCurrency.Default
private var selectedToken: CryptoCurrencyStatus? = null
val uiState = MutableStateFlow<FeeSelectorUM>(params.state)
init {
@ -112,9 +115,19 @@ internal class FeeSelectorLogic @AssistedInject constructor(
}
override fun onTokenSelected(status: CryptoCurrencyStatus) {
if (status.currency !is CryptoCurrency.Token ||
status.currency.id == params.feeCryptoCurrencyStatus.currency.id) {
selectedToken = null
loadFee()
return
}
uiState.update(FeeSelectorTokenSelectedTransformer(status))
uiState.update { state ->
if (state is FeeSelectorUM.Content) {
state.copy(
selectedFeeItem = FeeItem.Loading,
feeItems = persistentListOf(FeeItem.Loading),
feeExtraInfo = state.feeExtraInfo.copy(
feeCryptoCurrencyStatus = status,
),
@ -123,6 +136,14 @@ internal class FeeSelectorLogic @AssistedInject constructor(
state
}
}
if (status.currency is CryptoCurrency.Token) {
selectedToken = status
} else {
selectedToken = null
}
loadFee()
}
override fun onCustomFeeValueChange(index: Int, value: String) {
@ -219,7 +240,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
private suspend fun callLoadFee(): Either<GetFeeError, LoadedFeeResult> {
val extended = params.onLoadFeeExtended
return if (extended != null && sendFeatureToggles.isGaslessTransactionsEnabled) {
extended().fold(
extended(selectedToken).fold(
ifLeft = { error ->
if (error is GetFeeError.GaslessError) {
params.onLoadFee().map { LoadedFeeResult.Basic(it) }

View file

@ -43,7 +43,8 @@ internal class FeeSelectorLoadedTransformer(
)
val selectedFee = when (prevState) {
is FeeSelectorUM.Content -> feeItems.first { it.isSameClass(prevState.selectedFeeItem) }
is FeeSelectorUM.Content ->
feeItems.firstOrNull { it.isSameClass(prevState.selectedFeeItem) } ?: FeeItem.Loading
is FeeSelectorUM.Error,
FeeSelectorUM.Loading,
-> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market }
@ -52,7 +53,7 @@ internal class FeeSelectorLoadedTransformer(
val nonce = ((prevState as? FeeSelectorUM.Content)?.feeNonce as? FeeNonce.Nonce)?.nonce
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
isPrimaryButtonEnabled = selectedFee !is FeeItem.Loading,
fees = fees.transactionFee,
feeItems = feeItems,
selectedFeeItem = selectedFee,

View file

@ -0,0 +1,26 @@
package com.tangem.features.send.v2.feeselector.model.transformers
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
class FeeSelectorTokenSelectedTransformer(
private val selectedToken: CryptoCurrencyStatus,
) : Transformer<FeeSelectorUM> {
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
return if (prevState is FeeSelectorUM.Content) {
prevState.copy(
selectedFeeItem = FeeItem.Loading,
feeItems = persistentListOf(FeeItem.Loading),
feeExtraInfo = prevState.feeExtraInfo.copy(
feeCryptoCurrencyStatus = selectedToken,
),
)
} else {
prevState
}
}
}

View file

@ -216,6 +216,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
callback = model,
predefinedValues = model.predefinedValues,
onLoadFee = model::loadFee,
onLoadFeeExtended = model::loadFeeExtended,
onSendTransaction = {
innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess)
},

View file

@ -15,6 +15,7 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent
import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData
@ -86,6 +87,7 @@ internal class SendConfirmComponent(
params = FeeSelectorParams.FeeSelectorBlockParams(
state = model.uiState.value.feeSelectorUM,
onLoadFee = params.onLoadFee,
onLoadFeeExtended = params.onLoadFeeExtended,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeStateConfiguration = model.feeStateConfiguration,
@ -164,6 +166,7 @@ internal class SendConfirmComponent(
val isBalanceHidingFlow: StateFlow<Boolean>,
val predefinedValues: PredefinedValues,
val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>,
val onLoadFeeExtended: suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>,
val onSendTransaction: () -> Unit,
)

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationUM
@ -37,8 +38,11 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.models.TransactionFeeExtended
import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase
import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
import com.tangem.domain.utils.convertToSdkAmount
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
@ -95,6 +99,8 @@ internal class SendModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
private val getFeeForTokenUseCase: GetFeeForTokenUseCase,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val sendAmountUpdateTrigger: SendAmountUpdateTrigger,
@ -245,10 +251,10 @@ internal class SendModel @Inject constructor(
showAlertError()
}
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
suspend fun prepareTransferTransaction(): Either<Throwable, TransactionData> {
val predefinedValues = predefinedValues
val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value
val transferTransaction = if (predefinedValues is PredefinedValues.Content.Deeplink) {
return if (predefinedValues is PredefinedValues.Content.Deeplink) {
val predefinedAmount = predefinedValues.amount.parseBigDecimalOrNull()
createTransferTransactionUseCase(
amount = predefinedAmount?.convertToSdkAmount(cryptoCurrencyStatus) ?: error("Invalid amount"),
@ -271,9 +277,12 @@ internal class SendModel @Inject constructor(
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,
)
}.getOrElse {
return GetFeeError.DataError(it).left()
}
}
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
val transferTransaction = prepareTransferTransaction()
.getOrElse { return GetFeeError.DataError(it).left() }
return getFeeUseCase(
transactionData = transferTransaction,
@ -282,6 +291,25 @@ internal class SendModel @Inject constructor(
)
}
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
val transferTransaction = prepareTransferTransaction()
.getOrElse { return GetFeeError.DataError(it).left() }
return if (maybeToken == null) {
getFeeForGaslessUseCase(
transactionData = transferTransaction,
userWallet = userWallet,
network = params.currency.network,
)
} else {
getFeeForTokenUseCase(
transactionData = transferTransaction,
userWallet = userWallet,
token = maybeToken.currency,
)
}
}
fun showAlertError() {
sendConfirmAlertFactory.getGenericErrorState(
onFailedTxEmailClick = ::onFailedTxEmailClick,