Updated on 2026-08-14
This commit is contained in:
parent
9b70e5bc82
commit
300179445d
11 changed files with 417 additions and 150 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 <a href =
|
||||
* "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1217-922&mode=design&t=t1PxyisyGJSwzQwg-4"
|
||||
* Market price block
|
||||
*
|
||||
* @param state component state
|
||||
* @param modifier modifier
|
||||
*
|
||||
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1217-922&mode
|
||||
* =design&t=t1PxyisyGJSwzQwg-4"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@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"),
|
||||
),
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<List<NetworkStatus>> = 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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<WalletState>,
|
||||
private val fiatCurrencyCode: String,
|
||||
private val fiatCurrencySymbol: String,
|
||||
) : Converter<Either<CurrencyError, CryptoCurrencyStatus>, WalletSingleCurrencyState.Content> {
|
||||
|
||||
override fun convert(value: Either<CurrencyError, CryptoCurrencyStatus>): 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserWallet>, selectedWalletIndex: Int): WalletState {
|
||||
|
|
@ -189,4 +199,10 @@ internal class WalletStateFactory(
|
|||
WalletManageButton.CopyAddress(onClick = {}),
|
||||
)
|
||||
}
|
||||
|
||||
fun getSingleCurrencyLoadedBalanceState(
|
||||
cryptoCurrencyEither: Either<CurrencyError, CryptoCurrencyStatus>,
|
||||
): WalletState {
|
||||
return singleCurrencyLoadedBalanceConverter.convert(cryptoCurrencyEither)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<UserWallet> 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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue