Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-24 17:31:26 +05:00
parent 58291942cf
commit 4d55089d5e
19 changed files with 191 additions and 7 deletions

View file

@ -219,5 +219,6 @@ sealed class AnalyticsParam {
const val ACTION = "Action"
const val COLLECTIONS = "Collections"
const val NFT = "Nft"
const val NONCE = "Nonce"
}
}

View file

@ -50,6 +50,7 @@ fun InputRowEnter(
text: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: TextReference? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
isSingleLine: Boolean = false,
@ -78,6 +79,7 @@ fun InputRowEnter(
SimpleTextField(
value = text,
onValueChange = onValueChange,
placeholder = placeholder,
color = textColor,
singleLine = isSingleLine,
visualTransformation = visualTransformation,

View file

@ -83,6 +83,7 @@ internal class DefaultTransactionRepository(
destination: String,
userWalletId: UserWalletId,
network: Network,
nonce: BigInteger?,
): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) {
val blockchain = network.toBlockchain()
val walletManager = walletManagersFacade.getOrCreateWalletManager(
@ -101,7 +102,7 @@ internal class DefaultTransactionRepository(
createTransactionDataExtras(
callData = callData,
network = network,
nonce = null,
nonce = nonce,
gasLimit = null,
)
} else {

View file

@ -33,6 +33,7 @@ interface TransactionRepository {
destination: String,
userWalletId: UserWalletId,
network: Network,
nonce: BigInteger? = null,
): TransactionData.Uncompiled
@Suppress("LongParameterList")

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.models.network.Network
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.wallets.models.UserWalletId
import java.math.BigInteger
/**
* Use case to create and get transfer transaction
@ -28,11 +29,13 @@ class CreateTransferTransactionUseCase(
destination: String,
userWalletId: UserWalletId,
network: Network,
nonce: BigInteger? = null,
) = Either.catch {
transactionRepository.createTransferTransaction(
amount = amount,
fee = fee,
memo = memo,
nonce = nonce,
destination = destination,
userWalletId = userWalletId,
network = network,
@ -49,11 +52,13 @@ class CreateTransferTransactionUseCase(
destination: String,
userWalletId: UserWalletId,
network: Network,
nonce: BigInteger? = null,
) = Either.catch {
transactionRepository.createTransferTransaction(
amount = amount,
memo = memo,
fee = null,
nonce = nonce,
destination = destination,
userWalletId = userWalletId,
network = network,

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.v2.common.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
@ -95,6 +96,20 @@ internal sealed class CommonSendAnalyticEvents(
params = mapOf(SOURCE to source.name),
)
/** Fee screen is closed with non empty nonce */
data class NonceInserted(
val categoryName: String,
val token: String,
val blockchain: String,
) : CommonSendAnalyticEvents(
category = categoryName,
event = "Nonce Inserted",
params = mapOf(
TOKEN_PARAM to token,
BLOCKCHAIN to blockchain,
),
)
companion object {
const val SEND_CATEGORY = "Token / Send"
const val NFT_SEND_CATEGORY = "NFT"

View file

@ -7,6 +7,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
import java.math.BigInteger
@Immutable
internal sealed class FeeSelectorUM {
@ -19,6 +20,9 @@ internal sealed class FeeSelectorUM {
val selectedFeeItem: FeeItem,
val isFeeApproximate: Boolean,
val feeFiatRateDataHolder: FeeFiatRateDataHolder?,
val displayNonceInput: Boolean,
val nonce: BigInteger?,
val onNonceChange: (String) -> Unit,
) : FeeSelectorUM()
}

View file

@ -21,6 +21,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
@ -55,6 +56,7 @@ import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.BigInteger
@Composable
internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> Unit) {
@ -75,7 +77,10 @@ internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () ->
)
},
content = {
FeeSelectorItems(state = state, modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp))
FeeSelectorItems(
state = state,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp),
)
},
footer = {
PrimaryButton(
@ -240,18 +245,25 @@ private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier =
isSelected = isSelected,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
displayNonceInput = state.displayNonceInput,
nonce = state.nonce,
onNonceChange = state.onNonceChange,
)
}
}
}
}
@Suppress("LongParameterList")
@Composable
private fun CustomFeeBlock(
customFee: FeeItem.Custom,
isSelected: Boolean,
iconBackgroundColor: Color,
iconTint: Color,
displayNonceInput: Boolean,
nonce: BigInteger?,
onNonceChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@ -281,7 +293,13 @@ private fun CustomFeeBlock(
enter = expandVertically().plus(fadeIn()),
exit = shrinkVertically().plus(fadeOut()),
) {
ExpandedCustomFeeItems(customFeeFields = customFee.customValues, onValueChange = { _, _ -> })
ExpandedCustomFeeItems(
customFeeFields = customFee.customValues,
onValueChange = { _, _ -> },
displayNonceInput = displayNonceInput,
nonce = nonce,
onNonceChange = onNonceChange,
)
}
}
}
@ -290,11 +308,14 @@ private fun CustomFeeBlock(
private fun ExpandedCustomFeeItems(
customFeeFields: ImmutableList<CustomFeeFieldUM>,
onValueChange: (Int, String) -> Unit,
displayNonceInput: Boolean,
nonce: BigInteger?,
onNonceChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
customFeeFields.fastForEachIndexed { index, field ->
val showDivider = index != customFeeFields.size - 1
val showDivider = index != customFeeFields.size - 1 || displayNonceInput
if (field.label != null) {
InputRowEnterInfoAmountV2(
text = field.value,
@ -323,6 +344,21 @@ private fun ExpandedCustomFeeItems(
)
}
}
if (displayNonceInput) {
// TODO implement v2 input without binding to amount
InputRowEnterInfoAmountV2(
text = nonce?.toString() ?: "",
decimals = 0,
title = resourceReference(R.string.send_nonce),
titleColor = TangemTheme.colors.text.tertiary,
symbol = null,
onValueChange = onNonceChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
keyboardActions = KeyboardActions(),
showDivider = false,
)
}
}
}
@ -466,6 +502,9 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<
rate = BigDecimal.TEN,
appCurrency = AppCurrency.Default,
),
displayNonceInput = true,
onNonceChange = {},
nonce = null,
),
),
)

View file

@ -2,8 +2,11 @@ package com.tangem.features.send.v2.send.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN
import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE
import com.tangem.core.analytics.models.AnalyticsParam.Key.NONCE
import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM
import com.tangem.core.ui.extensions.capitalize
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
/**
@ -18,11 +21,15 @@ internal sealed class SendAnalyticEvents(
data class TransactionScreenOpened(
val token: String,
val feeType: AnalyticsParam.FeeType,
val blockchain: String,
val nonceNotEmpty: Boolean,
) : SendAnalyticEvents(
event = "Transaction Sent Screen Opened",
params = mapOf(
TOKEN_PARAM to token,
FEE_TYPE to feeType.value,
BLOCKCHAIN to blockchain,
NONCE to nonceNotEmpty.toString().capitalize(),
),
)
}

View file

@ -26,6 +26,8 @@ internal class SendAnalyticHelper @Inject constructor(
SendAnalyticEvents.TransactionScreenOpened(
token = cryptoCurrency.symbol,
feeType = feeType,
blockchain = cryptoCurrency.network.name,
nonceNotEmpty = feeSelectorUM.nonce != null,
),
)
analyticsEventHandler.send(

View file

@ -320,6 +320,7 @@ internal class SendConfirmModel @Inject constructor(
val memo = destinationUM?.memoTextField?.value
val fee = feeSelectorUM?.selectedFee
val feeValue = fee?.amount?.value ?: return
val nonce = feeSelectorUM?.nonce
val receivingAmount = checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
@ -334,6 +335,7 @@ internal class SendConfirmModel @Inject constructor(
amount = receivingAmount.convertToSdkAmount(cryptoCurrency),
fee = fee,
memo = memo,
nonce = nonce,
destination = destination,
userWalletId = userWallet.walletId,
network = cryptoCurrency.network,

View file

@ -10,6 +10,8 @@ internal interface SendFeeClickIntents {
fun onCustomFeeValueChange(index: Int, value: String)
fun onNonceChange(value: String)
fun onReadMoreClick()
fun onNextClick()

View file

@ -9,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.NonceInserted
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.send.ui.state.ButtonsUM
@ -129,6 +130,12 @@ internal class SendFeeModel @Inject constructor(
updateFeeNotifications()
}
override fun onNonceChange(value: String) {
_uiState.update(
SendFeeNonceChangeTransformer(value = value),
)
}
override fun onReadMoreClick() {
val locale = if (Locale.getDefault().language == RU_LOCALE) RU_LOCALE else EN_LOCALE
val url = buildString {
@ -157,6 +164,16 @@ internal class SendFeeModel @Inject constructor(
),
)
if (feeSelectorUM.nonce != null) {
analyticsEventHandler.send(
NonceInserted(
categoryName = analyticsCategoryName,
token = cryptoCurrencyStatus.currency.symbol,
blockchain = cryptoCurrencyStatus.currency.network.name,
),
)
}
saveResult()
}
}

View file

@ -1,13 +1,14 @@
package com.tangem.features.send.v2.subcomponents.fee.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.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.fee.model.converters.FeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.SendFeeCustomFieldConverter
import com.tangem.utils.transformer.Transformer
internal class SendFeeLoadedTransformer(
@ -41,6 +42,7 @@ internal class SendFeeLoadedTransformer(
fees = fees,
customValues = customFeeFieldConverter.convert(fees.normal),
selectedFee = fees.normal,
nonce = prevState.nonce,
)
} else {
FeeSelectorUM.Content(
@ -53,12 +55,15 @@ internal class SendFeeLoadedTransformer(
customValues = feeSelectorUM.customValues,
),
),
nonce = prevState.nonce,
)
}
return state.copy(
feeSelectorUM = updatedFeeSelector,
isFeeApproximate = isFeeApproximate,
displayNonceInput = fees.normal is Fee.Ethereum,
nonce = prevState.nonce,
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.send.v2.subcomponents.fee.model.transformers
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.utils.transformer.Transformer
import java.math.BigInteger
internal class SendFeeNonceChangeTransformer(
private val value: String,
) : Transformer<FeeUM> {
override fun transform(prevState: FeeUM): FeeUM {
val state = prevState as? FeeUM.Content ?: return prevState
if (value.isEmpty()) {
return state.copy(nonce = null)
}
return try {
val nonce = BigInteger(value)
state.copy(
nonce = nonce,
feeSelectorUM = when (val feeSelectorUM = state.feeSelectorUM) {
is FeeSelectorUM.Loading,
is FeeSelectorUM.Error,
-> feeSelectorUM
is FeeSelectorUM.Content -> feeSelectorUM.copy(
nonce = nonce,
)
},
)
} catch (e: NumberFormatException) {
state
}
}
}

View file

@ -5,23 +5,31 @@ 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.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowEnter
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import kotlinx.collections.immutable.ImmutableList
@Suppress("LongParameterList")
@Composable
internal fun SendCustomFee(
customValues: ImmutableList<CustomFeeFieldUM>,
selectedFee: FeeType,
hasNotifications: Boolean,
onValueChange: (Int, String) -> Unit,
onNonceChange: (String) -> Unit,
nonce: String?,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
@ -77,6 +85,32 @@ internal fun SendCustomFee(
}
}
}
Nonce(
onNonceChange = onNonceChange,
nonce = nonce,
)
}
}
}
@Composable
private fun Nonce(onNonceChange: (String) -> Unit, nonce: String?) {
FooterContainer(
footer = resourceReference(R.string.send_nonce_footer),
modifier = Modifier.padding(bottom = 12.dp),
) {
InputRowEnter(
text = nonce.orEmpty(),
title = resourceReference(R.string.send_nonce),
onValueChange = onNonceChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
placeholder = resourceReference(R.string.send_nonce_hint),
showDivider = false,
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
),
)
}
}

View file

@ -40,6 +40,8 @@ internal fun SendFeeContent(state: FeeUM, clickIntents: SendFeeClickIntents) {
customFee(
feeSelectorUM = feeSelectorUM,
onValueChange = clickIntents::onCustomFeeValueChange,
onNonceChange = clickIntents::onNonceChange,
nonce = state.nonce?.toString().orEmpty(),
hasNotifications = hasNotifications,
)
}
@ -61,6 +63,8 @@ internal fun LazyListScope.customFee(
feeSelectorUM: FeeSelectorUM.Content,
hasNotifications: Boolean,
onValueChange: (Int, String) -> Unit,
onNonceChange: (String) -> Unit,
nonce: String?,
modifier: Modifier = Modifier,
) {
item(key = FEE_CUSTOM_KEY) {
@ -69,6 +73,8 @@ internal fun LazyListScope.customFee(
selectedFee = feeSelectorUM.selectedType,
hasNotifications = hasNotifications,
onValueChange = onValueChange,
onNonceChange = onNonceChange,
nonce = nonce,
modifier = modifier
.fillMaxWidth()
.animateItem()

View file

@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.transaction.error.GetFeeError
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigInteger
@Stable
internal sealed class FeeSelectorUM {
@ -16,6 +17,7 @@ internal sealed class FeeSelectorUM {
val selectedType: FeeType = FeeType.Market,
val selectedFee: Fee?,
val customValues: ImmutableList<CustomFeeFieldUM> = persistentListOf(),
val nonce: BigInteger? = null,
) : FeeSelectorUM()
data object Loading : FeeSelectorUM()

View file

@ -6,6 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.BigInteger
@Stable
internal sealed class FeeUM {
@ -22,6 +23,8 @@ internal sealed class FeeUM {
val isCustomSelected: Boolean,
val isTronToken: Boolean,
val customValues: ImmutableList<CustomFeeFieldUM> = persistentListOf(),
val displayNonceInput: Boolean = false,
val nonce: BigInteger? = null,
val notifications: ImmutableList<NotificationUM>,
val isEditingDisabled: Boolean = false,
) : FeeUM()