Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-06 13:44:56 +03:00
parent 680794d447
commit a61004a3c1
18 changed files with 459 additions and 106 deletions

View file

@ -0,0 +1,18 @@
package com.tangem.core.decompose.utils
import com.arkivanov.decompose.router.stack.StackNavigation
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
/**
* Retrieves the current stack of items from the [com.arkivanov.decompose.router.stack.ChildStack]
* @return A list representing the current stack of items
*/
suspend fun <T : Any> StackNavigation<T>.stack(): List<T> {
return suspendCancellableCoroutine { continuation ->
this.navigate(
transformer = { stack -> stack },
onComplete = { newStack, oldStack -> continuation.resume(newStack) },
)
}
}

View file

@ -21,6 +21,9 @@ dependencies {
/** Common */
implementation(projects.common.ui)
/** Domain */
implementation(projects.domain.transaction)
/** Domain models */
api(projects.domain.models)
implementation(projects.domain.appCurrency.models)

View file

@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.R
import com.tangem.features.send.v2.api.entity.FeeItem.*
@ -61,6 +62,7 @@ data class FeeExtraInfo(
val isFeeApproximate: Boolean,
val isFeeConvertibleToFiat: Boolean,
val isTronToken: Boolean,
val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
)
sealed class FeeNonce {

View file

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

View file

@ -5,10 +5,8 @@ import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -26,7 +24,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("PropertyUsedBeforeDeclaration")
internal class DefaultFeeSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
@ -35,22 +32,22 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
private val feeSpeedSelectorComponentFactory: FeeSpeedSelectorComponent.Factory,
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
private val stackNavigation = instanceKeeper.getOrCreateSimple { StackNavigation<FeeSelectorRoute>() }
private val innerRouter = InnerRouter<FeeSelectorRoute>(
stackNavigation = stackNavigation,
private val model: FeeSelectorModel = getOrCreateModel(params = params)
val innerRouter = InnerRouter<FeeSelectorRoute>(
stackNavigation = model.stackNavigation,
popCallback = {
stackNavigation.pop { success ->
model.stackNavigation.pop { success ->
if (!success) {
dismiss()
}
}
},
)
private val model: FeeSelectorModel = getOrCreateModel(params = params, router = innerRouter)
private val contentStack = childStack(
key = "FeeSelectorDetailsStack",
source = stackNavigation,
source = model.stackNavigation,
serializer = FeeSelectorRoute.serializer(),
initialConfiguration = model.getInitialRoute(),
handleBackButton = false,
@ -69,7 +66,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
FeeSelectorModalBottomSheet(
childStack = contentStack,
state = state,
feeSelectorIntents = model.intents,
feeSelectorIntents = model,
feeDisplaySource = params.feeDisplaySource,
onDismiss = onDismiss,
onBack = innerRouter::pop,
@ -85,7 +82,7 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
val componentParams = FeeSelectorComponentParams(
parentParams = params,
state = model.uiState,
intents = model.intents,
intents = model,
)
return when (config) {

View file

@ -1,23 +1,59 @@
package com.tangem.features.send.v2.feeselector.component.extended.entity
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.icons.IconTint
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 SelectedTokenItemConverter(
appCurrency: AppCurrency,
onTokenClick: () -> Unit,
private val appCurrencyProvider: Provider<AppCurrency>,
private val onTokenClick: () -> Unit,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
private val tokenItemConverter = TokenItemStateConverter(
appCurrency = appCurrency,
onItemClick = { _, _ -> onTokenClick() },
)
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
// TODO
return tokenItemConverter.convert(value)
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.Icon(
iconRes = R.drawable.ic_select_18_24,
tint = IconTint.Informative,
),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = resourceReference(
R.string.common_balance,
wrappedList(balance),
),
isAvailable = false,
),
subtitle2State = null,
)
}
else -> state
}
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentPar
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
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -29,44 +30,48 @@ 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()
}
val uiState: StateFlow<FeeExtendedSelectorUM>
field = MutableStateFlow<FeeExtendedSelectorUM>(getInitialState())
init {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
uiState.value = getInitialState()
}
params.state
.filterIsInstance<FeeSelectorUM.Content>()
.onEach(::onParentStateUpdated)
.launchIn(modelScope)
}
private fun getInitialState(): FeeExtendedSelectorUM {
val status = params.parentParams.feeCryptoCurrencyStatus
val parentContentState = params.state.value as FeeSelectorUM.Content
return FeeExtendedSelectorUM(
parent = parentContentState,
token = SelectedTokenItemConverter(
appCurrency = appCurrency,
onTokenClick = {
router.push(FeeSelectorRoute.ChooseToken)
},
).convert(status),
token = tokenConverter.convert(parentContentState.feeExtraInfo.feeCryptoCurrencyStatus),
fee = parentContentState.selectedFeeItem,
onFeeClick = {
router.push(FeeSelectorRoute.ChooseSpeed)
},
onFeeClick = { router.push(FeeSelectorRoute.ChooseSpeed) },
)
}
private fun onParentStateUpdated(state: FeeSelectorUM) {
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun onParentStateUpdated(state: FeeSelectorUM.Content) {
uiState.update { current ->
current.copy(
parent = state as FeeSelectorUM.Content,
parent = state,
token = tokenConverter.convert(state.feeExtraInfo.feeCryptoCurrencyStatus),
fee = state.selectedFeeItem,
)
}
}

View file

@ -29,6 +29,9 @@ import com.tangem.core.ui.format.bigdecimal.format
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.extended.entity.FeeExtendedSelectorUM
import com.tangem.features.send.v2.feeselector.component.speed.ui.RegularFeeItemContent
@ -95,6 +98,7 @@ fun FeeExtendedSelectorContent(state: FeeExtendedSelectorUM, modifier: Modifier
}
}
@Suppress("LongMethod")
@Preview
@Composable
private fun Preview() {
@ -115,6 +119,34 @@ private fun Preview() {
onItemLongClick = {},
)
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 lowFeeItem =
FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)))
@ -130,6 +162,7 @@ private fun Preview() {
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeNonce = FeeNonce.None,
feeFiatRateUM = FeeFiatRateUM(

View file

@ -50,6 +50,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
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.speed.FeeSpeedSelectorIntents
import com.tangem.features.send.v2.feeselector.component.speed.StubFeeSpeedSelectorIntents
@ -473,6 +476,7 @@ private class FeeSpeedSelectorUMContentProvider : CollectionPreviewParameterProv
isFeeApproximate = true,
isFeeConvertibleToFiat = true,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal.TEN,
@ -484,44 +488,74 @@ private class FeeSpeedSelectorUMContentProvider : CollectionPreviewParameterProv
),
)
private val customFeeItem = FeeItem.Custom(
fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)),
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.119806",
onValueChange = {},
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(),
symbol = "ETH",
private val cryptoCurrencyStatus
get() = 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,
title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_custom_amount_fee_footer),
label = stringReference("~ 0,03 \$"),
isReadonly = false,
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/medium/bitcoin.png",
isCustom = false,
),
CustomFeeFieldUM(
value = "40000",
onValueChange = {},
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(),
symbol = "GWEI",
decimals = 8,
title = resourceReference(R.string.send_gas_price),
footer = resourceReference(R.string.send_gas_price_footer),
label = null,
isReadonly = false,
value = CryptoCurrencyStatus.Loading,
)
private val customFeeItem
get() = FeeItem.Custom(
fee = Fee.Common(Amount(value = BigDecimal("0.05"), blockchain = Blockchain.Ethereum)),
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.119806",
onValueChange = {},
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(),
symbol = "ETH",
decimals = 8,
title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_custom_amount_fee_footer),
label = stringReference("~ 0,03 \$"),
isReadonly = false,
),
CustomFeeFieldUM(
value = "40000",
onValueChange = {},
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(),
symbol = "GWEI",
decimals = 8,
title = resourceReference(R.string.send_gas_price),
footer = resourceReference(R.string.send_gas_price_footer),
label = null,
isReadonly = false,
),
CustomFeeFieldUM(
value = "31400",
onValueChange = {},
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(),
symbol = null,
decimals = 8,
title = resourceReference(R.string.send_gas_limit),
footer = resourceReference(R.string.send_gas_limit_footer),
label = null,
isReadonly = false,
),
),
CustomFeeFieldUM(
value = "31400",
onValueChange = {},
keyboardOptions = KeyboardOptions(),
keyboardActions = KeyboardActions(),
symbol = null,
decimals = 8,
title = resourceReference(R.string.send_gas_limit),
footer = resourceReference(R.string.send_gas_limit_footer),
label = null,
isReadonly = false,
),
),
)
)

View file

@ -25,14 +25,14 @@ import javax.inject.Inject
@ModelScoped
internal class FeeSelectorBlockModel @Inject constructor(
paramsContainer: ParamsContainer,
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val feeSelectorLogicFactory: FeeSelectorLogic.Factory,
private val analyticsEventHandler: AnalyticsEventHandler,
private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase,
private val urlOpener: UrlOpener,
) : Model(), FeeSelectorModelCallback {
private val params = paramsContainer.require<FeeSelectorParams>()
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorBlockParams>()
private val feeSelectorLogic = feeSelectorLogicFactory.create(
params = params,
modelScope = modelScope,

View file

@ -1,11 +1,18 @@
package com.tangem.features.send.v2.feeselector.model
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.blockchain.common.AmountType
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.CryptoCurrencyStatus
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.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeNonce
@ -38,6 +45,8 @@ internal class FeeSelectorLogic @AssistedInject constructor(
private val feeSelectorCheckReloadTrigger: FeeSelectorCheckReloadTrigger,
private val feeSelectorAlertFactory: FeeSelectorAlertFactory,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
) : FeeSelectorIntents {
private var appCurrency: AppCurrency = AppCurrency.Default
@ -51,10 +60,6 @@ internal class FeeSelectorLogic @AssistedInject constructor(
loadFee()
}
fun updateState(feeSelectorUM: FeeSelectorUM) {
uiState.value = feeSelectorUM
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
@ -66,18 +71,18 @@ internal class FeeSelectorLogic @AssistedInject constructor(
uiState.update(FeeSelectorLoadingTransformer)
}
modelScope.launch {
params.onLoadFee()
callLoadFee()
.fold(
ifLeft = { error -> uiState.update(FeeSelectorErrorTransformer(error)) },
ifRight = { fee ->
uiState.update(
FeeSelectorLoadedTransformer(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
feeCryptoCurrencyStatus = fee.selectedTokenOrNull ?: params.feeCryptoCurrencyStatus,
appCurrency = appCurrency,
fees = fee,
feeStateConfiguration = params.feeStateConfiguration,
isFeeApproximate = isFeeApproximate(fee.normal.amount.type),
isFeeApproximate = isFeeApproximate(fee.transactionFee.normal.amount.type),
feeSelectorIntents = this@FeeSelectorLogic,
),
)
@ -98,9 +103,6 @@ internal class FeeSelectorLogic @AssistedInject constructor(
)
}
uiState.update(FeeItemSelectedTransformer(feeItem))
if (feeItem !is FeeItem.Custom) {
onDoneClick()
}
}
override fun onCustomFeeValueChange(index: Int, value: String) {
@ -141,8 +143,6 @@ internal class FeeSelectorLogic @AssistedInject constructor(
),
)
}
(params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value)
}
private fun subscribeOnFeeReloadTriggerUpdates() {
@ -170,10 +170,10 @@ internal class FeeSelectorLogic @AssistedInject constructor(
private fun checkLoadFee() {
modelScope.launch {
params.onLoadFee().fold(
callLoadFee().fold(
ifRight = { newFee ->
feeSelectorAlertFactory.getFeeUpdatedAlert(
newTransactionFee = newFee,
newTransactionFee = newFee.transactionFee,
feeSelectorUM = uiState.value,
proceedAction = {
modelScope.launch {
@ -195,6 +195,61 @@ internal class FeeSelectorLogic @AssistedInject constructor(
}
}
@Suppress("UnreachableCode")
private suspend fun callLoadFee(): Either<GetFeeError, LoadedFeeResult> {
val extended = params.onLoadFeeExtended
return if (extended != null && sendFeatureToggles.isGaslessTransactionsEnabled) {
extended().fold(
ifLeft = { error ->
if (error is GetFeeError.GaslessError) {
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
} else {
Either.Left(error)
}
},
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
},
),
)
},
)
} else {
params.onLoadFee().map { LoadedFeeResult.Basic(it) }
}
}
sealed class LoadedFeeResult {
data class Extended(
val fee: TransactionFeeExtended,
val selectedToken: CryptoCurrencyStatus?,
) : LoadedFeeResult()
data class Basic(val fee: TransactionFee) : LoadedFeeResult()
val selectedTokenOrNull: CryptoCurrencyStatus?
get() = when (this) {
is Extended -> selectedToken
is Basic -> null
}
val transactionFee: TransactionFee
get() = when (this) {
is Extended -> fee.transactionFee
is Basic -> fee
}
}
@AssistedFactory
interface Factory {
fun create(params: FeeSelectorParams, modelScope: CoroutineScope): FeeSelectorLogic

View file

@ -1,13 +1,20 @@
package com.tangem.features.send.v2.feeselector.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
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.features.send.v2.api.SendFeatureToggles
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.route.FeeSelectorRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.isSingleItem
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ -17,16 +24,61 @@ internal class FeeSelectorModel @Inject constructor(
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val sendFeatureToggles: SendFeatureToggles,
) : Model() {
) : Model(), FeeSelectorIntents {
private val params = paramsContainer.require<FeeSelectorParams>()
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorDetailsParams>()
private val feeSelectorLogic = feeSelectorLogicFactory.create(
params = params,
modelScope = modelScope,
)
val stackNavigation = StackNavigation<FeeSelectorRoute>()
val uiState = feeSelectorLogic.uiState
val intents: FeeSelectorIntents = feeSelectorLogic
override fun onFeeItemSelected(feeItem: FeeItem) {
feeSelectorLogic.onFeeItemSelected(feeItem)
if (contentState().selectedFeeItem is FeeItem.Custom) {
return
}
modelScope.launch {
val stack = stackNavigation.stack()
if (stack.isSingleItem() && stack[0] is FeeSelectorRoute.ChooseSpeed) {
// In case of only speed selection, we finish the flow immediately after selection
feeSelectorLogic.onDoneClick()
params.callback.onFeeResult(uiState.value)
} else {
stackNavigation.pop()
}
}
}
override fun onCustomFeeValueChange(index: Int, value: String) {
feeSelectorLogic.onCustomFeeValueChange(index, value)
}
override fun onNonceChange(value: String) {
feeSelectorLogic.onNonceChange(value)
}
override fun onDoneClick() {
modelScope.launch {
val stack = stackNavigation.stack()
if (stack.isSingleItem()) {
feeSelectorLogic.onDoneClick()
params.callback.onFeeResult(uiState.value)
} else {
stackNavigation.pop()
}
}
}
private fun contentState(): FeeSelectorUM.Content {
val currentState = uiState.value
require(currentState is FeeSelectorUM.Content) { "Current state must be FeeSelectorUM.Content" }
return currentState
}
fun getInitialRoute(): FeeSelectorRoute {
if (sendFeatureToggles.isGaslessTransactionsEnabled.not()) {

View file

@ -1,13 +1,13 @@
package com.tangem.features.send.v2.feeselector.model.transformers
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.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.*
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.features.send.v2.feeselector.model.FeeSelectorLogic.LoadedFeeResult
import com.tangem.lib.crypto.BlockchainUtils.isTron
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
@ -17,7 +17,7 @@ internal class FeeSelectorLoadedTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val fees: TransactionFee,
private val fees: LoadedFeeResult,
private val feeStateConfiguration: FeeSelectorParams.FeeStateConfiguration,
private val isFeeApproximate: Boolean,
private val feeSelectorIntents: FeeSelectorIntents,
@ -25,7 +25,7 @@ internal class FeeSelectorLoadedTransformer(
private val feeItemsConverter = FeeItemConverter(
feeStateConfiguration = feeStateConfiguration,
normalFee = fees.normal,
normalFee = fees.transactionFee.normal,
feeSelectorIntents = feeSelectorIntents,
appCurrency = appCurrency,
cryptoCurrencyStatus = feeCryptoCurrencyStatus,
@ -37,7 +37,9 @@ internal class FeeSelectorLoadedTransformer(
} else {
null
}
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(FeeItemConverter.Input(fees, prevCustomFee))
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(
FeeItemConverter.Input(fees.transactionFee, prevCustomFee),
)
val selectedFee = when (prevState) {
is FeeSelectorUM.Content -> feeItems.first { it.isSameClass(prevState.selectedFeeItem) }
@ -50,7 +52,7 @@ internal class FeeSelectorLoadedTransformer(
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = fees,
fees = fees.transactionFee,
feeItems = feeItems,
selectedFeeItem = selectedFee,
feeExtraInfo = FeeExtraInfo(
@ -58,6 +60,7 @@ internal class FeeSelectorLoadedTransformer(
isFeeConvertibleToFiat = feeCryptoCurrencyStatus.currency.network.hasFiatFeeRate,
isTronToken = cryptoCurrencyStatus.currency is CryptoCurrency.Token &&
isTron(cryptoCurrencyStatus.currency.network.rawId),
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
),
feeFiatRateUM = feeCryptoCurrencyStatus.value.fiatRate?.let { rate ->
FeeFiatRateUM(
@ -65,7 +68,7 @@ internal class FeeSelectorLoadedTransformer(
appCurrency = appCurrency,
)
},
feeNonce = if (fees.normal is Fee.Ethereum) {
feeNonce = if (fees.transactionFee.normal is Fee.Ethereum) {
FeeNonce.Nonce(
nonce = nonce,
onNonceChange = feeSelectorIntents::onNonceChange,

View file

@ -15,7 +15,7 @@ internal sealed interface FeeSelectorRoute : Route {
@Serializable
data object NetworkFee : FeeSelectorRoute {
override val title: TextReference = resourceReference(R.string.common_fee_selector_title)
override val title: TextReference = resourceReference(R.string.common_network_fee_title)
}
@Serializable

View file

@ -42,6 +42,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.FeeSelectorBlockTestTags
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.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.api.entity.*
import com.tangem.features.send.v2.impl.R
@ -238,11 +241,48 @@ private fun FeeSelectorBlockContent_Preview(@PreviewParameter(FeeSelectorUMProvi
}
private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
private val maxFeeItem = FeeItem.Market(
fee = Fee.Common(amount = Amount(value = BigDecimal("100000000"), blockchain = Blockchain.Hedera)),
)
private val lowFeeItem =
FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)))
private val maxFeeItem
get() = FeeItem.Market(
fee = Fee.Common(amount = Amount(value = BigDecimal("100000000"), blockchain = Blockchain.Hedera)),
)
private val lowFeeItem
get() = FeeItem.Market(
Fee.Common(
amount = Amount(
value = BigDecimal("0.0002876"),
blockchain = Blockchain.Ethereum,
),
),
)
val cryptoCurrencyStatus
get() = 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,
)
override val values: Sequence<FeeSelectorUM> = sequenceOf(
FeeSelectorUM.Content(
@ -253,6 +293,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeNonce = FeeNonce.None,
feeFiatRateUM = FeeFiatRateUM(
@ -269,6 +310,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeNonce = FeeNonce.None,
feeFiatRateUM = FeeFiatRateUM(

View file

@ -8,6 +8,8 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendConfirmationNotificationsTransformerV2
@ -28,6 +30,34 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
private val analyticsCategoryName = "test_category"
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,
)
@Test
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
// GIVEN
@ -191,6 +221,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
@ -274,6 +305,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
@ -345,6 +377,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
@ -428,6 +461,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),

View file

@ -10,6 +10,8 @@ import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
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.common.ui.state.ConfirmUM
import io.mockk.mockk
@ -30,6 +32,34 @@ class SendConfirmationNotificationsTransformerV2Test {
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
private val analyticsCategoryName = "test_category"
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,
)
@Test
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
// GIVEN
@ -247,6 +277,7 @@ class SendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
@ -330,6 +361,7 @@ class SendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
@ -401,6 +433,7 @@ class SendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
@ -484,6 +517,7 @@ class SendConfirmationNotificationsTransformerV2Test {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),

View file

@ -333,6 +333,7 @@ private fun SendWithSwapSuccessContent_Preview() {
isFeeApproximate = false,
isFeeConvertibleToFiat = false,
isTronToken = false,
feeCryptoCurrencyStatus = SwapAmountContentPreview.cryptoCurrencyStatus,
),
feeFiatRateUM = null,
feeNonce = FeeNonce.None,