Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-27 15:59:43 +03:00
commit 71748eff67
517 changed files with 14212 additions and 3419 deletions

View file

@ -4,5 +4,8 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.send.v2.api.SendFeatureToggles
internal class DefaultSendFeatureToggles(
@Suppress("UnusedPrivateMember") private val featureToggles: FeatureTogglesManager,
) : SendFeatureToggles
private val featureToggles: FeatureTogglesManager,
) : SendFeatureToggles {
override val isSendRedesignEnabled: Boolean
get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED")
}

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

@ -0,0 +1,185 @@
package com.tangem.features.send.v2.feeselector
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.utils.getFiatString
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
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.extensions.stringResourceSafe
import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
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.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM
import com.tangem.features.send.v2.feeselector.entity.FeeItem
import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.entity.PrimaryButtonConfig
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
import com.tangem.features.send.v2.impl.R
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: FeeSelectorParams.FeeSelectorBlockParams,
) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext {
private val model: FeeSelectorModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
FeeSelectorBlockContent(modifier = modifier, state = state)
}
@AssistedFactory
interface Factory : FeeSelectorBlockComponent.Factory {
override fun create(
context: AppComponentContext,
params: FeeSelectorParams.FeeSelectorBlockParams,
): DefaultFeeSelectorBlockComponent
}
}
@Composable
private fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.background(TangemTheme.colors.background.action)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier.size(24.dp),
painter = painterResource(R.drawable.ic_fee_new_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
Text(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
text = stringResourceSafe(R.string.common_network_fee_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
Icon(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing6)
.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_token_info_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
SpacerWMax()
when (state) {
is FeeSelectorUM.Content -> FeeContent(state)
is FeeSelectorUM.Loading -> FeeLoading()
is FeeSelectorUM.Error -> FeeError()
}
}
}
@Composable
private fun RowScope.FeeError() {
Text(
text = EMPTY_BALANCE_SIGN,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body2,
)
}
@Composable
private fun RowScope.FeeLoading() {
TextShimmer(
radius = TangemTheme.dimens.radius3,
style = TangemTheme.typography.body1,
modifier = Modifier.width(width = TangemTheme.dimens.size90),
)
}
@Composable
private fun RowScope.FeeContent(state: FeeSelectorUM.Content) {
EllipsisText(
text = if (state.feeFiatRateUM != null) {
getFiatString(
value = state.selectedFeeItem.fee.amount.value,
rate = state.feeFiatRateUM.rate,
appCurrency = state.feeFiatRateUM.appCurrency,
approximate = state.isFeeApproximate,
)
} else {
state.selectedFeeItem.fee.amount.value.format {
crypto(
symbol = state.selectedFeeItem.fee.amount.currencySymbol,
decimals = state.selectedFeeItem.fee.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
}
},
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.End,
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing4),
)
Icon(
modifier = Modifier.size(width = 18.dp, height = 24.dp),
painter = painterResource(id = R.drawable.ic_select_18_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun FeeSelectorBlockContent_Preview() {
TangemThemePreview {
val feeItem = FeeItem.Market(
Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)),
)
FeeSelectorBlockContent(
modifier = Modifier.fillMaxWidth(),
state = FeeSelectorUM.Content(
doneButtonConfig = PrimaryButtonConfig(enabled = true, onClick = {}),
feeItems = persistentListOf(feeItem),
onFeeSelected = {},
selectedFeeItem = feeItem,
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("2500"),
appCurrency = AppCurrency.Default,
),
displayNonceInput = false,
nonce = null,
onNonceChange = {},
),
)
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.features.send.v2.feeselector
import androidx.activity.compose.BackHandler
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.features.send.v2.api.FeeSelectorComponent
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultFeeSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: FeeSelectorComponent.Params,
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
private val model: FeeSelectorModel = getOrCreateModel(params = params)
override fun dismiss() {
model.dismiss()
}
@Composable
override fun BottomSheet() {
FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = TODO())
}
// Temporary workaround, to use test this component
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = router::pop)
FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state)
}
@AssistedFactory
interface Factory : FeeSelectorComponent.Factory {
override fun create(
context: AppComponentContext,
params: FeeSelectorComponent.Params,
): DefaultFeeSelectorComponent
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.send.v2.feeselector.di
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.FeeSelectorComponent
import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorBlockComponent
import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface FeeSelectorFeatureModule {
@Binds
@Singleton
fun bindComponentFactory(factory: DefaultFeeSelectorComponent.Factory): FeeSelectorComponent.Factory
@Binds
@Singleton
fun bindBlockComponentFactory(factory: DefaultFeeSelectorBlockComponent.Factory): FeeSelectorBlockComponent.Factory
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.send.v2.feeselector.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
@Module
@InstallIn(ModelComponent::class)
internal interface FeeSelectorModelModule {
@Binds
@IntoMap
@ClassKey(FeeSelectorModel::class)
fun bindModel(model: FeeSelectorModel): Model
}

View file

@ -0,0 +1,56 @@
package com.tangem.features.send.v2.feeselector.entity
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.transaction.error.GetFeeError
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 {
abstract val doneButtonConfig: PrimaryButtonConfig
data class Loading(private val onDone: () -> Unit) : FeeSelectorUM() {
override val doneButtonConfig = PrimaryButtonConfig(enabled = false, onClick = onDone)
}
data class Error(val error: GetFeeError, private val onDone: () -> Unit) : FeeSelectorUM() {
override val doneButtonConfig = PrimaryButtonConfig(enabled = false, onClick = onDone)
}
data class Content(
override val doneButtonConfig: PrimaryButtonConfig,
val feeItems: ImmutableList<FeeItem>,
val onFeeSelected: (FeeItem) -> Unit,
val selectedFeeItem: FeeItem,
val isFeeApproximate: Boolean,
val feeFiatRateUM: FeeFiatRateUM?,
val displayNonceInput: Boolean,
val nonce: BigInteger?,
val onNonceChange: (String) -> Unit,
) : FeeSelectorUM()
}
internal data class PrimaryButtonConfig(val enabled: Boolean, val onClick: () -> Unit)
@Immutable
internal data class FeeFiatRateUM(
val rate: BigDecimal,
val appCurrency: AppCurrency,
)
@Immutable
internal sealed class FeeItem {
abstract val fee: Fee
data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem()
data class Slow(override val fee: Fee) : FeeItem()
data class Market(override val fee: Fee) : FeeItem()
data class Fast(override val fee: Fee) : FeeItem()
data class Custom(override val fee: Fee, val customValues: ImmutableList<CustomFeeFieldUM>) : FeeItem()
}

View file

@ -0,0 +1,101 @@
package com.tangem.features.send.v2.feeselector.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.blockchain.common.AmountType
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.navigation.Router
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.entity.FeeItem
import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.model.transformers.FeeItemSelectedTransformer
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer
import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.update
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class FeeSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<FeeSelectorParams.FeeSelectorBlockParams>()
private var appCurrency: AppCurrency = AppCurrency.Default
val uiState: StateFlow<FeeSelectorUM>
field = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading(onDone = ::onDone))
init {
initAppCurrency()
loadFee()
}
fun dismiss() {
router.pop()
}
private fun initAppCurrency() {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
}
}
private fun loadFee() {
modelScope.launch {
params.onLoadFee()
.fold(
ifLeft = { uiState.update(FeeSelectorErrorTransformer(it)) },
ifRight = {
uiState.update(
FeeSelectorLoadedTransformer(
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
appCurrency = appCurrency,
fees = it,
suggestedFeeState = params.suggestedFeeState,
isFeeApproximate = isFeeApproximate(it.normal.amount.type),
onFeeSelected = ::onFeeItemSelected,
onCustomFeeValueChange = ::onCustomFeeValueChange,
onNextClick = ::onNextClick,
),
)
},
)
}
}
private fun isFeeApproximate(amountType: AmountType): Boolean {
val networkId = params.network.id
return isFeeApproximateUseCase(networkId = networkId, amountType = amountType)
}
private fun onFeeItemSelected(feeItem: FeeItem) {
uiState.update(FeeItemSelectedTransformer(feeItem))
}
@Suppress("UnusedPrivateMember")
private fun onCustomFeeValueChange(index: Int, value: String) {
// TODO: [REDACTED_JIRA]
}
private fun onNextClick() {
// TODO: [REDACTED_JIRA]
}
private fun onDone() {
// TODO: [REDACTED_JIRA]
}
}

View file

@ -0,0 +1,64 @@
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.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.entity.FeeItem
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
internal class FeeItemConverter(
private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState,
private val normalFee: Fee,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
private val appCurrency: AppCurrency,
cryptoCurrencyStatus: CryptoCurrencyStatus,
) : Converter<TransactionFee, ImmutableList<FeeItem>> {
private val customFeeFieldConverter = FeeSelectorCustomFieldConverter(
onCustomFeeValueChange = onCustomFeeValueChange,
onNextClick = onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
normalFee = normalFee,
)
override fun convert(value: TransactionFee): ImmutableList<FeeItem> {
val fees = mutableListOf<FeeItem>()
when (suggestedFeeState) {
FeeSelectorParams.SuggestedFeeState.None -> Unit
is FeeSelectorParams.SuggestedFeeState.Suggestion -> fees.add(
FeeItem.Suggested(
title = suggestedFeeState.title,
fee = suggestedFeeState.fee,
),
)
}
when (value) {
is TransactionFee.Choosable -> {
fees.add(FeeItem.Slow(fee = value.minimum))
fees.add(FeeItem.Market(fee = value.normal))
fees.add(FeeItem.Fast(fee = value.priority))
}
is TransactionFee.Single -> {
fees.add(FeeItem.Market(fee = value.normal))
}
}
val customFeeFields = customFeeFieldConverter.convert(normalFee)
if (customFeeFields.isNotEmpty()) {
fees.add(
FeeItem.Custom(
fee = customFeeFieldConverter.convertBack(customFeeFields),
customValues = customFeeFields,
),
)
}
return fees.toImmutableList()
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.send.v2.feeselector.model.transformers
import com.tangem.features.send.v2.feeselector.entity.FeeItem
import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM
import com.tangem.utils.transformer.Transformer
internal class FeeItemSelectedTransformer(private val selectedFeeItem: FeeItem) : Transformer<FeeSelectorUM> {
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
prevState as? FeeSelectorUM.Content ?: return prevState
return prevState.copy(selectedFeeItem = selectedFeeItem)
}
}

View file

@ -0,0 +1,114 @@
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.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.utils.converter.TwoWayConverter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal class FeeSelectorCustomFieldConverter(
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
private val normalFee: Fee,
) : TwoWayConverter<Fee, ImmutableList<CustomFeeFieldUM>> {
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
EthereumCustomFeeConverter(
onCustomFeeValueChange = onCustomFeeValueChange,
onNextClick = onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
}
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
BitcoinCustomFeeConverter(
onCustomFeeValueChange = onCustomFeeValueChange,
onNextClick = onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
}
private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
KaspaCustomFeeConverter(
onCustomFeeValueChange = onCustomFeeValueChange,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
}
override fun convert(value: Fee): ImmutableList<CustomFeeFieldUM> {
return when (value) {
is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value)
is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value)
else -> persistentListOf()
}
}
override fun convertBack(value: ImmutableList<CustomFeeFieldUM>): Fee {
return if (value.isEmpty()) {
normalFee
} else {
when (normalFee) {
is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = value)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = value)
is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = value)
else -> {
val customFee = value.firstOrNull()
Fee.Common(
normalFee.amount.copy(
value = customFee?.value?.parseToBigDecimal(customFee.decimals),
),
)
}
}
}
}
fun onValueChange(feeSelectorState: FeeSelectorUM.Content, index: Int, value: String) =
when (val fee = feeSelectorState.fees.normal) {
is Fee.Ethereum -> ethereumCustomFeeConverter.onValueChange(
feeValue = fee,
customValues = feeSelectorState.customValues,
index = index,
value = value,
)
is Fee.Bitcoin -> bitcoinCustomFeeConverter.onValueChange(
customValues = feeSelectorState.customValues,
index = index,
value = value,
txSize = fee.txSize,
)
is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange(
customValues = feeSelectorState.customValues,
index = index,
value = value,
)
else -> feeSelectorState.customValues
}
fun tryAutoFixValue(feeSelectorState: FeeSelectorUM.Content) = when (feeSelectorState.fees) {
is TransactionFee.Choosable -> feeSelectorState.fees.minimum
is TransactionFee.Single -> feeSelectorState.fees.normal
}.let {
when (it) {
is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue(
minimumFee = it,
customValues = feeSelectorState.customValues,
)
else -> feeSelectorState.customValues
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.send.v2.feeselector.model.transformers
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM
import com.tangem.utils.transformer.Transformer
internal class FeeSelectorErrorTransformer(private val error: GetFeeError) : Transformer<FeeSelectorUM> {
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
return FeeSelectorUM.Error(error = error, onDone = prevState.doneButtonConfig.onClick)
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.send.v2.feeselector.model.transformers
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.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateUM
import com.tangem.features.send.v2.feeselector.entity.FeeItem
import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
@Suppress("LongParameterList")
internal class FeeSelectorLoadedTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val appCurrency: AppCurrency,
private val fees: TransactionFee,
private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState,
private val isFeeApproximate: Boolean,
private val onFeeSelected: (FeeItem) -> Unit,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
) : Transformer<FeeSelectorUM> {
private val feeItemsConverter = FeeItemConverter(
suggestedFeeState = suggestedFeeState,
normalFee = fees.normal,
onCustomFeeValueChange = onCustomFeeValueChange,
onNextClick = onNextClick,
appCurrency = appCurrency,
cryptoCurrencyStatus = cryptoCurrencyStatus,
)
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
val feeItems: ImmutableList<FeeItem> = feeItemsConverter.convert(fees)
val selectedFee = feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market }
return FeeSelectorUM.Content(
doneButtonConfig = prevState.doneButtonConfig.copy(enabled = true),
feeItems = feeItems,
selectedFeeItem = selectedFee,
isFeeApproximate = isFeeApproximate,
onFeeSelected = onFeeSelected,
feeFiatRateUM = cryptoCurrencyStatus.value.fiatRate?.let { rate ->
FeeFiatRateUM(
rate = rate,
appCurrency = appCurrency,
)
},
displayNonceInput = false,
nonce = null,
onNonceChange = {},
)
}
}

View file

@ -0,0 +1,544 @@
package com.tangem.features.send.v2.feeselector.ui
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
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
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
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.features.send.v2.feeselector.entity.FeeFiatRateUM
import com.tangem.features.send.v2.feeselector.entity.FeeItem
import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.entity.PrimaryButtonConfig
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
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) {
if (state !is FeeSelectorUM.Content) return
TangemModalBottomSheetWithFooter<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.primary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_network_fee_title),
startIconRes = R.drawable.ic_back_24,
onStartClick = onDismiss,
)
},
content = {
FeeSelectorItems(
state = state,
modifier = Modifier.padding(vertical = 4.dp, horizontal = 16.dp),
)
},
footer = {
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResourceSafe(R.string.common_done),
onClick = state.doneButtonConfig.onClick,
enabled = state.doneButtonConfig.enabled,
)
},
)
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
private fun FeeSelectorItems(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
state.feeItems.fastForEachIndexed { index, item ->
val isSelected = item == state.selectedFeeItem
val lastItem = index == state.feeItems.size - 1
val iconTint by animateColorAsState(
targetValue = if (isSelected) TangemTheme.colors.icon.accent else TangemTheme.colors.text.tertiary,
label = "Fee selector icon tint change",
)
val iconBackgroundColor by animateColorAsState(
targetValue = if (isSelected) {
TangemTheme.colors.icon.accent.copy(alpha = 0.1F)
} else {
TangemTheme.colors.background.secondary
},
label = "Fee selector icon background change",
)
val onSelect by rememberUpdatedState(state.onFeeSelected)
val itemModifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.primary)
.then(
if (isSelected) {
Modifier
.border(
width = 2.5.dp,
color = iconTint.copy(alpha = 0.2F),
shape = RoundedCornerShape(16.dp),
)
.padding(2.5.dp)
.border(width = 1.dp, color = iconTint, shape = RoundedCornerShape(14.dp))
.clip(RoundedCornerShape(14.dp))
} else {
Modifier
},
)
.clickableSingle(onClick = { onSelect(item) })
when (item) {
is FeeItem.Suggested -> RegularFeeItemContent(
modifier = itemModifier,
title = item.title,
iconRes = R.drawable.ic_star_mini_24,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
preDot = stringReference(
item.fee.amount.value.format {
crypto(
symbol = item.fee.amount.currencySymbol,
decimals = item.fee.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateUM != null) {
getFiatReference(
value = item.fee.amount.value,
rate = state.feeFiatRateUM.rate,
appCurrency = state.feeFiatRateUM.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.fee.amount.currencySymbol.length,
showDivider = !isSelected && !lastItem,
)
is FeeItem.Slow -> RegularFeeItemContent(
modifier = itemModifier,
title = resourceReference(R.string.common_fee_selector_option_slow),
iconRes = R.drawable.ic_tortoise_24,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
preDot = stringReference(
item.fee.amount.value.format {
crypto(
symbol = item.fee.amount.currencySymbol,
decimals = item.fee.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateUM != null) {
getFiatReference(
value = item.fee.amount.value,
rate = state.feeFiatRateUM.rate,
appCurrency = state.feeFiatRateUM.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.fee.amount.currencySymbol.length,
showDivider = !isSelected && !lastItem,
)
is FeeItem.Market -> RegularFeeItemContent(
modifier = itemModifier,
title = resourceReference(R.string.common_fee_selector_option_market),
iconRes = R.drawable.ic_bird_24,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
preDot = stringReference(
item.fee.amount.value.format {
crypto(
symbol = item.fee.amount.currencySymbol,
decimals = item.fee.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateUM != null) {
getFiatReference(
value = item.fee.amount.value,
rate = state.feeFiatRateUM.rate,
appCurrency = state.feeFiatRateUM.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.fee.amount.currencySymbol.length,
showDivider = !isSelected && !lastItem,
)
is FeeItem.Fast -> RegularFeeItemContent(
modifier = itemModifier,
title = resourceReference(R.string.common_fee_selector_option_fast),
iconRes = R.drawable.ic_hare_24,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
preDot = stringReference(
item.fee.amount.value.format {
crypto(
symbol = item.fee.amount.currencySymbol,
decimals = item.fee.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateUM != null) {
getFiatReference(
value = item.fee.amount.value,
rate = state.feeFiatRateUM.rate,
appCurrency = state.feeFiatRateUM.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.fee.amount.currencySymbol.length,
showDivider = !isSelected && !lastItem,
)
is FeeItem.Custom -> CustomFeeBlock(
modifier = itemModifier,
customFee = item,
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) {
Row(
modifier = Modifier.padding(all = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier
.size(36.dp)
.background(color = iconBackgroundColor, shape = CircleShape)
.padding(6.dp),
painter = painterResource(R.drawable.ic_edit_v2_24),
tint = iconTint,
contentDescription = null,
)
Text(
text = stringResourceSafe(R.string.common_custom),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
}
AnimatedVisibility(
visible = isSelected,
label = "Custom Fee Selected Animation",
enter = expandVertically().plus(fadeIn()),
exit = shrinkVertically().plus(fadeOut()),
) {
ExpandedCustomFeeItems(
customFeeFields = customFee.customValues,
onValueChange = { _, _ -> },
displayNonceInput = displayNonceInput,
nonce = nonce,
onNonceChange = onNonceChange,
)
}
}
}
@Composable
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 || displayNonceInput
if (field.label != null) {
InputRowEnterInfoAmountV2(
text = field.value,
decimals = field.decimals,
symbol = field.symbol,
title = field.title,
titleColor = TangemTheme.colors.text.tertiary,
info = field.label,
keyboardOptions = field.keyboardOptions,
keyboardActions = field.keyboardActions,
onValueChange = { onValueChange(index, it) },
showDivider = showDivider,
isReadOnly = field.isReadonly,
)
} else {
InputRowEnterInfoAmountV2(
text = field.value,
decimals = field.decimals,
title = field.title,
titleColor = TangemTheme.colors.text.tertiary,
symbol = field.symbol,
onValueChange = { onValueChange(index, it) },
keyboardOptions = field.keyboardOptions,
keyboardActions = field.keyboardActions,
showDivider = showDivider,
)
}
}
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,
)
}
}
}
@Composable
private fun RegularFeeItemContent(
title: TextReference,
@DrawableRes iconRes: Int,
iconBackgroundColor: Color,
iconTint: Color,
modifier: Modifier = Modifier,
preDot: TextReference? = null,
postDot: TextReference? = null,
ellipsizeOffset: Int? = null,
showDivider: Boolean = true,
) {
Column(modifier = modifier) {
Row(
modifier = Modifier.padding(all = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
modifier = Modifier
.size(36.dp)
.background(color = iconBackgroundColor, shape = CircleShape)
.padding(6.dp),
painter = painterResource(iconRes),
tint = iconTint,
contentDescription = null,
)
FeeDescription(
title = title,
preDot = preDot,
postDot = postDot,
ellipsizeOffset = ellipsizeOffset,
)
}
if (showDivider) {
HorizontalDivider(
modifier = Modifier.padding(start = 60.dp, end = 12.dp),
color = TangemTheme.colors.stroke.primary,
thickness = 0.5.dp,
)
}
}
}
@Composable
private fun FeeDescription(
title: TextReference,
modifier: Modifier = Modifier,
preDot: TextReference? = null,
postDot: TextReference? = null,
ellipsizeOffset: Int? = null,
) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
)
if (preDot != null) {
FeeValueContent(preDot = preDot, postDot = postDot, ellipsizeOffset = ellipsizeOffset)
}
}
}
@Composable
private fun FeeValueContent(preDot: TextReference, postDot: TextReference?, ellipsizeOffset: Int? = null) {
val ellipsis = if (ellipsizeOffset == null) {
TextEllipsis.End
} else {
TextEllipsis.OffsetEnd(ellipsizeOffset)
}
val textColor = TangemTheme.colors.text.tertiary
val textStyle = TangemTheme.typography.caption1
Row {
EllipsisText(
text = preDot.resolveReference(),
style = textStyle,
color = textColor,
textAlign = TextAlign.End,
ellipsis = ellipsis,
)
if (postDot != null) {
Text(
text = StringsSigns.DOT,
style = textStyle,
color = textColor,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4),
)
Text(text = postDot.resolveReference(), style = textStyle, color = textColor)
}
}
}
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun FeeSelectorBS_Preview(
@PreviewParameter(FeeSelectorUMContentProvider::class)
state: FeeSelectorUM.Content,
) {
TangemThemePreview {
FeeSelectorModalBottomSheet(onDismiss = {}, state = state)
}
}
private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<FeeSelectorUM.Content>(
collection = listOf(
FeeSelectorUM.Content(
doneButtonConfig = PrimaryButtonConfig(enabled = true, onClick = {}),
feeItems = persistentListOf(
FeeItem.Suggested(
title = stringReference("Suggested by Tangem"),
fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)),
),
FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))),
FeeItem.Market(fee = Fee.Common(Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum))),
FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))),
customFeeItem,
),
onFeeSelected = {},
// selectedFeeItem = FeeItem.Market(
// amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum),
// ),
selectedFeeItem = customFeeItem,
isFeeApproximate = true,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal.TEN,
appCurrency = AppCurrency.Default,
),
displayNonceInput = true,
onNonceChange = {},
nonce = null,
),
),
)
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",
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,
),
),
)

View file

@ -206,6 +206,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
childStack = childStack,
)
},
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
),
)
} else {
@ -225,6 +226,7 @@ internal class DefaultSendComponent @AssistedInject constructor(
return if (sendAmount != null && destinationAddress != null &&
feeCryptoCurrencyStatus != null && cryptoCurrencyStatus != null
) {
// TODO Apply new component [REDACTED_TASK_KEY]
SendFeeComponent(
appComponentContext = factoryContext,
params = SendFeeComponentParams.FeeParams(

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

@ -65,6 +65,7 @@ internal class SendConfirmComponent(
appCurrency = params.appCurrency,
blockClickEnableFlow = blockClickEnableFlow.asStateFlow(),
predefinedValues = params.predefinedValues,
isRedesignEnabled = model.uiState.value.isRedesignEnabled,
),
onResult = model::onAmountResult,
onClick = model::showEditAmount,

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,
@ -454,6 +456,7 @@ internal class SendConfirmModel @Inject constructor(
}
}
@Suppress("LongMethod")
private fun configConfirmNavigation() {
combine(
flow = uiState,
@ -470,7 +473,11 @@ internal class SendConfirmModel @Inject constructor(
id = R.string.send_summary_title,
formatArgs = wrappedList(params.cryptoCurrencyStatus.currency.name),
),
subtitle = amountUM?.title,
subtitle = if (uiState.value.isRedesignEnabled) {
null
} else {
amountUM?.title
},
backIconRes = R.drawable.ic_close_24,
backIconClick = {
analyticsEventHandler.send(

View file

@ -98,8 +98,13 @@ private fun LazyListScope.blocks(
modifier = Modifier.padding(vertical = 12.dp),
)
}
destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier)
if (uiState.isRedesignEnabled) {
amountBlockComponent.Content(modifier = Modifier)
destinationBlockComponent.Content(modifier = Modifier)
} else {
destinationBlockComponent.Content(modifier = Modifier)
amountBlockComponent.Content(modifier = Modifier)
}
feeBlockComponent.Content(modifier = Modifier)
}
}

View file

@ -37,6 +37,7 @@ import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.send.v2.api.SendComponent
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.common.CommonSendRoute
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.SendConfirmAlertFactory
@ -87,6 +88,7 @@ internal class SendModel @Inject constructor(
private val createTransferTransactionUseCase: CreateTransferTransactionUseCase,
private val getFeeUseCase: GetFeeUseCase,
private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendComponentCallback {
private val params: SendComponent.Params = paramsContainer.require()
@ -361,9 +363,11 @@ internal class SendModel @Inject constructor(
amountUM = AmountState.Empty(),
destinationUM = SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
).transform(DestinationUM.Empty()),
feeUM = FeeUM.Empty(),
confirmUM = ConfirmUM.Empty,
navigationUM = NavigationUM.Empty,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
)
}

View file

@ -12,4 +12,5 @@ internal data class SendUM(
val feeUM: FeeUM,
val confirmUM: ConfirmUM,
val navigationUM: NavigationUM,
val isRedesignEnabled: Boolean,
)

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountBlock
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
@ -37,11 +38,24 @@ internal class SendAmountBlockComponent(
val state by model.uiState.collectAsStateWithLifecycle()
val isClickEnabled by params.blockClickEnableFlow.collectAsStateWithLifecycle()
AmountBlock(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
if (params.isRedesignEnabled) {
val amountState = state as? AmountState.Data ?: return
AmountBlockV2(
amountState = state,
currencyIconState = amountState.tokenIconState,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
modifier = modifier,
)
} else {
AmountBlock(
amountState = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
onClick = onClick,
)
}
}
}

View file

@ -18,6 +18,7 @@ internal sealed class SendAmountComponentParams {
abstract val appCurrency: AppCurrency
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
abstract val predefinedValues: PredefinedValues
abstract val isRedesignEnabled: Boolean
data class AmountParams(
override val state: AmountState,
@ -26,6 +27,7 @@ internal sealed class SendAmountComponentParams {
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val predefinedValues: PredefinedValues,
override val isRedesignEnabled: Boolean,
val callback: ModelCallback,
val currentRoute: Flow<CommonSendRoute.Amount>,
val isBalanceHidingFlow: StateFlow<Boolean>,
@ -40,6 +42,7 @@ internal sealed class SendAmountComponentParams {
override val appCurrency: AppCurrency,
override val cryptoCurrencyStatus: CryptoCurrencyStatus,
override val predefinedValues: PredefinedValues,
override val isRedesignEnabled: Boolean,
val blockClickEnableFlow: StateFlow<Boolean>,
) : SendAmountComponentParams()
}

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.ui.state.NavigationUM
import com.tangem.features.send.v2.impl.R
@ -47,6 +48,7 @@ internal class SendAmountModel @Inject constructor(
private val feeReloadTrigger: SendFeeReloadTrigger,
private val sendAmountUpdateQRListener: SendAmountUpdateQRListener,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), AmountScreenClickIntents {
private val params: SendAmountComponentParams = paramsContainer.require()
@ -95,6 +97,7 @@ internal class SendAmountModel @Inject constructor(
cryptoCurrencyStatus = cryptoCurrencyStatus,
maxEnterAmount = maxAmountBoundary,
iconStateConverter = CryptoCurrencyToIconStateConverter(),
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
).convert(
AmountParameters(
title = stringReference(userWallet.name),

View file

@ -40,6 +40,7 @@ internal class SendDestinationBlockComponent(
destinationUM = state,
isClickDisabled = !isClickEnabled,
isEditingDisabled = params.predefinedValues is PredefinedValues.Content.Deeplink,
isRedesignEnabled = (params.state as? DestinationUM.Content)?.isRedesignEnabled ?: false,
onClick = onClick,
)
}

View file

@ -23,6 +23,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.isLocked
import com.tangem.domain.wallets.models.isMultiCurrency
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.send.v2.api.SendFeatureToggles
import com.tangem.features.send.v2.common.PredefinedValues
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource
@ -65,6 +66,7 @@ internal class SendDestinationModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val parseQrCodeUseCase: ParseQrCodeUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val sendFeatureToggles: SendFeatureToggles,
) : Model(), SendDestinationClickIntents {
private val params: SendDestinationComponentParams = paramsContainer.require()
@ -90,6 +92,7 @@ internal class SendDestinationModel @Inject constructor(
_uiState.update(
SendDestinationInitialStateTransformer(
cryptoCurrency = cryptoCurrency,
isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled,
isInitialized = true,
),
)
@ -297,6 +300,7 @@ internal class SendDestinationModel @Inject constructor(
flow2 = params.currentRoute,
transform = { state, route -> state to route },
).onEach { (state, route) ->
val isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled
params.callback.onNavigationResult(
NavigationUM.Content(
title = params.title,
@ -319,8 +323,16 @@ internal class SendDestinationModel @Inject constructor(
}
params.onBackClick()
},
additionalIconRes = R.drawable.ic_qrcode_scan_24,
additionalIconClick = ::onQrCodeScanClick,
additionalIconRes = if (isRedesignEnabled) {
null
} else {
R.drawable.ic_qrcode_scan_24
},
additionalIconClick = if (isRedesignEnabled) {
null
} else {
::onQrCodeScanClick
},
primaryButton = ButtonsUM.PrimaryButtonUM(
text = if (route.isEditMode) {
resourceReference(R.string.common_continue)

View file

@ -13,6 +13,7 @@ import com.tangem.utils.transformer.Transformer
internal class SendDestinationInitialStateTransformer(
val cryptoCurrency: CryptoCurrency,
val isRedesignEnabled: Boolean,
val isInitialized: Boolean = false,
) : Transformer<DestinationUM> {
override fun transform(prevState: DestinationUM): DestinationUM {
@ -54,6 +55,7 @@ internal class SendDestinationInitialStateTransformer(
recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT),
networkName = cryptoCurrency.network.name,
isValidating = false,
isRedesignEnabled = isRedesignEnabled,
)
}
}

View file

@ -1,26 +1,38 @@
package com.tangem.features.send.v2.subcomponents.destination.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM
import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun DestinationBlock(
destinationUM: DestinationUM,
isClickDisabled: Boolean,
isEditingDisabled: Boolean,
isRedesignEnabled: Boolean,
onClick: () -> Unit,
) {
if (destinationUM !is DestinationUM.Content) return
@ -33,8 +45,15 @@ internal fun DestinationBlock(
.clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick)
.padding(TangemTheme.dimens.spacing12),
) {
AddressBlock(destinationUM.addressTextField)
MemoBlock(destinationUM.memoTextField)
if (isRedesignEnabled) {
AddressWithMemoBlock(
address = destinationUM.addressTextField,
memo = destinationUM.memoTextField,
)
} else {
AddressBlock(destinationUM.addressTextField)
MemoBlock(destinationUM.memoTextField)
}
}
}
@ -84,4 +103,124 @@ private fun MemoBlock(memo: DestinationTextFieldUM.RecipientMemo?) {
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
@Composable
private fun AddressWithMemoBlock(
address: DestinationTextFieldUM.RecipientAddress,
memo: DestinationTextFieldUM.RecipientMemo?,
) {
Text(
text = stringResourceSafe(R.string.send_to_address),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24),
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
) {
Text(
modifier = Modifier.weight(1f),
text = address.value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
IdentIcon(
address = address.value,
modifier = Modifier
.size(TangemTheme.dimens.size36)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.background(TangemTheme.colors.background.tertiary),
)
}
if (memo != null && memo.value.isNotBlank()) {
Text(
text = stringResourceSafe(R.string.send_memo, memo.value),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DestinationBlockPreview(
@PreviewParameter(DestinationBlockPreviewProvider::class) state: DestinationUM.Content,
) {
TangemThemePreview {
DestinationBlock(
destinationUM = state,
isClickDisabled = false,
isEditingDisabled = false,
isRedesignEnabled = state.isRedesignEnabled,
onClick = {},
)
}
}
private class DestinationBlockPreviewProvider : PreviewParameterProvider<DestinationUM.Content> {
override val values: Sequence<DestinationUM.Content>
get() = sequenceOf(
DestinationUM.Content(
isPrimaryButtonEnabled = true,
addressTextField = DestinationTextFieldUM.RecipientAddress(
value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter address"),
label = TextReference.Str("Recipient Address"),
isError = false,
error = null,
isValuePasted = false,
),
memoTextField = DestinationTextFieldUM.RecipientMemo(
value = "Test memo for transaction",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter memo (optional)"),
label = TextReference.Str("Memo"),
isError = false,
error = null,
disabledText = TextReference.Str("Memo disabled"),
isEnabled = true,
isValuePasted = false,
),
recent = emptyList<DestinationRecipientListUM>().toImmutableList(),
wallets = emptyList<DestinationRecipientListUM>().toImmutableList(),
networkName = "Ethereum",
isValidating = false,
isInitialized = true,
isRedesignEnabled = false,
),
DestinationUM.Content(
isPrimaryButtonEnabled = true,
addressTextField = DestinationTextFieldUM.RecipientAddress(
value = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter address"),
label = TextReference.Str("Recipient Address"),
isError = false,
error = null,
isValuePasted = false,
),
memoTextField = DestinationTextFieldUM.RecipientMemo(
value = "Test memo for transaction",
keyboardOptions = KeyboardOptions.Default,
placeholder = TextReference.Str("Enter memo (optional)"),
label = TextReference.Str("Memo"),
isError = false,
error = null,
disabledText = TextReference.Str("Memo disabled"),
isEnabled = true,
isValuePasted = false,
),
recent = emptyList<DestinationRecipientListUM>().toImmutableList(),
wallets = emptyList<DestinationRecipientListUM>().toImmutableList(),
networkName = "Ethereum",
isValidating = false,
isInitialized = true,
isRedesignEnabled = true,
),
)
}

View file

@ -16,6 +16,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowRecipient
@ -60,10 +64,13 @@ internal fun SendDestinationContent(
isError = isError,
isValidating = isValidating,
onAddressChange = clickIntents::onRecipientAddressValueChange,
onQrCodeClick = clickIntents::onQrCodeScanClick,
isRedesignEnabled = state.isRedesignEnabled,
)
memoField(
memoField = memoField,
onMemoChange = clickIntents::onRecipientMemoValueChange,
isRedesignEnabled = state.isRedesignEnabled,
)
listHeaderItem(
titleRes = R.string.send_recipient_wallets_title,
@ -100,16 +107,23 @@ internal fun SendDestinationContent(
}
}
@Suppress("LongParameterList")
private fun LazyListScope.addressItem(
address: DestinationTextFieldUM.RecipientAddress,
networkName: String,
isError: Boolean,
isValidating: Boolean,
onAddressChange: (String, EnterAddressSource) -> Unit,
onQrCodeClick: () -> Unit,
isRedesignEnabled: Boolean,
) {
item(key = ADDRESS_FIELD_KEY) {
FooterContainer(
footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)),
footer = if (isRedesignEnabled) {
buildHighlightedFooterText(networkName)
} else {
resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName))
},
) {
InputRowRecipient(
value = address.value,
@ -117,6 +131,8 @@ private fun LazyListScope.addressItem(
placeholder = address.placeholder,
onValueChange = { onAddressChange(it, EnterAddressSource.InputField) },
onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) },
onQrCodeClick = onQrCodeClick,
isRedesignEnabled = isRedesignEnabled,
isError = isError,
isLoading = isValidating,
error = address.error,
@ -131,9 +147,37 @@ private fun LazyListScope.addressItem(
}
}
@Composable
private fun buildHighlightedFooterText(networkName: String): TextReference {
return annotatedReference(
buildAnnotatedString {
val styledText = stringResourceSafe(
R.string.send_recipient_address_footer_highlighted_part,
networkName,
)
val styledColor = TangemTheme.colors.text.secondary
appendWithStyledPlaceholder(
template = stringResourceSafe(
R.string.send_recipient_address_footer_v2,
networkName,
),
placeholder = networkName,
) {
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
appendColored(
text = styledText,
color = styledColor,
)
}
}
},
)
}
private fun LazyListScope.memoField(
memoField: DestinationTextFieldUM.RecipientMemo?,
onMemoChange: (String, Boolean) -> Unit,
isRedesignEnabled: Boolean,
) {
if (memoField != null) {
item(key = MEMO_FIELD_KEY) {
@ -142,7 +186,24 @@ private fun LazyListScope.memoField(
value = memoField.value,
label = memoField.label,
placeholder = placeholder,
footer = resourceReference(R.string.send_recipient_memo_footer),
footer = if (isRedesignEnabled) {
annotatedReference(
buildAnnotatedString {
append(stringResourceSafe(R.string.send_recipient_memo_footer_v2))
append("\n")
withStyle(SpanStyle(fontWeight = FontWeight.Medium)) {
appendColored(
text = stringResourceSafe(
R.string.send_recipient_memo_footer_v2_highlighted,
),
color = TangemTheme.colors.text.secondary,
)
}
},
)
} else {
resourceReference(R.string.send_recipient_memo_footer)
},
onValueChange = { onMemoChange(it, false) },
onPasteClick = { onMemoChange(it, true) },
modifier = Modifier.padding(top = 20.dp),
@ -151,6 +212,7 @@ private fun LazyListScope.memoField(
error = memoField.error,
isReadOnly = !memoField.isEnabled,
isValuePasted = memoField.isValuePasted,
isRedesignEnabled = isRedesignEnabled,
)
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.containers.FooterContainer
@Suppress("LongMethod", "LongParameterList")
@Composable
internal fun TextFieldWithPaste(
value: String,
@ -27,6 +28,7 @@ internal fun TextFieldWithPaste(
label: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
isRedesignEnabled: Boolean,
modifier: Modifier = Modifier,
footer: TextReference? = null,
labelStyle: TextStyle = TangemTheme.typography.body2,
@ -96,6 +98,18 @@ internal fun TextFieldWithPaste(
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
backgroundColorEnabled = if (isRedesignEnabled) {
TangemTheme.colors.button.secondary
} else {
TangemTheme.colors.button.primary
},
textColor = if (isRedesignEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary2
},
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
)
}
}

View file

@ -17,6 +17,7 @@ internal sealed class DestinationUM {
val networkName: String,
val isValidating: Boolean = false,
val isInitialized: Boolean = false,
val isRedesignEnabled: Boolean = false,
) : DestinationUM()
data class Empty(

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

@ -4,8 +4,8 @@ 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.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList

View file

@ -5,11 +5,11 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.utils.converter.TwoWayConverter
import kotlinx.collections.immutable.ImmutableList
@ -24,7 +24,8 @@ internal class SendFeeCustomFieldConverter(
private val ethereumCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
EthereumCustomFeeConverter(
clickIntents = clickIntents,
onCustomFeeValueChange = clickIntents::onCustomFeeValueChange,
onNextClick = clickIntents::onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
@ -32,7 +33,8 @@ internal class SendFeeCustomFieldConverter(
private val bitcoinCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
BitcoinCustomFeeConverter(
clickIntents = clickIntents,
onCustomFeeValueChange = clickIntents::onCustomFeeValueChange,
onNextClick = clickIntents::onNextClick,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
@ -40,7 +42,7 @@ internal class SendFeeCustomFieldConverter(
private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) {
KaspaCustomFeeConverter(
clickIntents = clickIntents,
onCustomFeeValueChange = clickIntents::onCustomFeeValueChange,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)

View file

@ -1,7 +1,7 @@
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList

View file

@ -11,11 +11,10 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.lib.crypto.BlockchainUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -24,7 +23,8 @@ import java.math.BigDecimal
import java.math.RoundingMode
internal class BitcoinCustomFeeConverter(
private val clickIntents: SendFeeClickIntents,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : CustomFeeConverter<Fee.Bitcoin> {
@ -40,7 +40,7 @@ internal class BitcoinCustomFeeConverter(
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
decimals = value.amount.decimals,
symbol = value.amount.currencySymbol,
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
onValueChange = { onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Companion.Next,
keyboardType = KeyboardType.Companion.Number,
@ -65,7 +65,7 @@ internal class BitcoinCustomFeeConverter(
symbol = "",
title = resourceReference(R.string.send_satoshi_per_byte_title),
footer = resourceReference(R.string.send_satoshi_per_byte_text),
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) },
onValueChange = { onCustomFeeValueChange(FEE_SATOSHI_INDEX, it) },
keyboardOptions = KeyboardOptions(
imeAction = if (checkExceedBalance(
feeBalance = currencyStatus.amount,
@ -78,9 +78,7 @@ internal class BitcoinCustomFeeConverter(
},
keyboardType = KeyboardType.Companion.Number,
),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onNextClick() },
),
keyboardActions = KeyboardActions(onDone = { onNextClick() }),
),
)
} else {

View file

@ -1,8 +1,8 @@
package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
/**

View file

@ -10,15 +10,15 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
internal class EthereumCustomFeeConverter(
private val clickIntents: SendFeeClickIntents,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val onNextClick: () -> Unit,
private val appCurrency: AppCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : BaseEthereumCustomFeeConverter<Fee.Ethereum> {
@ -26,13 +26,13 @@ internal class EthereumCustomFeeConverter(
private val currencyStatus = feeCryptoCurrencyStatus.value
private val legacyFeeConverter = EthereumLegacyCustomFeeConverter(
clickIntents = clickIntents,
onCustomFeeValueChange = onCustomFeeValueChange,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
private val eipFeeConverter = EthereumEIPCustomFeeConverter(
clickIntents = clickIntents,
onCustomFeeValueChange = onCustomFeeValueChange,
appCurrency = appCurrency,
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
)
@ -84,7 +84,7 @@ internal class EthereumCustomFeeConverter(
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
decimals = value.amount.decimals,
symbol = value.amount.currencySymbol,
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT, it) },
onValueChange = { onCustomFeeValueChange(FEE_AMOUNT, it) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
title = resourceReference(R.string.send_max_fee),
footer = resourceReference(R.string.send_custom_amount_fee_footer),
@ -106,13 +106,13 @@ internal class EthereumCustomFeeConverter(
symbol = "",
title = resourceReference(R.string.send_gas_limit),
footer = resourceReference(R.string.send_gas_limit_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(getGasLimitIndex(value), it) },
onValueChange = { onCustomFeeValueChange(getGasLimitIndex(value), it) },
keyboardOptions = KeyboardOptions(
imeAction = if (isExceedBalance) ImeAction.None else ImeAction.Done,
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = { clickIntents.onNextClick() },
onDone = { onNextClick() },
),
)
}

View file

@ -11,22 +11,21 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.RoundingMode
internal class EthereumEIPCustomFeeConverter(
private val clickIntents: SendFeeClickIntents,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val appCurrency: AppCurrency,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : BaseEthereumCustomFeeConverter<Fee.Ethereum.EIP1559> {
@ -41,7 +40,7 @@ internal class EthereumEIPCustomFeeConverter(
symbol = ETHEREUM_GAS_UNIT,
title = resourceReference(R.string.send_custom_evm_max_fee),
footer = resourceReference(R.string.send_custom_evm_max_fee_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(MAX_FEE, it) },
onValueChange = { onCustomFeeValueChange(MAX_FEE, it) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
keyboardActions = KeyboardActions(),
),
@ -51,7 +50,7 @@ internal class EthereumEIPCustomFeeConverter(
symbol = ETHEREUM_GAS_UNIT,
title = resourceReference(R.string.send_custom_evm_priority_fee),
footer = resourceReference(R.string.send_custom_evm_priority_fee_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(PRIORITY_FEE, it) },
onValueChange = { onCustomFeeValueChange(PRIORITY_FEE, it) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Number),
keyboardActions = KeyboardActions(),
),

View file

@ -11,22 +11,21 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.ETHEREUM_GAS_UNIT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.FEE_AMOUNT
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.RoundingMode
internal class EthereumLegacyCustomFeeConverter(
private val clickIntents: SendFeeClickIntents,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val appCurrency: AppCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : BaseEthereumCustomFeeConverter<Fee.Ethereum.Legacy> {
@ -41,7 +40,7 @@ internal class EthereumLegacyCustomFeeConverter(
symbol = ETHEREUM_GAS_UNIT,
title = resourceReference(R.string.send_gas_price),
footer = resourceReference(R.string.send_gas_price_footer),
onValueChange = { clickIntents.onCustomFeeValueChange(GAS_PRICE, it) },
onValueChange = { onCustomFeeValueChange(GAS_PRICE, it) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Number,

View file

@ -11,17 +11,16 @@ import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.impl.R
import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.RoundingMode
internal class KaspaCustomFeeConverter(
private val clickIntents: SendFeeClickIntents,
private val onCustomFeeValueChange: (Int, String) -> Unit,
private val appCurrency: AppCurrency,
feeCryptoCurrencyStatus: CryptoCurrencyStatus,
) : CustomFeeConverter<Fee.Kaspa> {
@ -35,7 +34,7 @@ internal class KaspaCustomFeeConverter(
value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(),
decimals = value.amount.decimals,
symbol = value.amount.currencySymbol,
onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
onValueChange = { onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) },
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Companion.Next,
keyboardType = KeyboardType.Companion.Number,

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.feeselector.api.entity.CustomFeeFieldUM
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

@ -0,0 +1,20 @@
package com.tangem.features.send.v2.subcomponents.fee.ui.state
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
data class CustomFeeFieldUM(
val value: String,
val onValueChange: (String) -> Unit,
val keyboardOptions: KeyboardOptions,
val keyboardActions: KeyboardActions,
val symbol: String?,
val decimals: Int,
val title: TextReference,
val footer: TextReference,
val label: TextReference? = null,
val isReadonly: Boolean = false,
)

View file

@ -5,9 +5,9 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigInteger
@Stable
internal sealed class FeeSelectorUM {
@ -17,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

@ -3,10 +3,10 @@ package com.tangem.features.send.v2.subcomponents.fee.ui.state
import androidx.compose.runtime.Stable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.BigInteger
@Stable
internal sealed class FeeUM {
@ -23,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()