From 8bd68643e77a406ea4b650c5c825bbc96df07e70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Oct 2023 13:36:53 +0300 Subject: [PATCH 1/3] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 1 + .../currency/DefaultCurrencyIcon.kt | 60 ++++++++++++ .../components/currency/fiaticon/FiatIcon.kt | 38 ++++++++ .../currency/tokenicon}/ContentIcon.kt | 72 ++------------ .../currency/tokenicon}/IconBadge.kt | 2 +- .../currency/tokenicon}/TokenIcon.kt | 28 ++++-- .../currency/tokenicon/TokenIconState.kt | 85 +++++++++++++++++ .../CryptoCurrencyToIconStateConverter.kt | 26 ++--- .../com/tangem/core/ui/res/TangemColors.kt | 1 - .../src/main/res/drawable/ic_shape_circle.xml | 9 ++ .../presentation/common/WalletPreviewData.kt | 7 +- .../common/component/TokenItem.kt | 2 +- .../common/state/TokenItemState.kt | 95 ++----------------- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- ...ryptoCurrencyStatusToTokenItemConverter.kt | 2 +- 15 files changed, 254 insertions(+), 176 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon => core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon}/ContentIcon.kt (55%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon => core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon}/IconBadge.kt (96%) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon => core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon}/TokenIcon.kt (77%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils => core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter}/CryptoCurrencyToIconStateConverter.kt (69%) create mode 100644 core/ui/src/main/res/drawable/ic_shape_circle.xml diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3a09e91308..8cf16b6848 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.paging) implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt new file mode 100644 index 0000000000..792cbab8b0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt @@ -0,0 +1,60 @@ +package com.tangem.core.ui.components.currency + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.ImageBackgroundContrastChecker +import kotlinx.coroutines.launch + +@Composable +internal inline fun DefaultCurrencyIcon( + iconData: Any, + alpha: Float, + colorFilter: ColorFilter?, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } + val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() + val isDarkTheme = isSystemInDarkTheme() + val coroutineScope = rememberCoroutineScope() + + SubcomposeAsyncImage( + modifier = modifier + .background( + color = iconBackgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconData) + .crossfade(enable = true) + .allowHardware(false) + .listener( + onSuccess = { _, result -> + if (isDarkTheme) { + coroutineScope.launch { + val color = ImageBackgroundContrastChecker( + drawable = result.drawable, + backgroundColor = itemBackgroundColor, + ).getContrastColorIfNeeded(isDarkTheme) + iconBackgroundColor = color + } + } + }, + ).build(), + loading = { LoadingIcon() }, + error = { errorIcon() }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt new file mode 100644 index 0000000000..879ea4575a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/fiaticon/FiatIcon.kt @@ -0,0 +1,38 @@ +package com.tangem.core.ui.components.currency.fiaticon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.DefaultCurrencyIcon + +/** + * Simple icon from network + * + * @param url link to icon + * @param fallbackResId fallback icon + * @param modifier component modifier + */ +@Composable +fun FiatIcon( + url: String?, + modifier: Modifier = Modifier, + @DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle, +) { + val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url + + DefaultCurrencyIcon( + modifier = modifier, + iconData = iconData, + errorIcon = { + Image( + painter = painterResource(id = fallbackResId), + contentDescription = null, + ) + }, + alpha = 1f, + colorFilter = null, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt similarity index 55% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt index dd1be11f5a..dd3e892301 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt @@ -1,44 +1,36 @@ -package com.tangem.feature.wallet.presentation.common.component.token.icon +package com.tangem.core.ui.components.currency.tokenicon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import kotlinx.coroutines.launch +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.DefaultCurrencyIcon @Composable internal fun ContentIcon( - icon: TokenItemState.IconState, + icon: TokenIconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier, ) { when (icon) { - is TokenItemState.IconState.CoinIcon -> CoinIcon( + is TokenIconState.CoinIcon -> CoinIcon( modifier = modifier, url = icon.url, fallbackResId = icon.fallbackResId, alpha = alpha, colorFilter = colorFilter, ) - is TokenItemState.IconState.TokenIcon -> TokenIcon( + is TokenIconState.TokenIcon -> TokenIcon( modifier = modifier, url = icon.url, alpha = alpha, @@ -52,14 +44,14 @@ internal fun ContentIcon( ) }, ) - is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon( + is TokenIconState.CustomTokenIcon -> CustomTokenIcon( modifier = modifier, tint = icon.tint, background = icon.background, alpha = alpha, ) - TokenItemState.IconState.Loading, - TokenItemState.IconState.Locked, + TokenIconState.Loading, + TokenIconState.Locked, -> Unit } } @@ -128,48 +120,4 @@ private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifi contentDescription = null, ) } -} - -@Composable -private inline fun DefaultCurrencyIcon( - iconData: Any, - alpha: Float, - colorFilter: ColorFilter?, - crossinline errorIcon: @Composable () -> Unit, - modifier: Modifier = Modifier, -) { - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - SubcomposeAsyncImage( - modifier = modifier - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ), - model = ImageRequest.Builder(context = LocalContext.current) - .data(iconData) - .crossfade(enable = true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = itemBackgroundColor, - ).getContrastColorIfNeeded(isDarkTheme) - iconBackgroundColor = color - } - } - }, - ).build(), - loading = { LoadingIcon() }, - error = { errorIcon() }, - alpha = alpha, - colorFilter = colorFilter, - contentDescription = null, - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt similarity index 96% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt index 6fd0339923..e21ce3274c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component.token.icon +package com.tangem.core.ui.components.currency.tokenicon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt similarity index 77% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt index 275c58d54f..07d299f583 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component.token.icon +package com.tangem.core.ui.components.currency.tokenicon import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -14,14 +14,22 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.IconState as TokenIconState private const val GRAY_SCALE_SATURATION = 0f private const val GRAY_SCALE_ALPHA = 0.4f private const val NORMAL_ALPHA = 1f +/** + * Cryptocurrency icon with network badge + * + * TODO [separate domain from ui]([REDACTED_JIRA]) + * + * @param state cryptocurrency icon config + * @param modifier component modifier + * @param shouldDisplayNetwork specifies whether to display network badge + */ @Composable -internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { +fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) @@ -34,7 +42,11 @@ internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { is TokenIconState.CustomTokenIcon, is TokenIconState.TokenIcon, -> { - ContentIconContainer(modifier = iconModifier, icon = state) + ContentIconContainer( + icon = state, + modifier = iconModifier, + shouldDisplayNetwork = shouldDisplayNetwork, + ) } } } @@ -60,7 +72,11 @@ private fun LockedIcon(modifier: Modifier = Modifier) { } @Composable -private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifier = Modifier) { +private fun BoxScope.ContentIconContainer( + icon: TokenIconState, + modifier: Modifier = Modifier, + shouldDisplayNetwork: Boolean = true, +) { val networkBadgeOffset = TangemTheme.dimens.spacing4 val (alpha, colorFilter) = remember(icon.isGrayscale) { if (icon.isGrayscale) { @@ -77,7 +93,7 @@ private fun BoxScope.ContentIconContainer(icon: TokenIconState, modifier: Modifi colorFilter = colorFilter, ) - if (icon.networkBadgeIconResId != null) { + if (icon.networkBadgeIconResId != null && shouldDisplayNetwork) { NetworkBadge( modifier = Modifier .offset(x = networkBadgeOffset, y = -networkBadgeOffset) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt new file mode 100644 index 0000000000..f51adb19b3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt @@ -0,0 +1,85 @@ +package com.tangem.core.ui.components.currency.tokenicon + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +/** + * Represents the various states an icon can be in. + * + * [REDACTED_TODO_COMMENT] + */ +@Immutable +sealed class TokenIconState { + + abstract val isGrayscale: Boolean + abstract val showCustomBadge: Boolean + abstract val networkBadgeIconResId: Int? + + /** + * Represents a coin icon. + * + * @property url The URL where the coin icon can be fetched from. May be `null` if not found. + * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property showCustomBadge Specifies whether to show the custom token badge. + */ + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + override val showCustomBadge: Boolean, + ) : TokenIconState() { + + override val networkBadgeIconResId: Int? = null + } + + /** + * Represents a token icon. + * + * @property url The URL where the token icon can be fetched from. May be `null` if not found. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property showCustomBadge Specifies whether to show the custom token badge. + * @property fallbackTint The color to be used for tinting the fallback icon. + * @property fallbackBackground The background color to be used for the fallback icon. + */ + data class TokenIcon( + val url: String?, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + override val showCustomBadge: Boolean, + val fallbackTint: Color, + val fallbackBackground: Color, + ) : TokenIconState() + + /** + * Represents a custom token icon. + * + * @property tint The color to be used for tinting the icon. + * @property background The background color to be used for the icon. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CustomTokenIcon( + val tint: Color, + val background: Color, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + ) : TokenIconState() { + + override val showCustomBadge: Boolean = true + } + + object Loading : TokenIconState() { + override val isGrayscale: Boolean = false + override val showCustomBadge: Boolean = false + override val networkBadgeIconResId: Int? = null + } + + object Locked : TokenIconState() { + override val isGrayscale: Boolean = false + override val showCustomBadge: Boolean = false + override val networkBadgeIconResId: Int? = null + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt similarity index 69% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt index 0af718eb22..6509d4baf1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt @@ -1,27 +1,27 @@ -package com.tangem.feature.wallet.presentation.common.utils +package com.tangem.core.ui.components.currency.tokenicon.converter +import com.tangem.common.Converter +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.utils.converter.Converter -internal class CryptoCurrencyToIconStateConverter : Converter { +/** + * Converts [CryptoCurrencyStatus] to [TokenIconState] + */ +class CryptoCurrencyToIconStateConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState { + override fun convert(value: CryptoCurrencyStatus): TokenIconState { return when (val currency = value.currency) { is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError) is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError) } } - private fun getIconStateForCoin( - coin: CryptoCurrency.Coin, - isUnreachable: Boolean, - ): TokenItemState.IconState.CoinIcon { - return TokenItemState.IconState.CoinIcon( + private fun getIconStateForCoin(coin: CryptoCurrency.Coin, isUnreachable: Boolean): TokenIconState.CoinIcon { + return TokenIconState.CoinIcon( url = coin.iconUrl, fallbackResId = coin.networkIconResId, isGrayscale = coin.network.isTestnet || isUnreachable, @@ -29,20 +29,20 @@ internal class CryptoCurrencyToIconStateConverter : Converter + + + + + \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 4506069426..15c78e5b89 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent @@ -87,7 +88,7 @@ internal object WalletPreviewData { } val coinIconState - get() = TokenItemState.IconState.CoinIcon( + get() = TokenIconState.CoinIcon( url = null, fallbackResId = R.drawable.img_polygon_22, isGrayscale = false, @@ -95,7 +96,7 @@ internal object WalletPreviewData { ) private val tokenIconState - get() = TokenItemState.IconState.TokenIcon( + get() = TokenIconState.TokenIcon( url = null, networkBadgeIconResId = R.drawable.img_polygon_22, fallbackTint = TangemColorPalette.Black, @@ -105,7 +106,7 @@ internal object WalletPreviewData { ) private val customTokenIconState - get() = TokenItemState.IconState.CustomTokenIcon( + get() = TokenIconState.CustomTokenIcon( tint = TangemColorPalette.Black, background = TangemColorPalette.Meadow, networkBadgeIconResId = R.drawable.img_polygon_22, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 9e0ea7e0cd..fbc0f7af7b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.* -import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState import kotlin.math.max diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index b9f7b9d127..52bdd7abed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -1,9 +1,8 @@ package com.tangem.feature.wallet.presentation.common.state -import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState /** Token item state */ @Immutable @@ -11,7 +10,7 @@ internal sealed class TokenItemState { abstract val id: String - abstract val iconState: IconState + abstract val iconState: TokenIconState abstract val titleState: TitleState @@ -24,7 +23,7 @@ internal sealed class TokenItemState { /** Loading token state */ data class Loading( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState.Content, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading @@ -34,7 +33,7 @@ internal sealed class TokenItemState { /** Locked token state */ data class Locked(override val id: String) : TokenItemState() { - override val iconState: IconState = IconState.Locked + override val iconState: TokenIconState = TokenIconState.Locked override val titleState: TitleState = TitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked @@ -52,7 +51,7 @@ internal sealed class TokenItemState { */ data class Content( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, @@ -70,7 +69,7 @@ internal sealed class TokenItemState { */ data class Draggable( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { @@ -89,7 +88,7 @@ internal sealed class TokenItemState { */ data class Unreachable( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, @@ -109,7 +108,7 @@ internal sealed class TokenItemState { */ data class NoAddress( override val id: String, - override val iconState: IconState, + override val iconState: TokenIconState, override val titleState: TitleState, val onItemLongClick: () -> Unit, ) : TokenItemState() { @@ -118,84 +117,6 @@ internal sealed class TokenItemState { override val priceChangeState: PriceChangeState? = null } - /** - * Represents the various states an icon can be in. - */ - @Immutable - sealed class IconState { - - abstract val isGrayscale: Boolean - abstract val showCustomBadge: Boolean - abstract val networkBadgeIconResId: Int? - - /** - * Represents a coin icon. - * - * @property url The URL where the coin icon can be fetched from. May be `null` if not found. - * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. - * @property isGrayscale Specifies whether to show the icon in grayscale. - * @property showCustomBadge Specifies whether to show the custom token badge. - */ - data class CoinIcon( - val url: String?, - @DrawableRes val fallbackResId: Int, - override val isGrayscale: Boolean, - override val showCustomBadge: Boolean, - ) : IconState() { - - override val networkBadgeIconResId: Int? = null - } - - /** - * Represents a token icon. - * - * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property networkBadgeIconResId The drawable resource ID for the network badge. - * @property isGrayscale Specifies whether to show the icon in grayscale. - * @property showCustomBadge Specifies whether to show the custom token badge. - * @property fallbackTint The color to be used for tinting the fallback icon. - * @property fallbackBackground The background color to be used for the fallback icon. - */ - data class TokenIcon( - val url: String?, - @DrawableRes override val networkBadgeIconResId: Int, - override val isGrayscale: Boolean, - override val showCustomBadge: Boolean, - val fallbackTint: Color, - val fallbackBackground: Color, - ) : IconState() - - /** - * Represents a custom token icon. - * - * @property tint The color to be used for tinting the icon. - * @property background The background color to be used for the icon. - * @property networkBadgeIconResId The drawable resource ID for the network badge. - * @property isGrayscale Specifies whether to show the icon in grayscale. - */ - data class CustomTokenIcon( - val tint: Color, - val background: Color, - @DrawableRes override val networkBadgeIconResId: Int, - override val isGrayscale: Boolean, - ) : IconState() { - - override val showCustomBadge: Boolean = true - } - - object Loading : IconState() { - override val isGrayscale: Boolean = false - override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null - } - - object Locked : IconState() { - override val isGrayscale: Boolean = false - override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null - } - } - @Immutable sealed class TitleState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index aa518deba2..60445f1928 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index 59b3b14b1e..df51275678 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -6,7 +6,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal From cd28b908733ec699bcb9fb9cff2c651eabd3be65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Oct 2023 15:08:44 +0300 Subject: [PATCH 2/3] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 4 +- .../com/tangem/core/ui/components/Buttons.kt | 9 +- .../components/buttons/common/TangemButton.kt | 4 +- .../segmentedbutton/SegmentedButton.kt | 161 +++++++++++++++++ features/send/impl/build.gradle.kts | 14 ++ .../state/SendAmountSegmentedButtonsConfig.kt | 21 +++ .../presentation/send/state/SendUiState.kt | 57 ++++++ .../send/state/fields/SendTextField.kt | 35 ++++ .../send/ui/SendBottomSheetConfig.kt | 7 - .../send/ui/SendNavigationButtons.kt | 52 +++--- .../impl/presentation/send/ui/SendScreen.kt | 7 +- .../impl/presentation/send/ui/SendStates.kt | 34 ---- .../send/ui/amount/AmountField.kt | 168 ++++++++++++++++++ .../send/ui/amount/AmountFieldContainer.kt | 61 +++++++ 15 files changed, 557 insertions(+), 78 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 3345fb4f82..76f6aa404f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -90,6 +90,7 @@ Нравится Заблокирован Основная сеть + Далее Нет Нет адреса Нет данных diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 456401a4a4..3e043496b0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -89,6 +89,7 @@ Like Locked Main network + Next No No address No data @@ -114,7 +115,6 @@ Submit Success Swap - Next terms and conditions Transaction failed Transactions @@ -392,6 +392,7 @@ Normal Priority Maximum amount + Max Network fee Sending %s Total @@ -400,6 +401,7 @@ %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while Invalid address + Insufficient funds for transfer Buy now I have a promo code… Tangem Wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 385824799e..cac2add621 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -7,11 +7,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.* import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.res.TangemTheme @@ -146,6 +145,8 @@ fun SecondaryButton( modifier: Modifier = Modifier, showProgress: Boolean = false, enabled: Boolean = true, + size: TangemButtonSize = TangemButtonSize.Default, + shape: Shape = size.toShape(), ) { TangemButton( modifier = modifier, @@ -155,6 +156,8 @@ fun SecondaryButton( colors = TangemButtonsDefaults.secondaryButtonColors, enabled = enabled, showProgress = showProgress, + size = size, + shape = shape, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index fc114059fe..f245558598 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle @@ -29,13 +30,14 @@ fun TangemButton( size: TangemButtonSize = TangemButtonSize.Default, elevation: ButtonElevation = TangemButtonsDefaults.elevation, textStyle: TextStyle = TangemTheme.typography.button, + shape: Shape = size.toShape(), ) { Button( modifier = modifier.heightIn(min = size.toHeightDp()), onClick = { if (!showProgress) onClick() }, enabled = enabled, elevation = elevation, - shape = size.toShape(), + shape = shape, colors = colors, contentPadding = size.toContentPadding(icon = icon), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt new file mode 100644 index 0000000000..df18f78ab0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -0,0 +1,161 @@ +package com.tangem.core.ui.components.buttons.segmentedbutton + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +/** + * Segmented buttons + * + * [Figma component](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1961-2004&mode=design&t=OFPQ18YhLHVAANab-4) + * + * @param config list of buttons in SegmentedButtons + * @param onClick button click + * @param modifier component modifier + * @param color default button color + * @param selectedColor selected button color + * @param dividerColor border and divider color + * @param showIndication show ripple indication + * @param buttonContent content as separate button + */ +@Composable +inline fun SegmentedButtons( + config: PersistentList, + crossinline onClick: (T) -> Unit, + modifier: Modifier = Modifier, + color: Color = TangemTheme.colors.background.tertiary, + selectedColor: Color = TangemTheme.colors.background.action, + dividerColor: Color = TangemTheme.colors.stroke.primary, + showIndication: Boolean = true, + crossinline buttonContent: @Composable (T) -> Unit, +) { + if (config.isEmpty() || config.size == 1) return + + var selected by remember { mutableIntStateOf(0) } + + Row( + modifier = modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius26)) + .background(dividerColor) + .padding(TangemTheme.dimens.spacing1), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing1), + ) { + repeat(config.size) { index -> + val leftRadius = if (index == 0) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0 + val rightRadius = if (index == config.lastIndex) TangemTheme.dimens.radius26 else TangemTheme.dimens.radius0 + + Box( + modifier = Modifier + .weight(1f) + .background( + color = if (index == selected) selectedColor else color, + shape = RoundedCornerShape( + topStart = leftRadius, + topEnd = rightRadius, + bottomEnd = rightRadius, + bottomStart = leftRadius, + ), + ) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = if (showIndication) LocalIndication.current else null, + ) { + onClick(config[index]) + selected = index + }, + ) { + buttonContent.invoke(config[index]) + } + } + } +} + +@Preview +@Composable +private fun SegmentedButtonsPreview_Light( + @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, +) { + TangemTheme { + SegmentedButtons( + config = config, + onClick = {}, + ) { + Text( + text = it.text, + modifier = Modifier.padding(TangemTheme.dimens.spacing16), + ) + } + } +} + +@Preview +@Composable +private fun SegmentedButtonsPreview_Dark( + @PreviewParameter(SegmentedButtonsPreviewProvider::class) config: PersistentList, +) { + TangemTheme(isDark = true) { + SegmentedButtons( + config = config, + onClick = {}, + ) { + Text( + text = it.text, + modifier = Modifier.padding(TangemTheme.dimens.spacing16), + ) + } + } +} + +//region Preview config +/** + * Segmented buttons preview model + * + * @param text button title + */ +internal data class SegmentedButtonsConfigPreview( + val text: String, +) + +/** + * Segmented button preview provider + */ +internal class SegmentedButtonsPreviewProvider : + CollectionPreviewParameterProvider>( + collection = listOf( + persistentListOf( + SegmentedButtonsConfigPreview( + text = "Title 1", + ), + SegmentedButtonsConfigPreview( + text = "Title 2", + ), + SegmentedButtonsConfigPreview( + text = "Title 3", + ), + ), + persistentListOf( + SegmentedButtonsConfigPreview( + text = "Title 1", + ), + SegmentedButtonsConfigPreview( + text = "Title 2", + ), + ), + ), + ) + +//endregion \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 3c6f6bd7f7..c0cfcb3894 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -14,7 +14,11 @@ dependencies { /** AndroidX */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.appCompat) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) implementation(deps.material) + implementation(deps.arrow.core) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -23,13 +27,23 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + + /** Common */ + implementation(projects.common) /** Core modules */ implementation(projects.core.featuretoggles) implementation(projects.core.ui) + implementation(projects.core.utils) /** Domain modules */ + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) /** Feature modules */ implementation(projects.features.send.api) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt new file mode 100644 index 0000000000..ea3220b6e6 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountSegmentedButtonsConfig.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.impl.presentation.send.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference + +/** + * Segmented buttons config + * + * @param title button title + * @param iconState currency icon state + * @param iconUrl currency icon url + * @param isFiat is fiat currency + */ +@Immutable +internal data class SendAmountSegmentedButtonsConfig( + val title: TextReference, + val iconState: TokenIconState? = null, + val iconUrl: String? = null, + val isFiat: Boolean, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt new file mode 100644 index 0000000000..10df3f1966 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt @@ -0,0 +1,57 @@ +package com.tangem.features.send.impl.presentation.send.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField +import kotlinx.collections.immutable.PersistentList + +/** + * Ui states of the send screen + */ +@Immutable +internal sealed class SendUiState { + + /** States with content */ + sealed class Content : SendUiState() { + + abstract val nextButtonEnabled: Boolean + + /** Initial state */ + data class Initial( + override val nextButtonEnabled: Boolean = false, + ) : Content() + + /** Amount state */ + data class AmountState( + override val nextButtonEnabled: Boolean = false, + val walletName: String, + val walletBalance: String, + val tokenIconState: TokenIconState, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val appCurrency: AppCurrency, + val isFiatValue: Boolean, + val segmentedButtonConfig: PersistentList, + val amountTextField: SendTextField.Amount, + ) : Content() + + // todo [REDACTED_JIRA] + /** Recipient state */ + data class RecipientState( + override val nextButtonEnabled: Boolean = false, + ) : Content() + + // todo [REDACTED_JIRA] + /** Fee and speed state */ + data class FeeState( + override val nextButtonEnabled: Boolean = false, + ) : Content() + + // todo [REDACTED_JIRA] + /** Send state */ + data class SendState( + override val nextButtonEnabled: Boolean = true, + ) : Content() + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt new file mode 100644 index 0000000000..e7677e747d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendTextField.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class SendTextField { + + /** Current value */ + abstract val value: String + + /** Lambda be invoked when value is been changed */ + abstract val onValueChange: (String) -> Unit + + /** Keyboard options */ + abstract val keyboardOptions: KeyboardOptions + + /** Label */ + abstract val label: TextReference + + /** Placeholder (hint) */ + abstract val placeholder: TextReference + + data class Amount( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + override val label: TextReference, + override val placeholder: TextReference, + val fiatValue: String, + val isError: Boolean, + val error: TextReference, + ) : SendTextField() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt deleted file mode 100644 index 73f5797f7b..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendBottomSheetConfig.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.send.impl.presentation.send.ui - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent - -data class SendBottomSheetConfig( - val currentState: SendStates, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt index 7630c89501..9604a47dfd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt @@ -18,17 +18,18 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.SendUiState @Composable -internal fun SendNavigationButtons(currentState: SendStates) { +internal fun SendNavigationButtons(uiState: SendUiState.Content) { Row( modifier = Modifier .fillMaxWidth() .padding(bottom = TangemTheme.dimens.spacing12), ) { - SendSecondaryNavigationButton(currentState) + SendSecondaryNavigationButton(uiState) SendPrimaryNavigationButton( - currentState, + uiState = uiState, modifier = Modifier .weight(1f) .padding(horizontal = TangemTheme.dimens.spacing16), @@ -37,9 +38,9 @@ internal fun SendNavigationButtons(currentState: SendStates) { } @Composable -private fun SendSecondaryNavigationButton(currentState: SendStates) { +private fun SendSecondaryNavigationButton(uiState: SendUiState.Content) { AnimatedVisibility( - visible = currentState == SendStates.Recipient || currentState == SendStates.Fee, + visible = uiState is SendUiState.Content.RecipientState || uiState is SendUiState.Content.FeeState, ) { Icon( modifier = Modifier @@ -57,40 +58,37 @@ private fun SendSecondaryNavigationButton(currentState: SendStates) { } @Composable -private fun SendPrimaryNavigationButton(currentState: SendStates, modifier: Modifier = Modifier) { - val buttonTextId = when (currentState) { - SendStates.Amount, - SendStates.Recipient, - SendStates.Fee, +private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: Modifier = Modifier) { + val buttonTextId = when (uiState) { + is SendUiState.Content.AmountState, + is SendUiState.Content.RecipientState, + is SendUiState.Content.FeeState, -> R.string.common_next - SendStates.Filled -> R.string.common_send - SendStates.Done -> R.string.common_close + is SendUiState.Content.SendState -> R.string.common_send + else -> R.string.common_close } AnimatedContent( targetState = buttonTextId, label = "Update send screen state", modifier = modifier, ) { textId -> - when (currentState) { - SendStates.Amount, - SendStates.Recipient, - SendStates.Fee, - SendStates.Done, - -> PrimaryButton( + if (uiState is SendUiState.Content.SendState) { + PrimaryButtonIconEnd( text = stringResource(textId), + iconResId = R.drawable.ic_tangem_24, + enabled = uiState.nextButtonEnabled, + onClick = { + // todo add next click + }, + ) + } else { + PrimaryButton( + text = stringResource(textId), + enabled = uiState.nextButtonEnabled, onClick = { // todo add next click }, ) - SendStates.Filled -> { - PrimaryButtonIconEnd( - text = stringResource(textId), - iconResId = R.drawable.ic_tangem_24, - onClick = { - // todo add next click - }, - ) - } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt index 3ec40b3169..61351bd875 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt @@ -9,17 +9,14 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.SendUiState @Composable fun SendScreen() { - // todo will be removed - val config = remember { SendBottomSheetConfig(SendStates.Amount) } - Column( modifier = Modifier .imePadding() @@ -42,7 +39,7 @@ fun SendScreen() { ) { SendScreenContent() } - SendNavigationButtons(config.currentState) + SendNavigationButtons(uiState = SendUiState.Content.Initial()) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt deleted file mode 100644 index f0a3e9840b..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendStates.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.features.send.impl.presentation.send.ui - -/** - * Screen states of Send - */ -enum class SendStates { - Amount, - Recipient, - Fee, - Filled, - Done, - ; - - companion object { - - /** Get next [SendStates] */ - fun SendStates.next(): SendStates { - return if (this.ordinal < SendStates.values().last().ordinal) { - SendStates.values()[this.ordinal + 1] - } else { - this - } - } - - /** Get previous [SendStates] */ - fun SendStates.previous(): SendStates { - return if (this.ordinal > 0) { - SendStates.values()[this.ordinal - 1] - } else { - this - } - } - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt new file mode 100644 index 0000000000..4eda379a95 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountField.kt @@ -0,0 +1,168 @@ +package com.tangem.features.send.impl.presentation.send.ui.amount + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.BottomCenter +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField + +@Composable +internal fun ColumnScope.AmountField( + sendField: SendTextField.Amount, + cryptoSymbol: String, + fiatSymbol: String, + isFiat: Boolean, +) { + val value = if (isFiat) sendField.fiatValue else sendField.value + val secondaryValue = if (!isFiat) sendField.fiatValue else sendField.value + val symbol = if (isFiat) fiatSymbol else cryptoSymbol + val secondarySymbol = if (!isFiat) fiatSymbol else cryptoSymbol + + AmountFieldInner( + value = value, + placeholder = sendField.placeholder, + symbol = symbol, + onValueChange = sendField.onValueChange, + keyboardOptions = sendField.keyboardOptions, + modifier = Modifier + .align(CenterHorizontally) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) + + Box( + modifier = Modifier + .align(CenterHorizontally) + .padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) { + Text( + text = "$secondaryValue $secondarySymbol", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .align(BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing32), + ) + AmountFieldError( + isError = sendField.isError, + error = sendField.error, + modifier = Modifier + .align(BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing12), + ) + } +} + +@Composable +private fun AmountFieldInner( + value: String, + placeholder: TextReference, + symbol: String, + onValueChange: (String) -> Unit, + keyboardOptions: KeyboardOptions, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .focusRequester(focusRequester) + .background(TangemTheme.colors.background.action), + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + keyboardOptions = keyboardOptions, + singleLine = true, + visualTransformation = AmountVisualTransformation(symbol), + decorationBox = { innerTextField -> + Box { + if (value.isBlank()) { + Text( + text = "${placeholder.resolveReference()} $symbol", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.disabled, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.TopCenter), + ) + } + innerTextField() + } + }, + ) +} + +@Composable +private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = isError, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + ) { + Text( + text = error.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.Center, + ) + } +} + +private class AmountVisualTransformation( + private val symbol: String, +) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return TransformedText( + buildAnnotatedString { + append(text) + if (text.isNotBlank()) { + append(" ") + append(symbol) + } + }, + object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return text.length + } + + override fun transformedToOriginal(offset: Int): Int { + return text.length + } + }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt new file mode 100644 index 0000000000..b7810d5acb --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/amount/AmountFieldContainer.kt @@ -0,0 +1,61 @@ +package com.tangem.features.send.impl.presentation.send.ui.amount + +import androidx.compose.foundation.background +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.material3.Text +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.text.style.TextAlign +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.send.state.SendUiState + +@Composable +internal fun AmountFieldContainer(amountState: SendUiState.Content.AmountState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing4, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action), + ) { + Text( + text = amountState.walletName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing14) + .align(Alignment.CenterHorizontally), + ) + Text( + text = amountState.walletBalance, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2) + .align(Alignment.CenterHorizontally), + ) + TokenIcon( + state = amountState.tokenIconState, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32) + .align(Alignment.CenterHorizontally), + ) + AmountField( + sendField = amountState.amountTextField, + cryptoSymbol = amountState.cryptoCurrencyStatus.currency.symbol, + fiatSymbol = amountState.appCurrency.symbol, + isFiat = amountState.isFiatValue, + ) + } +} \ No newline at end of file From d6370a1f59ca2c670f898f38882a6330701b1335 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Oct 2023 19:44:25 +0300 Subject: [PATCH 3/3] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 10 +- .../core/ui/utils/BigDecimalFormatter.kt | 8 +- .../send/api/navigation/SendRouter.kt | 1 + features/send/impl/build.gradle.kts | 2 + .../send/impl/presentation/SendFragment.kt | 19 ++- .../send/state/SendAmountStateConverter.kt | 63 +++++++ .../send/state/SendStateFactory.kt | 58 +++++++ .../presentation/send/state/SendUiState.kt | 27 ++- .../fields/SendAmountFieldChangeConverter.kt | 97 +++++++++++ .../state/fields/SendAmountFieldConverter.kt | 35 ++++ .../presentation/send/ui/SendAmountContent.kt | 109 ++++++++++++ .../send/ui/SendNavigationButtons.kt | 4 +- .../impl/presentation/send/ui/SendScreen.kt | 14 +- .../send/viewmodel/SendClickIntents.kt | 14 ++ .../send/viewmodel/SendViewModel.kt | 155 ++++++++++++++++++ 15 files changed, 592 insertions(+), 24 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 6e5f3a779f..40b4c0c73c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -366,7 +366,10 @@ class TradeCryptoMiddleware { ) } } - val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } @@ -416,7 +419,10 @@ class TradeCryptoMiddleware { is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") } - val bundle = bundleOf(SendRouter.CRYPTO_CURRENCY_KEY to currency) + val bundle = bundleOf( + SendRouter.CRYPTO_CURRENCY_KEY to currency, + SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 2350350cd0..d8e209f0ad 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -12,7 +12,9 @@ object BigDecimalFormatter { private const val TEMP_CURRENCY_CODE = "USD" - fun formatCryptoAmount(cryptoAmount: BigDecimal, cryptoCurrency: String, decimals: Int): String { + fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: String, decimals: Int): String { + if (cryptoAmount == null) return EMPTY_BALANCE_SIGN + val formatter = NumberFormat.getNumberInstance().apply { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 @@ -23,11 +25,13 @@ object BigDecimalFormatter { } fun formatFiatAmount( - fiatAmount: BigDecimal, + fiatAmount: BigDecimal?, fiatCurrencyCode: String, fiatCurrencySymbol: String, locale: Locale = Locale.getDefault(), ): String { + if (fiatAmount == null) return EMPTY_BALANCE_SIGN + val formatterCurrency = getCurrency(fiatCurrencyCode) val formatter = NumberFormat.getCurrencyInstance(locale).apply { currency = formatterCurrency diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index 692027bfe8..5a44a6614d 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -8,5 +8,6 @@ interface SendRouter { companion object { const val CRYPTO_CURRENCY_KEY = "send_crypto_currency" + const val USER_WALLET_ID_KEY = "send_user_wallet_id" } } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index c0cfcb3894..9c3a10e456 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.material) implementation(deps.arrow.core) + implementation(deps.tangem.card.core) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -38,6 +39,7 @@ dependencies { implementation(projects.core.utils) /** Domain modules */ + implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 334bdc5aeb..d33b82b860 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -4,10 +4,14 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.screen.ComposeBottomSheetFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.features.send.impl.presentation.send.state.SendUiState import com.tangem.features.send.impl.presentation.send.ui.SendScreen +import com.tangem.features.send.impl.presentation.send.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -24,13 +28,16 @@ internal class SendFragment : ComposeBottomSheetFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - SystemBarsEffect { - setSystemBarsColor(color = Color.Transparent) + val viewModel = hiltViewModel() + LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + + SystemBarsEffect { setSystemBarsColor(color = Color.Transparent) } + BackHandler { dismiss() } + + when (val state = viewModel.uiState) { + is SendUiState.Content -> SendScreen(state) + SendUiState.Dismiss -> dismiss() } - BackHandler { - dismiss() - } - SendScreen() } companion object { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt new file mode 100644 index 0000000000..c1a17d6011 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendAmountStateConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.send.impl.presentation.send.state + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class SendAmountStateConverter( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, + private val clickIntents: SendClickIntents, + private val iconStateConverter: CryptoCurrencyToIconStateConverter, + private val sendAmountFieldConverter: SendAmountFieldConverter, +) : Converter, SendUiState> { + + override fun convert(value: Either): SendUiState { + val userWallet = userWalletProvider() ?: return currentStateProvider() + val appCurrency = appCurrencyProvider() + return value.fold( + ifLeft = { + // TODO add error handling + currentStateProvider() + }, + ifRight = { + val fiat = formatFiatAmount(it.value.fiatAmount, appCurrency.code, appCurrency.symbol) + val crypto = formatCryptoAmount(it.value.amount, it.currency.symbol, it.currency.decimals) + SendUiState.Content.AmountState( + cryptoCurrencyStatus = it, + walletName = userWallet.name, + walletBalance = "$crypto ($fiat)", + tokenIconState = iconStateConverter.convert(it), + appCurrency = appCurrency, + amountTextField = sendAmountFieldConverter.convert(Unit), + isFiatValue = false, + clickIntents = clickIntents, + segmentedButtonConfig = persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(it.currency.symbol), + iconState = iconStateConverter.convert(it), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconState = iconStateConverter.convert(it), + isFiat = true, + ), + ), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt new file mode 100644 index 0000000000..bfca56ad1d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendStateFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.features.send.impl.presentation.send.state + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldChangeConverter +import com.tangem.features.send.impl.presentation.send.state.fields.SendAmountFieldConverter +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents + +internal class SendStateFactory( + private val clickIntents: SendClickIntents, + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, +) { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + private val amountFieldConverter by lazy { SendAmountFieldConverter(clickIntents) } + + private val amountFieldChangeConverter by lazy { SendAmountFieldChangeConverter(currentStateProvider) } + + private val amountStateConverter by lazy { + SendAmountStateConverter( + currentStateProvider = currentStateProvider, + appCurrencyProvider = appCurrencyProvider, + clickIntents = clickIntents, + iconStateConverter = iconStateConverter, + userWalletProvider = userWalletProvider, + sendAmountFieldConverter = amountFieldConverter, + ) + } + + fun getInitialState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + + fun getAmountState(cryptoCurrencyStatus: Either): SendUiState { + return amountStateConverter.convert(cryptoCurrencyStatus) + } + + fun getOnReceiveState(): SendUiState = SendUiState.Content.Initial(clickIntents = clickIntents) + + fun getOnAmountValueChange(value: String) = amountFieldChangeConverter.convert(value) + + fun getOnCurrencyChangedState(isFiat: Boolean): SendUiState { + val state = currentStateProvider() + val amountState = state as? SendUiState.Content.AmountState ?: return state + + return if (amountState.isFiatValue == isFiat) { + state + } else { + return state.copy(isFiatValue = isFiat) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt index 10df3f1966..0c48ca14b0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/SendUiState.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.send.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList /** @@ -16,42 +17,54 @@ internal sealed class SendUiState { /** States with content */ sealed class Content : SendUiState() { - abstract val nextButtonEnabled: Boolean + /** Is primary button enabled */ + abstract val isPrimaryButtonEnabled: Boolean + + /** Click intents */ + abstract val clickIntents: SendClickIntents /** Initial state */ data class Initial( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, ) : Content() /** Amount state */ data class AmountState( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, val walletName: String, val walletBalance: String, val tokenIconState: TokenIconState, val cryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrency: AppCurrency, val isFiatValue: Boolean, - val segmentedButtonConfig: PersistentList, + val segmentedButtonConfig: PersistentList, val amountTextField: SendTextField.Amount, ) : Content() // todo [REDACTED_JIRA] /** Recipient state */ data class RecipientState( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, ) : Content() // todo [REDACTED_JIRA] /** Fee and speed state */ data class FeeState( - override val nextButtonEnabled: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + override val clickIntents: SendClickIntents, ) : Content() // todo [REDACTED_JIRA] /** Send state */ data class SendState( - override val nextButtonEnabled: Boolean = true, + override val isPrimaryButtonEnabled: Boolean = true, + override val clickIntents: SendClickIntents, ) : Content() } + + /** Dismiss screen */ + object Dismiss : SendUiState() } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt new file mode 100644 index 0000000000..0775989341 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldChangeConverter.kt @@ -0,0 +1,97 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import com.tangem.common.Provider +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.utils.converter.Converter +import java.text.DecimalFormatSymbols +import java.text.NumberFormat + +internal class SendAmountFieldChangeConverter( + private val currentStateProvider: Provider, +) : Converter { + override fun convert(value: String): SendUiState { + val state = currentStateProvider() + + if ( + state !is SendUiState.Content.AmountState || + value.checkDecimalSeparatorDuplicate() + ) { + return state + } + + if (value.isEmpty()) return state.emptyState() + + val fiatRate = state.cryptoCurrencyStatus.value.fiatRate + + val trimmedValue = value.trim() + + val cryptoValue = if (state.isFiatValue) { + if (value.isNotBlank()) { + trimmedValue.toBigDecimal().divide(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + DEFAULT_VALUE + } + } else { + trimmedValue + } + + val fiatValue = if (!state.isFiatValue) { + if (value.isNotBlank()) { + trimmedValue.toBigDecimal().multiply(fiatRate)?.stripTrailingZeros()?.toPlainString().orEmpty() + } else { + DEFAULT_VALUE + } + } else { + trimmedValue + } + + val isExceedBalance = value.checkExceedBalance(state) + return state.copy( + amountTextField = state.amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + ), + isPrimaryButtonEnabled = !isExceedBalance, + ) + } + + private fun SendUiState.Content.AmountState.emptyState(): SendUiState { + return copy( + amountTextField = amountTextField.copy( + value = if (!isFiatValue) "" else DEFAULT_VALUE, + fiatValue = if (isFiatValue) "" else DEFAULT_VALUE, + isError = false, + ), + isPrimaryButtonEnabled = false, + ) + } + + private fun String.checkDecimalSeparatorDuplicate(): Boolean { + val regex = "[\\.\\,]".toRegex() + val decimalSeparatorCount = regex.findAll(this).count() + + return decimalSeparatorCount > 1 + } + + private fun String.checkExceedBalance(state: SendUiState.Content.AmountState): Boolean { + val currencyStatus = state.cryptoCurrencyStatus.value + return if (state.isFiatValue) { + toBigDecimal() > currencyStatus.fiatAmount + } else { + toBigDecimal() > currencyStatus.amount + } + } + + private fun String.trim(): String { + var trimmedValue = this + if (length > 1 && firstOrNull() == '0' && get(1).isDigit()) trimmedValue = drop(1) + + val separatorChar = DecimalFormatSymbols.getInstance().decimalSeparator.toString() + return trimmedValue.replace("[\\.\\,]".toRegex(), separatorChar) + } + + companion object { + private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt new file mode 100644 index 0000000000..8f09a10a04 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/state/fields/SendAmountFieldConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.impl.presentation.send.state.fields + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.send.viewmodel.SendClickIntents +import com.tangem.utils.converter.Converter +import java.text.NumberFormat + +internal class SendAmountFieldConverter( + private val clickIntents: SendClickIntents, +) : Converter { + + override fun convert(value: Unit): SendTextField.Amount { + return SendTextField.Amount( + value = "", + fiatValue = DEFAULT_VALUE, + onValueChange = clickIntents::onAmountValueChange, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + label = TextReference.Str(""), + placeholder = TextReference.Str(DEFAULT_VALUE), + isError = false, + error = TextReference.Res(R.string.send_insufficient_funds), + ) + } + + companion object { + private val DEFAULT_VALUE = NumberFormat.getInstance().format(0.00) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt new file mode 100644 index 0000000000..8f6dbcabc9 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendAmountContent.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.impl.presentation.send.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Alignment.Companion.CenterHorizontally +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.currency.fiaticon.FiatIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.send.state.SendAmountSegmentedButtonsConfig +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.features.send.impl.presentation.send.ui.amount.AmountFieldContainer + +@Composable +internal fun SendAmountContent(amountState: SendUiState.Content.AmountState) { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + Text( + text = stringResource(R.string.common_send), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) + .align(CenterHorizontally), + ) + AmountFieldContainer(amountState = amountState) + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) { + SegmentedButtons( + modifier = Modifier + .height(TangemTheme.dimens.size40) + .weight(1f), + config = amountState.segmentedButtonConfig, + onClick = { amountState.clickIntents.onCurrencyChangeClick(it.isFiat) }, + ) { + SendAmountCurrencyButton(it) + } + SecondaryButton( + text = stringResource(R.string.send_max_amount), + onClick = amountState.clickIntents::onMaxValueClick, + size = TangemButtonSize.Text, + shape = RoundedCornerShape(TangemTheme.dimens.radius26), + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .height(TangemTheme.dimens.size40), + ) + } + } +} + +@Composable +private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig) { + Row( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing10, + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (button.isFiat) { + FiatIcon( + url = button.iconUrl, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } else { + button.iconState?.let { + TokenIcon( + state = it, + shouldDisplayNetwork = false, + modifier = Modifier + .size(TangemTheme.dimens.size18), + ) + } + } + Text( + text = button.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt index 9604a47dfd..cf453d487a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendNavigationButtons.kt @@ -76,7 +76,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: PrimaryButtonIconEnd( text = stringResource(textId), iconResId = R.drawable.ic_tangem_24, - enabled = uiState.nextButtonEnabled, + enabled = uiState.isPrimaryButtonEnabled, onClick = { // todo add next click }, @@ -84,7 +84,7 @@ private fun SendPrimaryNavigationButton(uiState: SendUiState.Content, modifier: } else { PrimaryButton( text = stringResource(textId), - enabled = uiState.nextButtonEnabled, + enabled = uiState.isPrimaryButtonEnabled, onClick = { // todo add next click }, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt index 61351bd875..1e52cf291a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/ui/SendScreen.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.send.state.SendUiState @Composable -fun SendScreen() { +internal fun SendScreen(uiState: SendUiState.Content) { Column( modifier = Modifier .imePadding() @@ -37,13 +37,17 @@ fun SendScreen() { .weight(1f) .scrollable(state = rememberScrollState(), orientation = Orientation.Vertical), ) { - SendScreenContent() + SendScreenContent(uiState) } - SendNavigationButtons(uiState = SendUiState.Content.Initial()) + SendNavigationButtons(uiState) } } @Composable -fun SendScreenContent() { - // todo work in progress +private fun SendScreenContent(uiState: SendUiState.Content) { + when (uiState) { + is SendUiState.Content.AmountState -> SendAmountContent(uiState) + else -> { /* [REDACTED_TODO_COMMENT]*/ + } + } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt new file mode 100644 index 0000000000..3e6885543a --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendClickIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.features.send.impl.presentation.send.viewmodel + +interface SendClickIntents { + + fun onNextClick() + + fun onPrevClick() + + fun onAmountValueChange(value: String) + + fun onCurrencyChangeClick(isFiat: Boolean) + + fun onMaxValueClick() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt new file mode 100644 index 0000000000..5bc0189731 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/send/viewmodel/SendViewModel.kt @@ -0,0 +1,155 @@ +package com.tangem.features.send.impl.presentation.send.viewmodel + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.* +import arrow.core.getOrElse +import com.tangem.common.Provider +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.send.impl.presentation.send.state.SendStateFactory +import com.tangem.features.send.impl.presentation.send.state.SendUiState +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class SendViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { + + private val userWalletId: UserWalletId = savedStateHandle.get(SendRouter.USER_WALLET_ID_KEY) + ?.let { stringValue -> UserWalletId(stringValue) } + ?: error("This screen can't open without `UserWalletId`") + + private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + ?: error("This screen can't open without `CryptoCurrency`") + + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + + private val stateFactory = SendStateFactory( + clickIntents = this, + currentStateProvider = Provider { uiState }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + ) + + var uiState: SendUiState by mutableStateOf(stateFactory.getInitialState()) + private set + + private var userWallet: UserWallet? = null + + private var balanceJobHolder = JobHolder() + + override fun onCreate(owner: LifecycleOwner) { + subscribeOnCurrencyStatusUpdates(owner) + } + + private fun subscribeOnCurrencyStatusUpdates(owner: LifecycleOwner) { + viewModelScope.launch(dispatchers.io) { + getUserWalletUseCase(userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + getCurrencyStatusUpdates(owner, wallet) + }, + ifLeft = { + // TODO add error handling + return@launch + }, + ) + } + } + + private fun getCurrencyStatusUpdates(owner: LifecycleOwner, wallet: UserWallet) { + val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + derivationPath = cryptoCurrency.network.derivationPath, + isSingleWalletWithTokens = isSingleWallet, + ) + .flowWithLifecycle(owner.lifecycle) + .conflate() + .distinctUntilChanged() + .onEach { either -> + uiState = stateFactory.getAmountState( + cryptoCurrencyStatus = either, + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(balanceJobHolder) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + // region screen state navigation + override fun onNextClick() { + when (uiState) { + is SendUiState.Content.AmountState -> onRecipientStateClick() + is SendUiState.Content.RecipientState -> onFeeStateClick() + else -> { + // todo implement + } + } + } + + override fun onPrevClick() { + // todo implement + } + + private fun onRecipientStateClick() { + stateFactory.getOnReceiveState() + } + + private fun onFeeStateClick() { + // todo implement + } + // endregion + + // region amount state clicks + override fun onCurrencyChangeClick(isFiat: Boolean) { + uiState = stateFactory.getOnCurrencyChangedState(isFiat) + } + + override fun onAmountValueChange(value: String) { + uiState = stateFactory.getOnAmountValueChange(value) + } + + override fun onMaxValueClick() { + val amountState = uiState as? SendUiState.Content.AmountState ?: return + + val amount = if (amountState.isFiatValue) { + amountState.cryptoCurrencyStatus.value.fiatAmount + } else { + amountState.cryptoCurrencyStatus.value.amount + } + onAmountValueChange(amount?.toPlainString() ?: "0.00") + } + // endregion +} \ No newline at end of file