From b6cd2781ab1ec858165663d00dbb118cab694a44 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Nov 2023 18:22:53 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../states/ChooseProviderBottomSheetConfig.kt | 9 + .../swap/models/states/ProviderState.kt | 35 ++- .../swap/ui/ChooseProviderBottomSheet.kt | 103 +++++++ .../tangem/feature/swap/ui/ProviderItem.kt | 270 ++++++++++++++---- 4 files changed, 361 insertions(+), 56 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt new file mode 100644 index 0000000000..7507686111 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +class ChooseProviderBottomSheetConfig( + val selectedProviderId: String, + val providers: ImmutableList, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 6b878743e1..f78f6d6cb3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -2,15 +2,42 @@ package com.tangem.feature.swap.models.states sealed class ProviderState { - object Loading : ProviderState() + abstract val onProviderClick: ((String) -> Unit)? + abstract val id: String + + data class Loading( + override val id: String = "", + override val onProviderClick: ((String) -> Unit)? = null, + ) : ProviderState() data class Content( - val id: String, + override val id: String, val name: String, val type: String, val iconUrl: String, - val isBestTrade: Boolean, val rate: String, - val onProviderClick: () -> Unit, + val selectionType: SelectionType, + val additionalBadge: AdditionalBadge, + val percentLowerThenBest: Float?, + override val onProviderClick: (String) -> Unit, ) : ProviderState() + + data class Unavailable( + override val id: String, + val name: String, + val type: String, + val iconUrl: String, + val alertText: String, + override val onProviderClick: ((String) -> Unit)? = null, + ) : ProviderState() + + sealed class AdditionalBadge { + object BestTrade : AdditionalBadge() + object Empty : AdditionalBadge() + object PermissionRequired : AdditionalBadge() + } + + enum class SelectionType { + NONE, CLICK, SELECT + } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt new file mode 100644 index 0000000000..266c1f9b49 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -0,0 +1,103 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig +import com.tangem.feature.swap.models.states.ProviderState +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { content: ChooseProviderBottomSheetConfig -> + ChooseProviderBottomSheetContent(content = content) + } +} + +@Composable +private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.secondary), + ) { + Text( + text = "Choose provider", + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing10) + .align(Alignment.CenterHorizontally), + ) + Text( + text = "Providers facilitate transactions, ensuring smooth and efficient token exchanges", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing10) + .padding(horizontal = TangemTheme.dimens.spacing56) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) + Column( + modifier = Modifier + .padding(TangemTheme.dimens.spacing16) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) { + content.providers.forEach { + val isSelected = it.id == content.selectedProviderId + ProviderItem( + state = it, + isSelected = isSelected, + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } + } + } +} + +@Preview +@Composable +private fun ChooseProviderBottomSheet_Preview() { + val providers = listOf( + ProviderState.Content( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + rate = "1 000 000", + additionalBadge = ProviderState.AdditionalBadge.BestTrade, + percentLowerThenBest = -1.0f, + selectionType = ProviderState.SelectionType.SELECT, + onProviderClick = {}, + ), + ProviderState.Unavailable( + id = "2", + name = "1inch", + type = "DEX", + iconUrl = "", + alertText = "Unavailable", + ), + ) + TangemTheme(isDark = false) { + ChooseProviderBottomSheetContent( + ChooseProviderBottomSheetConfig( + selectedProviderId = "1", + providers = providers.toImmutableList(), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index 5dee0e3ff7..6602b25ca8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -10,6 +10,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -22,47 +24,58 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.states.ProviderState /** - * UI Item for swap provider + * UI Item for swap provider wrapped in a [BaseContainer] with rounded corners * * https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7856-41909&mode=design&t=vo7dyElitnzSPSW3-4 */ + +private const val GRAY_SCALE_SATURATION = 0f +private const val GRAY_SCALE_ALPHA = 0.4f +private val GrayscaleColorFilter: ColorFilter + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) + @Composable -fun ProviderItem(state: ProviderState) { +fun ProviderItemBlock(state: ProviderState) { + BaseContainer(state) { + ProviderItem( + state = state, + modifier = Modifier.align(Alignment.CenterStart), + ) + } +} + +@Composable +fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected: Boolean = false) { when (state) { is ProviderState.Content -> { - ContentProviderState(state = state) + ProviderContentState( + state = state, + modifier = modifier, + isSelected = isSelected, + ) } is ProviderState.Loading -> { - LoadingProviderState() + ProviderLoadingState( + modifier = modifier, + ) + } + is ProviderState.Unavailable -> { + ProviderUnavailableState( + state = state, + modifier = modifier, + ) } } } @Composable -private fun BaseContainer(onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit) { - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .clickable( - enabled = onClick != null, - onClick = { onClick?.invoke() }, - ) - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size68), - ) { - content() - } -} - -@Composable -private fun ContentProviderState(state: ProviderState.Content) { - BaseContainer(state.onProviderClick) { - Row( - modifier = Modifier.align(Alignment.CenterStart), - ) { +private fun ProviderContentState( + state: ProviderState.Content, + modifier: Modifier = Modifier, + isSelected: Boolean = false, +) { + Box(modifier = modifier.fillMaxWidth()) { + Row { SubcomposeAsyncImage( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) @@ -99,38 +112,99 @@ private fun ContentProviderState(state: ProviderState.Content) { color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) - if (state.isBestTrade) { - BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + when (state.additionalBadge) { + ProviderState.AdditionalBadge.BestTrade -> + BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + ProviderState.AdditionalBadge.PermissionRequired -> + PermissionBadgeItem(Modifier.padding(start = TangemTheme.dimens.spacing4)) + ProviderState.AdditionalBadge.Empty -> { + // no-op + } } } + Row( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + Text( + text = state.rate, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + if (state.percentLowerThenBest != null) { + Text( + text = "${state.percentLowerThenBest}%", // todo add to strings + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.warning, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + } + } + } + + ProviderChevron(state = state, isSelected = isSelected) + } +} + +@Composable +private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth()) { + Row { + val (alpha, colorFilter) = GRAY_SCALE_ALPHA to GrayscaleColorFilter + SubcomposeAsyncImage( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing12) + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + error = { + ErrorProviderIcon( + Modifier.size( + size = TangemTheme.dimens.size40, + ), + ) + }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) + + Column( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + ) { + Row { + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = state.type, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } Text( - text = state.rate, + text = state.alertText, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) } } - - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - modifier = Modifier - .align(alignment = Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - tint = TangemTheme.colors.icon.informative, - ) } } @Composable -private fun LoadingProviderState() { - BaseContainer { - Column( - modifier = Modifier - .align(Alignment.CenterStart) - .padding(vertical = TangemTheme.dimens.spacing12), - ) { +private fun ProviderLoadingState(modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth()) { + Column { Text( text = "Provider", style = TangemTheme.typography.caption2, @@ -168,6 +242,56 @@ private fun LoadingProviderState() { } } +@Composable +private fun BoxScope.ProviderChevron(state: ProviderState.Content, isSelected: Boolean) { + when (state.selectionType) { + ProviderState.SelectionType.NONE -> { + /* no-op */ + } + ProviderState.SelectionType.CLICK -> { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.informative, + ) + } + ProviderState.SelectionType.SELECT -> { + if (isSelected) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + contentDescription = null, + modifier = Modifier + .align(alignment = Alignment.CenterEnd) + .padding(end = TangemTheme.dimens.spacing12), + tint = TangemTheme.colors.icon.accent, + ) + } + } + } +} + +@Composable +private fun BaseContainer(state: ProviderState, content: @Composable BoxScope.() -> Unit) { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .clickable( + enabled = state.onProviderClick != null, + onClick = { state.onProviderClick?.invoke(state.id) }, + ) + .fillMaxWidth() + .defaultMinSize(minHeight = TangemTheme.dimens.size68), + ) { + content() + } +} + @Composable private fun ErrorProviderIcon(modifier: Modifier = Modifier) { Box( @@ -203,18 +327,35 @@ private fun BestTradeItem(modifier: Modifier = Modifier) { } } +@Composable +private fun PermissionBadgeItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.background.secondary, + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = "Permission required", + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + ) + } +} + @Preview @Composable private fun ProviderItem_Loading_Preview() { Column { TangemTheme(isDark = false) { - ProviderItem(state = ProviderState.Loading) + ProviderItemBlock(state = ProviderState.Loading()) } SpacerH24() TangemTheme(isDark = true) { - ProviderItem(state = ProviderState.Loading) + ProviderItemBlock(state = ProviderState.Loading()) } } } @@ -227,19 +368,44 @@ private fun ProviderItem_Content_Preview() { name = "1inch", type = "DEX", iconUrl = "", - isBestTrade = true, rate = "1 000 000", + additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, + percentLowerThenBest = -1.0f, + selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ) Column { TangemTheme(isDark = false) { - ProviderItem(state = state) + ProviderItemBlock(state = state) } SpacerH24() TangemTheme(isDark = true) { - ProviderItem(state = state) + ProviderItemBlock(state = state) + } + } +} + +@Preview +@Composable +private fun ProviderItem_Unavailable_Preview() { + val state = ProviderState.Unavailable( + id = "1", + name = "1inch", + type = "DEX", + iconUrl = "", + alertText = "Unavailable", + ) + Column { + TangemTheme(isDark = false) { + ProviderItemBlock(state = state) + } + + SpacerH24() + + TangemTheme(isDark = true) { + ProviderItemBlock(state = state) } } } \ No newline at end of file