Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-05 18:11:33 +03:00
parent 13b1bbc486
commit 7c92156d19
19 changed files with 472 additions and 152 deletions

View file

@ -1,32 +1,25 @@
package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModalBottomSheetWithBackHandling(
onDismissRequest: () -> Unit,
onBack: (() -> Unit),
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
@ -38,28 +31,9 @@ fun ModalBottomSheetWithBackHandling(
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) {
onBack()
}
val requester = remember { FocusRequester() }
val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier
.focusRequester(requester)
.focusable()
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.key == Key.Back &&
keyEvent.type == KeyEventType.KeyUp &&
!keyEvent.nativeKeyEvent.isCanceled
) {
backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
return@onPreviewKeyEvent true
}
return@onPreviewKeyEvent false
},
modifier = modifier,
sheetState = sheetState,
sheetMaxWidth = sheetMaxWidth,
shape = shape,
@ -69,18 +43,17 @@ fun ModalBottomSheetWithBackHandling(
scrimColor = scrimColor,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
properties = ModalBottomSheetDefaults.properties(
// Set false otherwise the onPreviewKeyEvent doesn't work at all.
// The functionality of shouldDismissOnBackPress is achieved by the BackHandler.
shouldDismissOnBackPress = false,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = onBack == null,
),
content = content,
)
content = {
content()
LaunchedEffect(Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke()
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)

View file

@ -207,32 +207,18 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
noinline onBack: (() -> Unit)? = null,
noinline bsContent: @Composable ColumnScope.() -> Unit,
) {
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
// region Preview

View file

@ -247,32 +247,18 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
// region Preview

View file

@ -2,7 +2,6 @@ package com.tangem.core.ui.components.bottomsheets.sheet
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.rememberModalBottomSheetState
@ -182,30 +181,16 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
ModalBottomSheetWithBackHandling(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}

View file

@ -7,4 +7,5 @@ enum class IconTint {
Accent,
Warning,
Inactive,
Informative,
}

View file

@ -43,6 +43,9 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
isFlickering = state.isFlickering,
)
}
is TokenFiatAmountState.Icon -> {
IconAmount(modifier = modifier, state = state)
}
is TokenFiatAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = 4.dp)
}
@ -53,6 +56,16 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
}
}
@Composable
private fun IconAmount(state: TokenFiatAmountState.Icon, modifier: Modifier = Modifier) {
Icon(
modifier = modifier,
painter = rememberVectorPainter(image = ImageVector.vectorResource(state.iconRes)),
tint = state.tint.color(),
contentDescription = null,
)
}
@Composable
private fun ContentFiatAmount(
text: String,
@ -76,11 +89,7 @@ private fun ContentFiatAmount(
Icon(
modifier = Modifier.size(12.dp),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = when (icon.tint) {
IconTint.Accent -> TangemTheme.colors.icon.accent
IconTint.Warning -> TangemTheme.colors.icon.attention
IconTint.Inactive -> TangemTheme.colors.icon.inactive
},
tint = icon.tint.color(),
contentDescription = null,
)
}
@ -91,6 +100,14 @@ private fun ContentFiatAmount(
}
}
@Composable
private fun IconTint.color() = when (this) {
IconTint.Accent -> TangemTheme.colors.icon.accent
IconTint.Warning -> TangemTheme.colors.icon.attention
IconTint.Inactive -> TangemTheme.colors.icon.inactive
IconTint.Informative -> TangemTheme.colors.icon.informative
}
@Composable
private fun FiatAmountText(
text: String,

View file

@ -235,6 +235,11 @@ sealed class TokenItemState {
val isFlickering: Boolean = false,
) : FiatAmountState()
data class Icon(
val iconRes: Int,
val tint: IconTint = IconTint.Inactive,
) : FiatAmountState()
data object Loading : FiatAmountState()
data object Locked : FiatAmountState()

View file

@ -71,6 +71,7 @@ dependencies {
implementation(projects.domain.swap.models)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.transaction)
/** Compose libraries */

View file

@ -7,6 +7,7 @@ import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.essenty.instancekeeper.getOrCreateSimple
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
@ -16,6 +17,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.api.FeeSelectorComponent
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
import com.tangem.features.send.v2.feeselector.component.extended.FeeExtendedSelectorComponent
import com.tangem.features.send.v2.feeselector.component.speed.FeeSpeedSelectorComponent
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
@ -29,13 +31,20 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: FeeSelectorParams.FeeSelectorDetailsParams,
@Assisted private val onDismiss: () -> Unit,
private val feeExtendedSelectorComponent: FeeExtendedSelectorComponent.Factory,
private val feeSpeedSelectorComponentFactory: FeeSpeedSelectorComponent.Factory,
) : FeeSelectorComponent, AppComponentContext by appComponentContext {
private val stackNavigation = instanceKeeper.getOrCreateSimple { StackNavigation<FeeSelectorRoute>() }
private val innerRouter = InnerRouter<FeeSelectorRoute>(
stackNavigation = stackNavigation,
popCallback = { model.onChildBack() },
popCallback = {
stackNavigation.pop { success ->
if (!success) {
dismiss()
}
}
},
)
private val model: FeeSelectorModel = getOrCreateModel(params = params, router = innerRouter)
@ -59,10 +68,11 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
FeeSelectorModalBottomSheet(
childStack = contentStack,
onDismiss = ::dismiss,
state = state,
feeSelectorIntents = model.intents,
feeDisplaySource = params.feeDisplaySource,
onDismiss = onDismiss,
onBack = innerRouter::pop,
)
}
@ -73,17 +83,21 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor(
)
val componentParams = FeeSelectorComponentParams(
parentParams = params,
state = model.uiState,
intents = model.intents,
)
return when (config) {
FeeSelectorRoute.NetworkFee -> feeExtendedSelectorComponent.create(
appComponentContext = appComponentContext,
params = componentParams,
)
FeeSelectorRoute.ChooseSpeed -> feeSpeedSelectorComponentFactory.create(
appComponentContext = appComponentContext,
params = componentParams,
)
FeeSelectorRoute.ChooseToken -> TODO()
FeeSelectorRoute.NetworkFee -> TODO()
}
}

View file

@ -1,10 +1,12 @@
package com.tangem.features.send.v2.feeselector.component
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.params.FeeSelectorParams
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import kotlinx.coroutines.flow.MutableStateFlow
internal class FeeSelectorComponentParams(
val parentParams: FeeSelectorParams.FeeSelectorDetailsParams,
val state: MutableStateFlow<FeeSelectorUM>,
val intents: FeeSelectorIntents,
)

View file

@ -0,0 +1,41 @@
package com.tangem.features.send.v2.feeselector.component.extended
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.core.ui.decompose.ComposableContentComponent
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
import com.tangem.features.send.v2.feeselector.component.extended.model.FeeExtendedSelectorModel
import com.tangem.features.send.v2.feeselector.component.extended.ui.FeeExtendedSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class FeeExtendedSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: FeeSelectorComponentParams,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: FeeExtendedSelectorModel = getOrCreateModel(params = params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
FeeExtendedSelectorContent(
modifier = modifier,
state = state,
)
}
@AssistedFactory
interface Factory {
fun create(
appComponentContext: AppComponentContext,
params: FeeSelectorComponentParams,
): FeeExtendedSelectorComponent
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.send.v2.feeselector.component.extended.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
@Immutable
data class FeeExtendedSelectorUM(
val parent: FeeSelectorUM.Content,
val token: TokenItemState,
val fee: FeeItem,
val onFeeClick: () -> Unit,
)

View file

@ -0,0 +1,23 @@
package com.tangem.features.send.v2.feeselector.component.extended.entity
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter
internal class SelectedTokenItemConverter(
appCurrency: AppCurrency,
onTokenClick: () -> Unit,
) : Converter<CryptoCurrencyStatus, TokenItemState> {
private val tokenItemConverter = TokenItemStateConverter(
appCurrency = appCurrency,
onItemClick = { _, _ -> onTokenClick() },
)
override fun convert(value: CryptoCurrencyStatus): TokenItemState {
// TODO
return tokenItemConverter.convert(value)
}
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.send.v2.feeselector.component.extended.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
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.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.component.FeeSelectorComponentParams
import com.tangem.features.send.v2.feeselector.component.extended.entity.FeeExtendedSelectorUM
import com.tangem.features.send.v2.feeselector.component.extended.entity.SelectedTokenItemConverter
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
class FeeExtendedSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val router: Router,
) : Model() {
private val params = paramsContainer.require<FeeSelectorComponentParams>()
private var appCurrency: AppCurrency = AppCurrency.Default
val uiState: StateFlow<FeeExtendedSelectorUM>
field = MutableStateFlow<FeeExtendedSelectorUM>(getInitialState())
init {
modelScope.launch {
appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }
uiState.value = getInitialState()
}
params.state
.onEach(::onParentStateUpdated)
.launchIn(modelScope)
}
private fun getInitialState(): FeeExtendedSelectorUM {
val status = params.parentParams.feeCryptoCurrencyStatus
val parentContentState = params.state.value as FeeSelectorUM.Content
return FeeExtendedSelectorUM(
parent = parentContentState,
token = SelectedTokenItemConverter(
appCurrency = appCurrency,
onTokenClick = {
router.push(FeeSelectorRoute.ChooseToken)
},
).convert(status),
fee = parentContentState.selectedFeeItem,
onFeeClick = {
router.push(FeeSelectorRoute.ChooseSpeed)
},
)
}
private fun onParentStateUpdated(state: FeeSelectorUM) {
uiState.update { current ->
current.copy(
parent = state as FeeSelectorUM.Content,
)
}
}
}

View file

@ -0,0 +1,154 @@
package com.tangem.features.send.v2.feeselector.component.extended.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
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.entity.*
import com.tangem.features.send.v2.feeselector.component.extended.entity.FeeExtendedSelectorUM
import com.tangem.features.send.v2.feeselector.component.speed.ui.RegularFeeItemContent
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@Composable
fun FeeExtendedSelectorContent(state: FeeExtendedSelectorUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(
bottom = 16.dp,
start = 16.dp,
end = 16.dp,
)
.fillMaxWidth(),
) {
TokenItem(
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.action),
state = state.token,
isBalanceHidden = false,
)
SpacerH8()
val feeFiatRateUM = state.parent.feeFiatRateUM
val fee = state.fee.fee
RegularFeeItemContent(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.clickable(onClick = state.onFeeClick)
.background(
TangemTheme.colors.background.action,
RoundedCornerShape(TangemTheme.dimens.radius14),
),
title = state.fee.title,
iconRes = state.fee.iconRes,
iconBackgroundColor = TangemTheme.colors.icon.accent.copy(alpha = 0.1F),
iconTint = TangemTheme.colors.icon.accent,
preDot = stringReference(
fee.amount.value.format {
crypto(
symbol = fee.amount.currencySymbol,
decimals = fee.amount.decimals,
).fee(canBeLower = state.parent.feeExtraInfo.isFeeApproximate)
},
),
postDot = if (state.parent.feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) {
getFiatReference(
value = fee.amount.value,
rate = feeFiatRateUM.rate,
appCurrency = feeFiatRateUM.appCurrency,
)
} else {
null
},
ellipsizeOffset = fee.amount.currencySymbol.length,
showSelectorIcon = true,
)
}
}
@Preview
@Composable
private fun Preview() {
val tokenItemState = TokenItemState.Content(
id = "1",
iconState = CurrencyIconState.Locked,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")),
fiatAmountState = TokenItemState.FiatAmountState.Icon(
R.drawable.ic_select_18_24,
tint = IconTint.Informative,
),
subtitle2State = null,
subtitleState = TokenItemState.SubtitleState.TextContent(
value = stringReference("Balance: 0,35853044 BTC"),
isAvailable = false,
),
onItemClick = {},
onItemLongClick = {},
)
val lowFeeItem =
FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)))
TangemThemePreview {
FeeExtendedSelectorContent(
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
state = FeeExtendedSelectorUM(
parent = FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
feeItems = persistentListOf(lowFeeItem),
selectedFeeItem = lowFeeItem,
feeExtraInfo = FeeExtraInfo(
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
isTronToken = false,
),
feeNonce = FeeNonce.None,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("2500"),
appCurrency = AppCurrency.Default,
),
fees = TransactionFee.Single(lowFeeItem.fee),
),
token = tokenItemState,
fee = FeeItem.Market(
fee = Fee.Common(
Amount(
value = BigDecimal("0.02"),
blockchain = Blockchain.Ethereum,
),
),
),
onFeeClick = {},
),
)
}
}

View file

@ -16,9 +16,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
@ -35,6 +37,7 @@ import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.inputrow.InputRowEnter
@ -309,7 +312,7 @@ private fun ExpandedCustomFeeItems(
}
@Composable
private fun RegularFeeItemContent(
internal fun RegularFeeItemContent(
title: TextReference,
@DrawableRes iconRes: Int,
iconBackgroundColor: Color,
@ -318,6 +321,7 @@ private fun RegularFeeItemContent(
preDot: TextReference? = null,
postDot: TextReference? = null,
ellipsizeOffset: Int? = null,
showSelectorIcon: Boolean = false,
) {
Column(modifier = modifier) {
Row(
@ -343,6 +347,16 @@ private fun RegularFeeItemContent(
postDot = postDot,
ellipsizeOffset = ellipsizeOffset,
)
if (showSelectorIcon) {
SpacerWMax()
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_select_18_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
}
}

View file

@ -2,6 +2,7 @@ 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.component.extended.model.FeeExtendedSelectorModel
import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel
import com.tangem.features.send.v2.feeselector.component.speed.model.FeeSpeedSelectorModel
import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel
@ -19,6 +20,11 @@ internal interface FeeSelectorModelModule {
@ClassKey(FeeSelectorModel::class)
fun bindModel(model: FeeSelectorModel): Model
@Binds
@IntoMap
@ClassKey(FeeExtendedSelectorModel::class)
fun bindExtendedModel(model: FeeExtendedSelectorModel): Model
@Binds
@IntoMap
@ClassKey(FeeSelectorBlockModel::class)

View file

@ -14,8 +14,8 @@ import javax.inject.Inject
@ModelScoped
internal class FeeSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
feeSelectorLogicFactory: FeeSelectorLogic.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val feeSelectorLogicFactory: FeeSelectorLogic.Factory,
private val sendFeatureToggles: SendFeatureToggles,
) : Model() {
@ -28,16 +28,12 @@ internal class FeeSelectorModel @Inject constructor(
val uiState = feeSelectorLogic.uiState
val intents: FeeSelectorIntents = feeSelectorLogic
fun onChildBack() {
// TODO("Not yet implemented")
}
fun getInitialRoute(): FeeSelectorRoute {
if (sendFeatureToggles.isGaslessTransactionsEnabled.not()) {
return FeeSelectorRoute.ChooseSpeed
}
// TODO: Add logic to choose initial route based on other conditions
return FeeSelectorRoute.ChooseSpeed
return FeeSelectorRoute.NetworkFee
}
}

View file

@ -27,6 +27,7 @@ import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.features.send.v2.feeselector.route.FeeSelectorRoute
import com.tangem.features.send.v2.impl.R
@Suppress("LongParameterList")
@Composable
internal fun FeeSelectorModalBottomSheet(
childStack: ChildStack<FeeSelectorRoute, ComposableContentComponent>,
@ -34,6 +35,7 @@ internal fun FeeSelectorModalBottomSheet(
feeSelectorIntents: FeeSelectorIntents,
feeDisplaySource: FeeSelectorParams.FeeDisplaySource,
onDismiss: () -> Unit,
onBack: () -> Unit,
) {
if (state !is FeeSelectorUM.Content) return
@ -44,25 +46,44 @@ internal fun FeeSelectorModalBottomSheet(
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
onBack = if (childStack.backStack.isNotEmpty()) onBack else null,
title = {
FeeTitle(childStack = childStack, feeDisplaySource = feeDisplaySource, onDismiss = onDismiss)
FeeTitle(
childStack = childStack,
feeDisplaySource = feeDisplaySource,
onBack = onBack,
)
},
content = {
Content(childStack = childStack)
},
footer = if (state.selectedFeeItem is FeeItem.Custom) {
{
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
enabled = state.isPrimaryButtonEnabled,
text = stringResourceSafe(R.string.common_done),
onClick = feeSelectorIntents::onDoneClick,
)
footer = when {
childStack.active.configuration is FeeSelectorRoute.NetworkFee -> {
{
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
enabled = state.isPrimaryButtonEnabled,
text = stringResourceSafe(R.string.common_confirm),
onClick = feeSelectorIntents::onDoneClick,
)
}
}
} else {
null
childStack.active.configuration is FeeSelectorRoute.ChooseSpeed &&
state.selectedFeeItem is FeeItem.Custom -> {
{
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
enabled = state.isPrimaryButtonEnabled,
text = stringResourceSafe(R.string.common_done),
onClick = feeSelectorIntents::onDoneClick,
)
}
}
else -> null
},
)
}
@ -71,7 +92,7 @@ internal fun FeeSelectorModalBottomSheet(
private fun FeeTitle(
childStack: ChildStack<FeeSelectorRoute, ComposableContentComponent>,
feeDisplaySource: FeeSelectorParams.FeeDisplaySource,
onDismiss: () -> Unit,
onBack: () -> Unit,
) {
Children(
stack = childStack,
@ -79,17 +100,25 @@ private fun FeeTitle(
) { current ->
when (feeDisplaySource) {
FeeSelectorParams.FeeDisplaySource.Screen -> {
TangemModalBottomSheetTitle(
title = current.configuration.title,
endIconRes = R.drawable.ic_close_24,
onEndClick = onDismiss,
)
if (childStack.backStack.isNotEmpty()) {
TangemModalBottomSheetTitle(
title = current.configuration.title,
startIconRes = R.drawable.ic_back_24,
onStartClick = onBack,
)
} else {
TangemModalBottomSheetTitle(
title = current.configuration.title,
endIconRes = R.drawable.ic_close_24,
onEndClick = onBack,
)
}
}
FeeSelectorParams.FeeDisplaySource.BottomSheet -> {
TangemModalBottomSheetTitle(
title = current.configuration.title,
startIconRes = R.drawable.ic_back_24,
onStartClick = onDismiss,
onStartClick = onBack,
)
}
}