Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-06 12:22:13 +05:00
parent 43c6359d2f
commit 880668b575
32 changed files with 898 additions and 17 deletions

View file

@ -207,6 +207,8 @@ dependencies {
implementation(projects.features.nft.impl)
implementation(projects.features.walletconnect.api)
implementation(projects.features.walletconnect.impl)
implementation(projects.features.feeSelector.api)
implementation(projects.features.feeSelector.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

@ -59,6 +59,7 @@ fun AmountTextField(
textStyle: TextStyle,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.text.primary1,
backgroundColor: Color = TangemTheme.colors.background.action,
visualTransformation: VisualTransformation = AmountVisualTransformation(decimals),
keyboardOptions: KeyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
@ -117,7 +118,7 @@ fun AmountTextField(
singleLine = true,
readOnly = !isEnabled,
visualTransformation = visualTransformation,
modifier = Modifier.background(TangemTheme.colors.background.action),
modifier = Modifier.background(backgroundColor),
)
}
}

View file

@ -1,16 +1,17 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
@ -106,3 +107,76 @@ fun InputRowEnterInfoAmount(
}
}
}
@Composable
fun InputRowEnterInfoAmountV2(
title: TextReference,
text: String,
decimals: Int,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
symbol: String? = null,
info: TextReference? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
infoColor: Color = TangemTheme.colors.text.tertiary,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
showDivider: Boolean = false,
isReadOnly: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
paddingValues = PaddingValues(horizontal = 12.dp),
) {
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Icon(
modifier = Modifier.size(16.dp),
painter = painterResource(R.drawable.ic_union_16),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
Row {
AmountTextField(
value = text,
decimals = decimals,
visualTransformation = AmountVisualTransformation(
decimals = decimals,
symbol = symbol,
decimalFormat = rememberDecimalFormat(),
),
onValueChange = onValueChange,
color = textColor,
isEnabled = !isReadOnly,
textStyle = TangemTheme.typography.body2,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
backgroundColor = Color.Transparent,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),
)
info?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = infoColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
)
}
}
}
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#1E1E1E"
android:pathData="M6.749,18.142L16.677,8.183L14.812,6.309L4.879,16.267L4.026,18.424C3.9,18.765 4.249,19.094 4.556,18.976L6.749,18.142ZM17.598,7.269L18.614,6.263C19.106,5.77 19.131,5.201 18.669,4.734L18.266,4.325C17.808,3.862 17.232,3.904 16.736,4.388L15.724,5.395L17.598,7.269Z" />
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#0099FF"
android:fillType="evenOdd"
android:pathData="M11.481,6.351C11.481,6.351 11.481,6.351 11.481,6.351L10.245,8.844C10.094,9.153 9.847,9.409 9.601,9.593C9.355,9.777 9.041,9.94 8.706,9.997L8.704,9.998L6.46,10.373C5.875,10.472 5.77,10.688 5.758,10.726C5.745,10.766 5.709,11.009 6.129,11.432L7.874,13.191C8.134,13.453 8.3,13.795 8.391,14.114C8.482,14.434 8.519,14.81 8.44,15.166L8.438,15.171L7.939,17.349C7.855,17.713 7.854,17.969 7.884,18.128C7.898,18.205 7.918,18.251 7.932,18.275C7.944,18.297 7.952,18.303 7.953,18.303C7.953,18.304 7.961,18.31 7.984,18.315C8.009,18.32 8.057,18.325 8.133,18.316C8.29,18.296 8.53,18.218 8.849,18.027L10.958,16.768C11.293,16.572 11.685,16.495 12.037,16.495C12.387,16.495 12.781,16.572 13.115,16.771C13.115,16.771 13.115,16.771 13.115,16.771L15.218,18.027C15.218,18.027 15.218,18.027 15.218,18.027C15.539,18.218 15.781,18.296 15.939,18.317C16.016,18.326 16.064,18.322 16.089,18.316C16.101,18.314 16.109,18.311 16.112,18.309C16.116,18.308 16.117,18.306 16.118,18.306C16.119,18.306 16.12,18.304 16.123,18.301C16.126,18.298 16.131,18.291 16.138,18.279C16.151,18.256 16.171,18.21 16.185,18.133C16.214,17.972 16.213,17.715 16.129,17.348L15.629,15.166C15.55,14.81 15.586,14.434 15.677,14.114C15.768,13.795 15.935,13.453 16.194,13.191L17.939,11.432L17.941,11.431C18.364,11.007 18.326,10.766 18.314,10.727C18.301,10.689 18.194,10.472 17.608,10.373L15.365,9.998C15.027,9.941 14.711,9.778 14.462,9.594C14.215,9.41 13.967,9.153 13.816,8.844L12.579,6.348C12.297,5.777 12.055,5.75 12.033,5.75C12.01,5.75 11.766,5.78 11.481,6.351ZM13.923,5.683C13.536,4.9 12.896,4.25 12.033,4.25C11.172,4.25 10.529,4.898 10.139,5.682L10.138,5.683L8.9,8.18L8.897,8.185C8.879,8.222 8.817,8.306 8.703,8.391C8.59,8.476 8.493,8.512 8.454,8.519L6.212,8.894C5.361,9.037 4.583,9.467 4.328,10.273C4.073,11.076 4.457,11.877 5.064,12.489L6.809,14.248C6.845,14.284 6.907,14.379 6.948,14.524C6.989,14.667 6.988,14.783 6.976,14.839L6.477,17.014C6.477,17.014 6.477,17.014 6.477,17.014C6.265,17.937 6.312,18.959 7.064,19.512C7.82,20.068 8.807,19.8 9.619,19.315C9.619,19.315 9.619,19.315 9.619,19.315L11.719,18.061C11.771,18.031 11.885,17.995 12.037,17.995C12.191,17.995 12.3,18.032 12.346,18.059L14.45,19.315C15.261,19.798 16.251,20.07 17.006,19.515C17.759,18.961 17.803,17.938 17.591,17.014L17.093,14.839C17.081,14.783 17.08,14.667 17.12,14.524C17.161,14.379 17.223,14.284 17.259,14.248L19.002,12.49C19.003,12.49 19.003,12.49 19.003,12.489C19.614,11.877 19.999,11.076 19.743,10.271C19.486,9.466 18.707,9.037 17.857,8.894L15.613,8.518C15.613,8.518 15.613,8.518 15.613,8.518C15.57,8.511 15.471,8.475 15.358,8.39C15.243,8.305 15.182,8.222 15.164,8.185L13.923,5.683C13.924,5.684 13.923,5.683 13.923,5.683Z" />
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:autoMirrored="true"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:fillColor="#919191"
android:pathData="M8,1.5C9.724,1.5 11.378,2.184 12.597,3.403C13.816,4.622 14.5,6.276 14.5,8C14.5,9.724 13.816,11.378 12.597,12.597C11.378,13.816 9.724,14.5 8,14.5C6.276,14.5 4.622,13.816 3.403,12.597C2.184,11.378 1.5,9.724 1.5,8C1.5,6.276 2.184,4.622 3.403,3.403C4.622,2.184 6.276,1.5 8,1.5ZM8,2.5C6.541,2.5 5.143,3.08 4.111,4.111C3.08,5.143 2.5,6.541 2.5,8C2.5,9.459 3.08,10.857 4.111,11.889C5.143,12.92 6.541,13.5 8,13.5C9.459,13.5 10.857,12.92 11.889,11.889C12.92,10.857 13.5,9.459 13.5,8C13.5,6.541 12.92,5.143 11.889,4.111C10.857,3.08 9.459,2.5 8,2.5ZM8,6.667C8.133,6.667 8.26,6.72 8.354,6.813C8.447,6.907 8.5,7.034 8.5,7.167V10.667H9C9.133,10.667 9.26,10.72 9.354,10.814C9.447,10.907 9.5,11.035 9.5,11.167C9.5,11.3 9.447,11.427 9.354,11.521C9.26,11.614 9.133,11.667 9,11.667H7C6.867,11.667 6.74,11.614 6.646,11.521C6.553,11.427 6.5,11.3 6.5,11.167C6.5,11.035 6.553,10.907 6.646,10.814C6.74,10.72 6.867,10.667 7,10.667H7.5V7.667H7.333C7.201,7.667 7.073,7.614 6.979,7.521C6.886,7.427 6.833,7.299 6.833,7.167C6.833,7.035 6.886,6.907 6.979,6.813C7.073,6.72 7.201,6.667 7.333,6.667H8ZM8,4.333C8.46,4.333 8.833,4.707 8.833,5.167C8.833,5.627 8.46,6 8,6C7.54,6 7.167,5.627 7.167,5.167C7.167,4.707 7.54,4.333 8,4.333Z" />
</vector>

1
features/fee-selector/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,27 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.feeselector.api"
}
dependencies {
/** Project - Core */
implementation(projects.core.ui)
implementation(projects.core.decompose)
/** Domain models */
// api(projects.domain.models)
implementation(projects.domain.appCurrency.models)
// implementation(projects.domain.wallets.models)
/** Compose */
implementation(deps.compose.runtime)
implementation(deps.compose.foundation)
/** Other */
implementation(deps.kotlin.immutable.collections)
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.feeselector.api.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
interface FeeSelectorComponent : ComposableContentComponent, ComposableBottomSheetComponent {
data class Params(val appCurrency: AppCurrency)
interface Factory : ComponentFactory<Params, FeeSelectorComponent>
}

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.v2.subcomponents.fee.ui.state
package com.tangem.features.feeselector.api.entity
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -6,7 +6,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal data class CustomFeeFieldUM(
data class CustomFeeFieldUM(
val value: String,
val onValueChange: (String) -> Unit,
val keyboardOptions: KeyboardOptions,

1
features/fee-selector/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,57 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.feeselector.impl"
}
dependencies {
/** Project - API */
implementation(projects.features.feeSelector.api)
/** Project - Core */
implementation(projects.common.ui)
implementation(projects.common.routing)
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.pagination)
implementation(projects.core.navigation)
/** Project - Domain */
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** AndroidX */
implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)
/** Compose */
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.shimmer)
/*** Tangem SDK */
implementation(tangemDeps.blockchain)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
implementation(deps.decompose.ext.compose)
implementation(deps.timber)
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.feeselector.impl.component
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.feeselector.api.component.FeeSelectorComponent
import com.tangem.features.feeselector.impl.model.FeeSelectorModel
import com.tangem.features.feeselector.impl.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) {
BackHandler(onBack = router::pop)
FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = TODO())
}
@AssistedFactory
interface Factory : FeeSelectorComponent.Factory {
override fun create(
context: AppComponentContext,
params: FeeSelectorComponent.Params,
): DefaultFeeSelectorComponent
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.feeselector.impl.di
import com.tangem.features.feeselector.api.component.FeeSelectorComponent
import com.tangem.features.feeselector.impl.component.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
}

View file

@ -0,0 +1,19 @@
package com.tangem.features.feeselector.impl.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.feeselector.impl.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,40 @@
package com.tangem.features.feeselector.impl.entity
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.Amount
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@Immutable
internal sealed class FeeSelectorUM {
data object Loading : FeeSelectorUM()
data class Content(
val isDoneEnabled: Boolean,
val feeItems: ImmutableList<FeeItem>,
val selectedFeeItem: FeeItem,
val isFeeApproximate: Boolean,
val feeFiatRateDataHolder: FeeFiatRateDataHolder?,
) : FeeSelectorUM()
}
@Immutable
data class FeeFiatRateDataHolder(
val rate: BigDecimal,
val appCurrency: AppCurrency,
)
@Immutable
internal sealed class FeeItem {
abstract val onSelect: () -> Unit
data class Suggested(val title: TextReference, val amount: Amount, override val onSelect: () -> Unit) : FeeItem()
data class Slow(val amount: Amount, override val onSelect: () -> Unit) : FeeItem()
data class Market(val amount: Amount, override val onSelect: () -> Unit) : FeeItem()
data class Fast(val amount: Amount, override val onSelect: () -> Unit) : FeeItem()
data class Custom(val customValues: ImmutableList<CustomFeeFieldUM>, override val onSelect: () -> Unit) : FeeItem()
}

View file

@ -0,0 +1,31 @@
package com.tangem.features.feeselector.impl.model
import androidx.compose.runtime.Stable
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.features.feeselector.api.component.FeeSelectorComponent
import com.tangem.features.feeselector.impl.entity.FeeSelectorUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class FeeSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<FeeSelectorComponent.Params>()
val uiState: StateFlow<FeeSelectorUM>
field = MutableStateFlow<FeeSelectorUM>(FeeSelectorUM.Loading)
fun dismiss() {
router.pop()
}
}

View file

@ -0,0 +1,510 @@
package com.tangem.features.feeselector.impl.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.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.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.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.feeselector.impl.R
import com.tangem.features.feeselector.impl.entity.FeeFiatRateDataHolder
import com.tangem.features.feeselector.impl.entity.FeeItem
import com.tangem.features.feeselector.impl.entity.FeeSelectorUM
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@Composable
internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM.Content, onDismiss: () -> Unit) {
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 = onDismiss,
enabled = state.isDoneEnabled,
)
},
)
}
@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(item.onSelect)
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)
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.amount.value.format {
crypto(
symbol = item.amount.currencySymbol,
decimals = item.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateDataHolder != null) {
getFiatReference(
value = item.amount.value,
rate = state.feeFiatRateDataHolder.rate,
appCurrency = state.feeFiatRateDataHolder.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.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.amount.value.format {
crypto(
symbol = item.amount.currencySymbol,
decimals = item.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateDataHolder != null) {
getFiatReference(
value = item.amount.value,
rate = state.feeFiatRateDataHolder.rate,
appCurrency = state.feeFiatRateDataHolder.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.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.amount.value.format {
crypto(
symbol = item.amount.currencySymbol,
decimals = item.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateDataHolder != null) {
getFiatReference(
value = item.amount.value,
rate = state.feeFiatRateDataHolder.rate,
appCurrency = state.feeFiatRateDataHolder.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.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.amount.value.format {
crypto(
symbol = item.amount.currencySymbol,
decimals = item.amount.decimals,
).fee(canBeLower = state.isFeeApproximate)
},
),
postDot = if (state.feeFiatRateDataHolder != null) {
getFiatReference(
value = item.amount.value,
rate = state.feeFiatRateDataHolder.rate,
appCurrency = state.feeFiatRateDataHolder.appCurrency,
)
} else {
null
},
ellipsizeOffset = item.amount.currencySymbol.length,
showDivider = !isSelected && !lastItem,
)
is FeeItem.Custom -> CustomFeeBlock(
modifier = itemModifier,
customFee = item,
isSelected = isSelected,
iconBackgroundColor = iconBackgroundColor,
iconTint = iconTint,
)
}
}
}
}
@Composable
private fun CustomFeeBlock(
customFee: FeeItem.Custom,
isSelected: Boolean,
iconBackgroundColor: Color,
iconTint: Color,
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 = { _, _ -> })
}
}
}
@Composable
private fun ExpandedCustomFeeItems(
customFeeFields: ImmutableList<CustomFeeFieldUM>,
onValueChange: (Int, String) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
customFeeFields.fastForEachIndexed { index, field ->
val showDivider = index != customFeeFields.size - 1
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,
)
}
}
}
}
@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(
isDoneEnabled = false,
feeItems = persistentListOf(
FeeItem.Suggested(
title = stringReference("Suggested by Tangem"),
amount = Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum),
onSelect = onSelectStub,
),
FeeItem.Slow(
amount = Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum),
onSelect = onSelectStub,
),
FeeItem.Market(
amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum),
onSelect = onSelectStub,
),
FeeItem.Fast(
amount = Amount(value = BigDecimal("0.3"), blockchain = Blockchain.Ethereum),
onSelect = onSelectStub,
),
FeeItem.Custom(customValues = customFeeFields, onSelect = onSelectStub),
),
// selectedFeeItem = FeeItem.Market(
// amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum),
// onSelect = onSelectStub
// ),
selectedFeeItem = FeeItem.Custom(customValues = customFeeFields, onSelect = onSelectStub),
isFeeApproximate = true,
feeFiatRateDataHolder = FeeFiatRateDataHolder(
rate = BigDecimal.TEN,
appCurrency = AppCurrency.Default,
),
),
),
)
private val onSelectStub: () -> Unit = {}
private val customFeeFields = 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

@ -16,6 +16,7 @@ dependencies {
implementation(projects.features.sendV2.api)
implementation(projects.features.txhistory.api)
implementation(projects.features.nft.api)
implementation(projects.features.feeSelector.api)
/** Libs */
implementation(projects.libs.crypto)

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

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.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList

View file

@ -11,11 +11,11 @@ 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

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,10 +10,10 @@ 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

View file

@ -11,6 +11,7 @@ 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
@ -19,7 +20,6 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth
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

View file

@ -11,6 +11,7 @@ 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
@ -19,7 +20,6 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth
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

View file

@ -11,10 +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.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

View file

@ -12,7 +12,7 @@ import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowEnterAmount
import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM
import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import kotlinx.collections.immutable.ImmutableList

View file

@ -5,6 +5,7 @@ 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

View file

@ -3,6 +3,7 @@ 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

View file

@ -239,6 +239,9 @@ include(":features:nft:impl")
include(":features:walletconnect:api")
include(":features:walletconnect:impl")
include(":features:fee-selector:api")
include(":features:fee-selector:impl")
// endregion Feature modules
// region Domain modules