diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 274f5bbe46..d5ab303555 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -28,7 +28,7 @@ internal object TokensDomainModule { @Provides @ViewModelScoped - fun provideGetPrimaryCurrencyUseCase( + fun provideGetCurrencyUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, @@ -37,6 +37,17 @@ internal object TokensDomainModule { return GetCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetPrimaryCurrencyUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetPrimaryCurrencyUseCase { + return GetPrimaryCurrencyUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + } + @Provides @ViewModelScoped fun provideToggleTokenListGroupingUseCase( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index d7f217308c..4192706627 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -1,5 +1,7 @@ package com.tangem.core.ui.components.marketprice +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -20,15 +22,22 @@ import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter /** - * @see Figma component */ @Composable fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) { var rootWidth by remember { mutableStateOf(value = 0) } + Column( modifier = modifier .background( @@ -42,81 +51,139 @@ fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), horizontalAlignment = Alignment.Start, ) { - Text( - text = stringResource(id = R.string.wallet_marketplace_block_title, state.currencyName), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - ) + Title(currencyName = state.currencyName) - when (state) { - is MarketPriceBlockState.Loading -> { - RectangleShimmer( - modifier = Modifier.size(width = TangemTheme.dimens.size158, height = TangemTheme.dimens.size20), - ) - } - is MarketPriceBlockState.Content -> { - Price( - config = state, + Content(state = state, rootWidth = rootWidth) + } +} + +@Composable +private fun Title(currencyName: String) { + Text( + text = stringResource(id = R.string.wallet_marketplace_block_title, currencyName), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun Content(state: MarketPriceBlockState, rootWidth: Int) { + AnimatedContent(targetState = state, label = "Update the content") { marketPriceBlockState -> + when (marketPriceBlockState) { + is MarketPriceBlockState.Content, + is MarketPriceBlockState.Error, + -> { + PriceContent( + state = marketPriceBlockState, priceWidthDp = with(LocalDensity.current) { rootWidth.div(other = 2).toDp() }, ) } + is MarketPriceBlockState.Loading -> LoadingContent() } } } @Composable -private fun Price(config: MarketPriceBlockState.Content, priceWidthDp: Dp) { +private fun PriceContent(state: MarketPriceBlockState, priceWidthDp: Dp) { Row( - modifier = Modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), ) { - Text( - text = config.price, - modifier = Modifier.widthIn(max = priceWidthDp), - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) + PriceBlock(state = state, priceWidthDp = priceWidthDp) - PriceChangeInPercent(config.priceChangeConfig) + QuoteTimeStatus() + } +} - Text( - text = stringResource(id = R.string.wallet_marketprice_block_update_time), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { + val priceModifier = Modifier.widthIn(max = priceWidthDp) + AnimatedContent(targetState = state, label = "Update the price block") { marketPriceBlockState -> + if (marketPriceBlockState is MarketPriceBlockState.Content) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + ) { + Price(price = marketPriceBlockState.price, modifier = priceModifier) + + PriceChangeInPercent(marketPriceBlockState.priceChangeConfig) + } + } else { + Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) + } } } +@Composable +private fun Price(price: String, modifier: Modifier = Modifier) { + Text( + text = price, + modifier = modifier, + color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = TangemTheme.typography.body2, + ) +} + +@OptIn(ExperimentalAnimationApi::class) @Composable private fun PriceChangeInPercent(config: PriceChangeConfig) { + AnimatedContent(targetState = config.type, label = "Update price change") { type -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), + ) { + Image( + painter = painterResource( + id = when (type) { + PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + }, + ), + contentDescription = null, + ) + + Text( + text = config.valueInPercent, + color = when (type) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning + }, + style = TangemTheme.typography.body2, + ) + } + } +} + +@Composable +private fun LoadingContent() { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), ) { - Image( - painter = painterResource( - id = when (config.type) { - PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 - }, + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size158, + height = TangemTheme.dimens.size20, ), - contentDescription = null, ) - Text( - text = config.valueInPercent, - color = when (config.type) { - PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent - PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning - }, - style = TangemTheme.typography.body2, - ) + QuoteTimeStatus() } } +@Composable +private fun QuoteTimeStatus() { + Text( + text = stringResource(id = R.string.wallet_marketprice_block_update_time), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + ) +} + @Preview @Composable private fun Preview_MarketPriceBlock_Light( @@ -143,7 +210,7 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr collection = listOf( MarketPriceBlockState.Content( currencyName = "BTC", - price = "98900", + price = "98900 $", priceChangeConfig = PriceChangeConfig( valueInPercent = "5.16%", type = PriceChangeConfig.Type.DOWN, @@ -151,12 +218,13 @@ private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterPr ), MarketPriceBlockState.Content( currencyName = "BTC", - price = "98900", + price = "98900 $", priceChangeConfig = PriceChangeConfig( valueInPercent = "10.89%", type = PriceChangeConfig.Type.UP, ), ), MarketPriceBlockState.Loading(currencyName = "BTC"), + MarketPriceBlockState.Error(currencyName = "BTC"), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt index ae59401aa9..30b652ea68 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt @@ -7,6 +7,8 @@ sealed interface MarketPriceBlockState { val currencyName: String + data class Error(override val currencyName: String) : MarketPriceBlockState + data class Loading(override val currencyName: String) : MarketPriceBlockState data class Content( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index d70869c7b8..e9e235c8d1 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.tokens.repository import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.utils.CardCurrenciesFactory import com.tangem.data.tokens.utils.NetworkConverter import com.tangem.data.tokens.utils.NetworkStatusFactory import com.tangem.data.tokens.utils.ResponseCurrenciesFactory @@ -32,8 +33,10 @@ internal class DefaultNetworksRepository( private val dispatchers: CoroutineDispatcherProvider, ) : NetworksRepository { + private val demoConfig by lazy { DemoConfig() } private val networkConverter by lazy { NetworkConverter() } - private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(DemoConfig()) } + private val cardCurrenciesFactory by lazy { CardCurrenciesFactory(demoConfig) } + private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(demoConfig) } private val networkStatusFactory by lazy { NetworkStatusFactory() } private val networksStatuses: MutableStateFlow> = MutableStateFlow(emptyList()) @@ -107,11 +110,18 @@ internal class DefaultNetworksRepository( val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find user wallet with provided ID: $userWalletId" } - val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - } - return responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card) + return if (userWallet.isMultiCurrency) { + val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + } + + responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card) + } else { + val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + + listOf(currency) + } } private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId" diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt index 8d2ac0b3be..9a5858f0dc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt @@ -61,7 +61,7 @@ internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { } private fun createCoin(blockchain: Blockchain, card: CardDTO): CryptoCurrency.Coin? { - if (blockchain != Blockchain.Unknown) { + if (blockchain == Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt index 352b0687a2..dd3bbbeee7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletLockedState.kt @@ -21,4 +21,12 @@ internal sealed interface WalletLockedState { /** Lambda be invoked when bottom sheet is dismissed */ val onBottomSheetDismiss: () -> Unit + + /** Get selected wallet index */ + fun getSelectedWalletIndex(): Int { + return when (this) { + is WalletMultiCurrencyState.Locked -> walletsListConfig.selectedWalletIndex + is WalletSingleCurrencyState.Locked -> walletsListConfig.selectedWalletIndex + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt new file mode 100644 index 0000000000..6087b1f2fa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -0,0 +1,120 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.error.CurrencyError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class WalletSingleCurrencyLoadedBalanceConverter( + private val currentStateProvider: Provider, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter, WalletSingleCurrencyState.Content> { + + override fun convert(value: Either): WalletSingleCurrencyState.Content { + return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + } + + private fun convertError(): WalletSingleCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + } + + private fun convert(status: CryptoCurrencyStatus): WalletSingleCurrencyState.Content { + val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + val currencyName = state.marketPriceBlockState.currencyName + return state.copy( + walletsListConfig = getUpdatedSelectedWallet(status.value, state), + marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + ) + } + + private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { + return when (status) { + is CryptoCurrencyStatus.Loaded -> MarketPriceBlockState.Content( + currencyName = currencyName, + price = BigDecimalFormatter.formatFiatAmount( + fiatAmount = status.fiatRate, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + ), + priceChangeConfig = PriceChangeConfig( + valueInPercent = BigDecimalFormatter.formatPercent( + percent = status.priceChange, + useAbsoluteValue = true, + ), + type = if (status.priceChange > BigDecimal.ZERO) { + PriceChangeConfig.Type.UP + } else { + PriceChangeConfig.Type.DOWN + }, + ), + ) + is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName) + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Unreachable, + -> MarketPriceBlockState.Error(currencyName) + } + } + + private fun getUpdatedSelectedWallet( + status: CryptoCurrencyStatus.Status, + state: WalletSingleCurrencyState, + ): WalletsListConfig { + val selectedWallet = state.walletsListConfig.wallets[state.walletsListConfig.selectedWalletIndex] + val updatedWallet = when (status) { + is CryptoCurrencyStatus.Loaded -> { + WalletCardState.Content( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = selectedWallet.additionalInfo, + imageResId = selectedWallet.imageResId, + onClick = selectedWallet.onClick, + balance = BigDecimalFormatter.formatFiatAmount( + fiatAmount = status.fiatAmount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + ), + ) + } + is CryptoCurrencyStatus.Loading -> { + WalletCardState.Loading( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = selectedWallet.additionalInfo, + imageResId = selectedWallet.imageResId, + onClick = selectedWallet.onClick, + ) + } + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.Unreachable, + -> { + WalletCardState.Error( + id = selectedWallet.id, + title = selectedWallet.title, + additionalInfo = selectedWallet.additionalInfo, + imageResId = selectedWallet.imageResId, + onClick = selectedWallet.onClick, + ) + } + } + + return state.walletsListConfig.copy( + wallets = state.walletsListConfig.wallets.toPersistentList() + .set(index = state.walletsListConfig.selectedWalletIndex, element = updatedWallet), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 0f4d3bd755..c1ab20757d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -4,7 +4,9 @@ import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.error.CurrencyError import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError @@ -64,6 +66,14 @@ internal class WalletStateFactory( ) } + private val singleCurrencyLoadedBalanceConverter by lazy { + WalletSingleCurrencyLoadedBalanceConverter( + currentStateProvider = currentStateProvider, + fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] + fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] + ) + } + fun getInitialState(): WalletState = WalletState.Initial(onBackClick = clickIntents::onBackClick) fun getSkeletonState(wallets: List, selectedWalletIndex: Int): WalletState { @@ -189,4 +199,10 @@ internal class WalletStateFactory( WalletManageButton.CopyAddress(onClick = {}), ) } + + fun getSingleCurrencyLoadedBalanceState( + cryptoCurrencyEither: Either, + ): WalletState { + return singleCurrencyLoadedBalanceConverter.convert(cryptoCurrencyEither) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index fd0498866a..ccff84d714 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -1,6 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -20,6 +23,7 @@ import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState @@ -64,85 +68,92 @@ internal fun WalletCard(state: WalletCardState, modifier: Modifier = Modifier) { } val imageWidth = TangemTheme.dimens.size120 - state.imageResId?.let { - WalletImage( - id = it, - modifier = Modifier.constrainAs(imageItem) { - centerVerticallyTo(parent) - top.linkTo(parent.top) - end.linkTo(parent.end) - height = Dimension.fillToConstraints - width = Dimension.value(imageWidth) - }, - ) - } + WalletImage( + id = state.imageResId, + modifier = Modifier.constrainAs(imageItem) { + centerVerticallyTo(parent) + top.linkTo(parent.top) + end.linkTo(parent.end) + height = Dimension.fillToConstraints + width = Dimension.value(imageWidth) + }, + ) } } } +@OptIn(ExperimentalAnimationApi::class) @Composable private fun Title(state: WalletCardState) { - when (state) { - is WalletCardState.HiddenContent -> { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + AnimatedContent(targetState = state, label = "Update the title") { + when (it) { + is WalletCardState.HiddenContent -> { + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { + Text( + text = it.title, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + ) + Icon( + modifier = Modifier.size(size = TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_eye_off_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } + is WalletCardState.Content, + is WalletCardState.Error, + is WalletCardState.Loading, + -> { Text( - text = state.title, + text = it.title, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, maxLines = 1, ) - Icon( - modifier = Modifier.size(size = TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_eye_off_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) } } - else -> { - Text( - text = state.title, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - ) - } } } +@OptIn(ExperimentalAnimationApi::class) @Composable private fun Balance(state: WalletCardState) { - when (state) { - is WalletCardState.Content -> { - ResizableText( - text = state.balance, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), - ) - } - is WalletCardState.Loading -> { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size102, - height = TangemTheme.dimens.size24, - ), - ) - } - is WalletCardState.HiddenContent -> { - Text( - text = DOTS, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - } - is WalletCardState.Error -> { - Text( - text = "—", - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) + AnimatedContent(targetState = state, label = "Update the balance") { + when (it) { + is WalletCardState.Content -> { + ResizableText( + text = it.balance, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), + ) + } + is WalletCardState.Loading -> { + RectangleShimmer( + modifier = Modifier.size( + width = TangemTheme.dimens.size102, + height = TangemTheme.dimens.size24, + ), + ) + } + is WalletCardState.HiddenContent -> { + Text( + text = DOTS, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + } + is WalletCardState.Error -> { + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + } } } } @@ -157,22 +168,23 @@ private fun AdditionalInfo(description: String) { } @Composable -private fun WalletImage(@DrawableRes id: Int, modifier: Modifier = Modifier) { - Image( - painter = painterResource(id), - contentDescription = null, - modifier = modifier, - contentScale = ContentScale.FillWidth, - ) +private fun WalletImage(@DrawableRes id: Int?, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = id != null, modifier = modifier) { + Image( + painter = painterResource(id = requireNotNull(id)), + contentDescription = null, + contentScale = ContentScale.FillWidth, + ) + } } // region Preview -@Preview +@Preview(widthDp = 360, heightDp = 360) @Composable private fun Preview_WalletCard_LightTheme(@PreviewParameter(WalletCardStateProvider::class) state: WalletCardState) { TangemTheme(isDark = false) { - WalletCard(state) + WalletCard(state = state, modifier = Modifier.fillMaxWidth()) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt index 94c0e30ade..6414445119 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt @@ -11,8 +11,8 @@ internal class JobHolder { private var job: Job? = null - /** Update current job */ - fun update(job: Job) { + /** Update current [job] */ + fun update(job: Job?) { this.job?.cancel() this.job = job } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index bc50b0d051..eb8e6247e1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import androidx.paging.cachedIn +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess @@ -14,6 +16,7 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.Network @@ -21,6 +24,7 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState @@ -55,6 +59,7 @@ internal class WalletViewModel @Inject constructor( private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, + private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyUseCase, private val getCardWasScannedUseCase: GetCardWasScannedUseCase, private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -97,6 +102,7 @@ internal class WalletViewModel @Inject constructor( private var wallets: List by Delegates.notNull() private val tokensJobHolder = JobHolder() + private val marketPriceJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() override fun onCreate(owner: LifecycleOwner) { @@ -115,10 +121,7 @@ internal class WalletViewModel @Inject constructor( val currentState = uiState val selectedWalletIndex = if (currentState is WalletLockedState) { - when (currentState) { - is WalletMultiCurrencyState.Locked -> currentState.walletsListConfig.selectedWalletIndex - is WalletSingleCurrencyState.Locked -> currentState.walletsListConfig.selectedWalletIndex - } + currentState.getSelectedWalletIndex() } else { val selectedWallet = getSelectedWalletUseCase().fold( ifLeft = { error("Selected wallet is null") }, @@ -135,12 +138,12 @@ internal class WalletViewModel @Inject constructor( val cardTypeResolver = getCardTypeResolver(index) when { getWallet(index).isLocked -> uiState = stateFactory.getLockedState() - cardTypeResolver.isMultiwalletAllowed() -> updateByTokensList(index, isRefreshing) - !cardTypeResolver.isMultiwalletAllowed() -> updateByTxHistory(index) + cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing) + !cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index) } } - private fun updateByTokensList(index: Int, isRefreshing: Boolean = false) { + private fun updateMultiCurrencyContent(index: Int, isRefreshing: Boolean = false) { val state = requireNotNull(uiState as? WalletMultiCurrencyState) { "Impossible to update tokens list if state isn't WalletMultiCurrencyState" } @@ -163,11 +166,19 @@ internal class WalletViewModel @Inject constructor( .saveIn(tokensJobHolder) } - private fun updateByTxHistory(index: Int) { + private fun updateSingleCurrencyContent(index: Int) { + val wallet = getWallet(index) + updateTxHistory( + blockchain = getCardTypeResolver(index).getBlockchain(), + derivationStyle = wallet.scanResponse.card.derivationStyle, + ) + updateMarketPrice(userWalletId = wallet.walletId) + updateNotifications(index) + } + + private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) { viewModelScope.launch(dispatchers.io) { - val wallet = getWallet(index) - val blockchain = getCardTypeResolver(index).getBlockchain() - val derivationPath = blockchain.derivationPath(style = wallet.scanResponse.card.derivationStyle)?.rawPath + val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath val txHistoryItemsCountEither = txHistoryItemsCountUseCase( networkId = Network.ID(blockchain.id), @@ -177,20 +188,25 @@ internal class WalletViewModel @Inject constructor( uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) txHistoryItemsCountEither.onRight { - updateTxHistory( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + networkId = Network.ID(blockchain.id), + derivationPath = derivationPath, + ).map { + it.cachedIn(viewModelScope) + }, ) } } - - updateNotifications(index) } - private fun updateTxHistory(networkId: Network.ID, derivationPath: String?) { - uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase(networkId, derivationPath).map { it.cachedIn(viewModelScope) }, - ) + private fun updateMarketPrice(userWalletId: UserWalletId) { + getPrimaryCurrencyUseCase(userWalletId = userWalletId) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSingleCurrencyLoadedBalanceState(cryptoCurrencyEither = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) } private fun updateNotifications(index: Int, tokenList: TokenList? = null) { @@ -300,6 +316,10 @@ internal class WalletViewModel @Inject constructor( if (state.walletsListConfig.selectedWalletIndex == index) return + tokensJobHolder.update(job = null) + marketPriceJobHolder.update(job = null) + notificationsJobHolder.update(job = null) + uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index) updateContentItems(index = index) @@ -328,7 +348,7 @@ internal class WalletViewModel @Inject constructor( override fun onReloadClick() { uiState = stateFactory.getStateAfterContentRefreshing() - updateByTxHistory( + updateSingleCurrencyContent( index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, ) }