Updated on 2026-08-14
This commit is contained in:
parent
13b1bbc486
commit
7c92156d19
19 changed files with 472 additions and 152 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -7,4 +7,5 @@ enum class IconTint {
|
|||
Accent,
|
||||
Warning,
|
||||
Inactive,
|
||||
Informative,
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue