Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-26 13:50:00 +03:00
parent b08892762c
commit f095f3a508
24 changed files with 184 additions and 26 deletions

View file

@ -88,18 +88,27 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
)
return when (config) {
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
FeeSelectorRoute.ChooseSpeed -> feeSpeedSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
FeeSelectorRoute.ChooseToken -> feeTokenSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
FeeSelectorRoute.NetworkFee -> {
model.onFeeScreenOpened()
feeExtendedSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
}
FeeSelectorRoute.ChooseSpeed -> {
model.onChooseSpeedScreenOpened()
feeSpeedSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
}
FeeSelectorRoute.ChooseToken -> {
model.onChooseTokenScreenOpened()
feeTokenSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
}
}
}

View file

@ -58,12 +58,6 @@ internal class FeeSelectorBlockModel @Inject constructor(
}
fun showFeeSelector() {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = params.analyticsCategoryName,
source = params.analyticsSendSource,
),
)
analyticsEventHandler.send(
CommonSendAnalyticEvents.ScreenReopened(
categoryName = params.analyticsCategoryName,

View file

@ -153,13 +153,15 @@ internal class FeeSelectorLogic @AssistedInject constructor(
categoryName = params.analyticsCategoryName,
feeType = feeSelectorUM.toAnalyticType(),
source = params.analyticsSendSource,
feeToken = feeSelectorUM.feeExtraInfo.feeCryptoCurrencyStatus.currency.symbol,
),
)
val isCustomFeeEdited = feeSelectorUM.selectedFeeItem.fee.amount.value != feeSelectorUM.fees.normal.amount.value
if (feeSelectorUM.selectedFeeItem is FeeItem.Custom && isCustomFeeEdited) {
analyticsEventHandler.send(GasPriceInserter(categoryName = params.analyticsCategoryName))
}
if (feeSelectorUM.feeNonce is FeeNonce.Nonce) {
val nonceField = feeSelectorUM.feeNonce as? FeeNonce.Nonce
if (nonceField != null && nonceField.nonce != null) {
analyticsEventHandler.send(
NonceInserted(
categoryName = params.analyticsCategoryName,
@ -268,12 +270,11 @@ internal class FeeSelectorLogic @AssistedInject constructor(
private suspend fun getSelectedTokenStatus(tokenId: CryptoCurrency.ID): Either<GetFeeError, CryptoCurrencyStatus> =
either {
val feeTokenId = tokenId
if (params.feeCryptoCurrencyStatus.currency.id != feeTokenId) {
if (params.feeCryptoCurrencyStatus.currency.id != tokenId) {
getSingleCryptoCurrencyStatusUseCase
.invokeMultiWalletSync(
userWalletId = params.userWalletId,
cryptoCurrencyId = feeTokenId,
cryptoCurrencyId = tokenId,
).getOrElse {
raise(GetFeeError.DataError(IllegalStateException("No token found for id: $tokenId")))
}

View file

@ -3,11 +3,13 @@ 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.analytics.api.AnalyticsEventHandler
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.features.send.v2.api.analytics.CommonSendAnalyticEvents
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
@ -23,6 +25,7 @@ internal class FeeSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model(), FeeSelectorIntents {
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorDetailsParams>()
@ -78,6 +81,42 @@ internal class FeeSelectorModel @Inject constructor(
}
}
fun onFeeScreenOpened() {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeSummaryScreenOpened(
categoryName = params.analyticsCategoryName,
source = params.analyticsSendSource,
),
)
}
fun onChooseSpeedScreenOpened() {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeScreenOpened(
categoryName = params.analyticsCategoryName,
source = params.analyticsSendSource,
),
)
}
fun onChooseTokenScreenOpened() {
analyticsEventHandler.send(
CommonSendAnalyticEvents.FeeTokenScreenOpened(
categoryName = params.analyticsCategoryName,
source = params.analyticsSendSource,
availableTokens = getAvailableTokensString(),
),
)
}
private fun getAvailableTokensString(): String {
return (uiState.value as? FeeSelectorUM.Content)
?.feeExtraInfo
?.availableFeeCurrencies
?.joinToString(", ") { it.currency.symbol }
.orEmpty()
}
private fun contentState(): FeeSelectorUM.Content {
val currentState = uiState.value
require(currentState is FeeSelectorUM.Content) { "Current state must be FeeSelectorUM.Content" }

View file

@ -65,7 +65,7 @@ internal fun FeeSelectorModalBottomSheet(
.fillMaxWidth()
.padding(horizontal = 16.dp),
enabled = state.isPrimaryButtonEnabled,
text = stringResourceSafe(R.string.common_confirm),
text = stringResourceSafe(R.string.common_apply),
onClick = feeSelectorIntents::onDoneClick,
)
}

View file

@ -27,6 +27,7 @@ internal sealed class SendAnalyticEvents(
val blockchain: String,
val isNonceNotEmpty: Boolean,
private val ensStatus: AnalyticsParam.EmptyFull,
private val feeToken: String,
val derivationIndex: Int?,
) : SendAnalyticEvents(
event = "Transaction Sent Screen Opened",

View file

@ -18,10 +18,16 @@ internal class SendAnalyticHelper @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) {
fun sendSuccessAnalytics(cryptoCurrency: CryptoCurrency, sendUM: SendUM, account: Account.CryptoPortfolio?) {
fun sendSuccessAnalytics(
cryptoCurrency: CryptoCurrency,
sendUM: SendUM,
feeToken: CryptoCurrency,
account: Account.CryptoPortfolio?,
) {
val destinationUM = sendUM.destinationUM as? DestinationUM.Content
val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return
val feeType = feeSelectorUM.toAnalyticType()
val feeTokenSymbol = feeToken.symbol
val isNotMainAccount = account != null && !account.isMainAccount
val derivationIndex = if (isNotMainAccount) account.derivationIndex.value else null
analyticsEventHandler.send(
@ -32,6 +38,7 @@ internal class SendAnalyticHelper @Inject constructor(
isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce,
ensStatus = getEnsStatus(sendUM),
derivationIndex = derivationIndex,
feeToken = feeTokenSymbol,
),
)
analyticsEventHandler.send(
@ -40,6 +47,7 @@ internal class SendAnalyticHelper @Inject constructor(
blockchain = cryptoCurrency.network.name,
token = cryptoCurrency.symbol,
feeType = feeType,
feeToken = feeTokenSymbol,
),
memoType = getSendTransactionMemoType(destinationUM?.memoTextField),
),

View file

@ -390,6 +390,7 @@ internal class SendConfirmModel @Inject constructor(
private suspend fun sendTransaction(txData: TransactionData.Uncompiled) {
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
val feeToken = getCurrencyStatusForFeePayment().currency
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
@ -433,6 +434,7 @@ internal class SendConfirmModel @Inject constructor(
cryptoCurrency = cryptoCurrency,
sendUM = uiState.value,
account = params.accountFlow.value,
feeToken = feeToken,
)
params.callback.onResult(uiState.value)
params.onSendTransaction()

View file

@ -5,6 +5,7 @@ import arrow.core.Either
import arrow.core.getOrElse
import arrow.core.left
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationUM
@ -28,6 +29,7 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
@ -72,6 +74,7 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned
internal interface SendComponentCallback :
SendAmountComponent.ModelCallback,
@ -452,6 +455,17 @@ internal class SendModel @Inject constructor(
}
}
fun getSelectedFeeToken(): CryptoCurrency {
val feeUMV2 = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content
val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
return if (isFeeInTokenCurrency) {
feeUMV2.feeExtraInfo.feeCryptoCurrencyStatus.currency
} else {
feeCryptoCurrencyStatusFlow.value.currency
}
}
private suspend fun getFeeCurrencyStatus(
cryptoCurrencyStatus: CryptoCurrencyStatus,
isMultiCurrency: Boolean,

View file

@ -36,6 +36,7 @@ internal class NFTSendAnalyticHelper @Inject constructor(
blockchain = cryptoCurrency.network.name,
token = NFT_SEND_CATEGORY, // should send "NFT" in token param
feeType = feeType,
feeToken = cryptoCurrency.symbol,
),
memoType = getSendTransactionMemoType(destinationUM?.memoTextField),
),