From 7c92156d19f6c267755c955756e77503f5dbe85f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Jan 2026 18:11:33 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../ModalBottomSheetWithBackHandling.kt | 49 ++---- .../modal/TangemModalBottomSheet.kt | 38 ++--- .../modal/TangemModalBottomSheetWithFooter.kt | 38 ++--- .../bottomsheets/sheet/TangemBottomSheet.kt | 39 ++--- .../core/ui/components/icons/IconTint.kt | 1 + .../token/internal/TokenFiatAmount.kt | 27 ++- .../components/token/state/TokenItemState.kt | 5 + features/send-v2/impl/build.gradle.kts | 1 + .../DefaultFeeSelectorComponent.kt | 20 ++- .../component/FeeSelectorComponentParams.kt | 2 + .../extended/FeeExtendedSelectorComponent.kt | 41 +++++ .../extended/entity/FeeExtendedSelectorUM.kt | 14 ++ .../entity/SelectedTokenItemConverter.kt | 23 +++ .../model/FeeExtendedSelectorModel.kt | 73 +++++++++ .../extended/ui/FeeExtendedSelectorContent.kt | 154 ++++++++++++++++++ .../speed/ui/FeeSpeedSelectorContent.kt | 16 +- .../feeselector/di/FeeSelectorModelModule.kt | 6 + .../v2/feeselector/model/FeeSelectorModel.kt | 8 +- .../ui/FeeSelectorModalBottomSheet.kt | 69 +++++--- 19 files changed, 472 insertions(+), 152 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/FeeExtendedSelectorComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/FeeExtendedSelectorUM.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/SelectedTokenItemConverter.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/model/FeeExtendedSelectorModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt index 95ce4090db..8b8f7d3fd4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/internal/ModalBottomSheetWithBackHandling.kt @@ -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) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt index a05b76f771..8731e72e07 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt @@ -207,32 +207,18 @@ inline fun 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 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt index 103aff8752..4f05ab4074 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetWithFooter.kt @@ -247,32 +247,18 @@ inline fun 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 diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt index 18bb77bdf7..d609afe29a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/sheet/TangemBottomSheet.kt @@ -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 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, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/icons/IconTint.kt b/core/ui/src/main/java/com/tangem/core/ui/components/icons/IconTint.kt index db6a607f67..a41cdb95ef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/icons/IconTint.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/icons/IconTint.kt @@ -7,4 +7,5 @@ enum class IconTint { Accent, Warning, Inactive, + Informative, } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt index 6b01ffbff7..3824de5dd7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt @@ -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, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 7193eed339..72bc32e2b6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -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() diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 32c849b301..66e97a0ebc 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -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 */ diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt index 807e95bcb3..8ac294c931 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt @@ -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() } private val innerRouter = InnerRouter( 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() } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt index cddc26216e..337a87186a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/FeeSelectorComponentParams.kt @@ -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, val intents: FeeSelectorIntents, ) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/FeeExtendedSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/FeeExtendedSelectorComponent.kt new file mode 100644 index 0000000000..76f9baab92 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/FeeExtendedSelectorComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/FeeExtendedSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/FeeExtendedSelectorUM.kt new file mode 100644 index 0000000000..a9406c3f73 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/FeeExtendedSelectorUM.kt @@ -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, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/SelectedTokenItemConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/SelectedTokenItemConverter.kt new file mode 100644 index 0000000000..3652e62cc6 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/entity/SelectedTokenItemConverter.kt @@ -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 { + + private val tokenItemConverter = TokenItemStateConverter( + appCurrency = appCurrency, + onItemClick = { _, _ -> onTokenClick() }, + ) + + override fun convert(value: CryptoCurrencyStatus): TokenItemState { + // TODO + return tokenItemConverter.convert(value) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/model/FeeExtendedSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/model/FeeExtendedSelectorModel.kt new file mode 100644 index 0000000000..4f586f70f0 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/model/FeeExtendedSelectorModel.kt @@ -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() + private var appCurrency: AppCurrency = AppCurrency.Default + + val uiState: StateFlow + field = MutableStateFlow(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, + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt new file mode 100644 index 0000000000..ff39f78cdd --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt @@ -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 = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt index 87eab44b5c..2a4a9e6bd5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt @@ -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, + ) + } } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt index 295b4f178e..52552917e3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt @@ -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) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 706881dbe0..5f82459764 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -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 } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 7bf68aab96..7deca5c48b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -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, @@ -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, 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, ) } }