Updated on 2026-08-14

This commit is contained in:
Tangem 2024-10-23 20:01:15 +03:00
commit cab30e9431
186 changed files with 3729 additions and 2030 deletions

View file

@ -0,0 +1,35 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Token list UM
*
* @property items items (search bar, tokens, headers)
* @property isBalanceHidden flag that indicates if balance should be hidden
*
[REDACTED_AUTHOR]
*/
internal data class TokenListUM(
val items: ImmutableList<TokensListItemState>,
val isBalanceHidden: Boolean,
) {
/** Get search bar if it exists */
fun getSearchBar(): TokensListItemState.SearchBar? {
return items.firstOrNull() as? TokensListItemState.SearchBar
}
/** Get tokens */
fun getTokens(): ImmutableList<TokensListItemState> {
if (getSearchBar() == null) return items
return if (items.size > 1) {
items.subList(fromIndex = 1, toIndex = items.size)
} else {
persistentListOf()
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import timber.log.Timber
import javax.inject.Inject
/**
* [TokenListUM] controller
*
[REDACTED_AUTHOR]
*/
internal class TokenListUMController @Inject constructor() {
val state: StateFlow<TokenListUM> get() = _state
private val _state: MutableStateFlow<TokenListUM> = MutableStateFlow(
value = TokenListUM(
items = persistentListOf(createInitialSearchBar()),
isBalanceHidden = false,
),
)
fun update(transform: (TokenListUM) -> TokenListUM) {
Timber.d("Applying non-name transformation")
_state.update(transform)
}
fun update(transformer: TokenListUMTransformer) {
Timber.d("Applying ${transformer::class.simpleName}")
_state.update(transformer::transform)
}
/** Get search bar if it exists */
fun getSearchBar(): TokensListItemState.SearchBar? = _state.value.getSearchBar()
private fun createInitialSearchBar(): TokensListItemState.SearchBar {
return TokensListItemState.SearchBar(
searchBarUM = SearchBarUM(
placeholderText = resourceReference(id = R.string.common_search),
query = "",
onQueryChange = {},
isActive = false,
onActiveChange = {},
),
)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity
import com.tangem.utils.transformer.Transformer
/**
* Base [TokenListUM] transformer
*
[REDACTED_AUTHOR]
*/
internal interface TokenListUMTransformer : Transformer<TokenListUM>

View file

@ -0,0 +1,33 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM
import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMTransformer
import kotlinx.collections.immutable.persistentListOf
/**
* Base [SearchBarUM] transformer
*
[REDACTED_AUTHOR]
*/
internal abstract class SearchBarUMTransformer : TokenListUMTransformer {
abstract fun transform(prevState: SearchBarUM): SearchBarUM
override fun transform(prevState: TokenListUM): TokenListUM {
val searchBarItem = prevState.getSearchBar()
return if (searchBarItem != null) {
val updatedSearchBar = searchBarItem.copy(searchBarUM = transform(searchBarItem.searchBarUM))
prevState.copy(
items = persistentListOf(
updatedSearchBar,
*prevState.getTokens().toTypedArray(),
),
)
} else {
prevState
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.wallet.impl.R
internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() {
override fun transform(prevState: SearchBarUM): SearchBarUM {
val placeholderText = if (isActive) {
TextReference.EMPTY
} else {
resourceReference(id = R.string.common_search)
}
return prevState.copy(placeholderText = placeholderText, isActive = isActive)
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
internal class UpdateSearchBarIntentsTransformer(
private val onQueryChange: (String) -> Unit,
private val onActiveChange: (Boolean) -> Unit,
) : SearchBarUMTransformer() {
override fun transform(prevState: SearchBarUM): SearchBarUM {
return prevState.copy(onQueryChange = onQueryChange, onActiveChange = onActiveChange)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer
import com.tangem.core.ui.components.fields.entity.SearchBarUM
internal class UpdateSearchQueryTransformer(private val newQuery: String) : SearchBarUMTransformer() {
override fun transform(prevState: SearchBarUM): SearchBarUM {
return prevState.copy(query = newQuery)
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.feature.wallet.presentation.tokenlist.entity.transformer
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUM
import com.tangem.feature.wallet.presentation.tokenlist.entity.TokenListUMTransformer
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState
import kotlinx.collections.immutable.toImmutableList
internal class UpdateTokenItemsTransformer(
appCurrency: AppCurrency,
onItemClick: (CryptoCurrencyStatus) -> Unit,
private val statuses: List<CryptoCurrencyStatus>,
private val isBalanceHidden: Boolean,
) : TokenListUMTransformer {
private val converter = TokenItemStateConverter(appCurrency = appCurrency, onItemClick = onItemClick)
override fun transform(prevState: TokenListUM): TokenListUM {
val items = converter.convertList(input = statuses).map(TokensListItemState::Token)
return prevState.copy(
items = (listOfNotNull(prevState.getSearchBar()) + items).toImmutableList(),
isBalanceHidden = isBalanceHidden,
)
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
import com.tangem.domain.analytics.model.WalletBalanceState
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
@ -38,7 +37,7 @@ internal class TokenListAnalyticsSender @Inject constructor(
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) return
val currenciesStatuses = getCurrenciesStatuses(tokenList)
val currenciesStatuses = tokenList.flattenCurrencies()
sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses)
sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses)
@ -46,12 +45,6 @@ internal class TokenListAnalyticsSender @Inject constructor(
sendTokenBalancesIfNeeded(currenciesStatuses)
}
private fun getCurrenciesStatuses(tokenList: TokenList): List<CryptoCurrencyStatus> = when (tokenList) {
is TokenList.Empty -> emptyList()
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
}
private fun sendBalanceLoadedEventIfNeeded(
fiatBalance: TotalFiatBalance,
currenciesStatuses: List<CryptoCurrencyStatus>,

View file

@ -7,11 +7,9 @@ import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.promo.PromoBanner
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.settings.ShouldShowRingPromoUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.repository.PromoRepository
import com.tangem.domain.wallets.models.UserWallet
@ -30,7 +28,7 @@ import kotlin.collections.count
@Suppress("LongParameterList")
@ViewModelScoped
internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase,
@ -44,7 +42,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) }
return combine(
flow = getTokenListUseCase.launch(userWallet.walletId),
flow = tokenListStore.getOrThrow(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = shouldShowRingPromoUseCase(userWalletId = userWallet.walletId),
@ -129,6 +127,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
clickIntents: WalletClickIntents,
) {
val currencies = maybeTokenList.getMissingAddressCurrencies()
.ifEmpty { return }
addIf(
element = WalletNotification.Informational.MissingAddresses(
@ -144,13 +143,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun Lce<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return emptyList()
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
return currencies
return tokenList
.flattenCurrencies()
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
}
@ -182,13 +176,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun Lce<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
return currencies.any { it.value is CryptoCurrencyStatus.Unreachable }
return tokenList.flattenCurrencies().any { it.value is CryptoCurrencyStatus.Unreachable }
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(

View file

@ -0,0 +1,62 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWalletId
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.shareIn
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
@ViewModelScoped
internal class MultiWalletTokenListStore @Inject constructor(
private val getTokenListUseCase: GetTokenListUseCase,
) {
private val flows: ConcurrentHashMap<UserWalletId, LceFlow<TokenListError, TokenList>> by lazy {
ConcurrentHashMap()
}
fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) {
if (flows[userWalletId] != null) {
Timber.d("Flow with token list for $userWalletId already exists")
return
}
coroutineScope.ensureActive()
flows[userWalletId] = getTokenListUseCase
.launch(userWalletId)
.shareIn(
scope = coroutineScope,
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
Timber.d("Flow with token list for $userWalletId created")
}
fun getOrThrow(userWalletId: UserWalletId): LceFlow<TokenListError, TokenList> {
return requireNotNull(flows[userWalletId]) {
"Flow with token list for $userWalletId doesn't exist"
}
}
fun remove(userWalletId: UserWalletId) {
flows.remove(userWalletId)
Timber.d("Flow with token list for $userWalletId removed")
}
fun clear() {
flows.clear()
Timber.d("All flows with token list cleared")
}
}

View file

@ -4,7 +4,8 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.plus
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.wallets.models.UserWallet
@ -97,13 +98,7 @@ internal object WalletAdditionalInfoFactory {
WalletAdditionalInfo(hideable = false, content = TextReference.Res(R.string.common_locked))
} else {
val blockchain = scanResponse.cardTypesResolver.getBlockchain()
val amount = currencyAmount?.let {
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = it,
cryptoCurrency = blockchain.currency,
decimals = blockchain.decimals(),
)
}
val amount = currencyAmount?.format { crypto(blockchain.currency, blockchain.decimals()) }
WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty()))
}

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain
import com.tangem.common.extensions.isZero
import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import javax.inject.Inject
@ -12,15 +11,7 @@ internal class WalletWithFundsChecker @Inject constructor(
) {
suspend fun check(tokenList: TokenList) {
val hasNonZeroWallets = when (tokenList) {
is TokenList.GroupedByNetwork -> {
tokenList.groups
.flatMap(NetworkGroup::currencies)
.hasNonZeroWallets()
}
is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets()
is TokenList.Empty -> false
}
val hasNonZeroWallets = tokenList.flattenCurrencies().hasNonZeroWallets()
if (hasNonZeroWallets) setWalletWithFundsFoundUseCase()
}

View file

@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber
@ -23,7 +23,7 @@ internal class MultiWalletContentLoader(
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
@ -38,7 +38,7 @@ internal class MultiWalletContentLoader(
clickIntents = clickIntents,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getTokenListUseCase = getTokenListUseCase,
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,

View file

@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
@ -21,7 +21,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
@ -35,7 +35,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
stateHolder = stateHolder,
tokenListAnalyticsSender = tokenListAnalyticsSender,
walletWithFundsChecker = walletWithFundsChecker,
getTokenListUseCase = getTokenListUseCase,
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.wallet.impl.R
@ -43,6 +44,11 @@ internal sealed class WalletTokensListState {
abstract val id: Any
data class SearchBar(
override val id: Any = "search_bar",
val searchBarUM: SearchBarUM,
) : TokensListItemState()
data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState()
data class Token(val state: TokenItemState) : TokensListItemState() {

View file

@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.visa.model.VisaCurrency
@ -41,11 +43,9 @@ internal class SetBalancesAndLimitsTransformer(
}
private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content(
availableBalance = BigDecimalFormatter.formatCryptoAmount(
visaCurrency.limits.remainingOtp,
visaCurrency.symbol,
visaCurrency.decimals,
),
availableBalance = visaCurrency.limits.remainingOtp.format {
crypto(visaCurrency.symbol, visaCurrency.decimals)
},
limitDays = Days.daysBetween(DateTime.now(), visaCurrency.limits.expirationDate).days.inc(),
isEnabled = true,
onClick = clickIntents::onBalancesAndLimitsClick,
@ -72,11 +72,9 @@ internal class SetBalancesAndLimitsTransformer(
imageResId = imageResId,
onRenameClick = onRenameClick,
onDeleteClick = onDeleteClick,
balance = BigDecimalFormatter.formatCryptoAmount(
visaCurrency.balances.available,
visaCurrency.symbol,
visaCurrency.decimals,
),
balance = visaCurrency.balances.available.format {
crypto(visaCurrency.symbol, visaCurrency.decimals)
},
cardCount = userWallet.getCardsCount(),
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBottomSheetConfig
@ -15,11 +16,7 @@ internal class BalancesAndLimitsBottomSheetConverter(
) : Converter<VisaCurrency, BalancesAndLimitsBottomSheetConfig> {
override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig {
fun formatAmount(amount: BigDecimal): String = BigDecimalFormatter.formatCryptoAmount(
amount,
cryptoCurrency = value.symbol,
decimals = value.decimals,
)
fun formatAmount(amount: BigDecimal): String = amount.format { crypto(value.symbol, value.decimals) }
val otpLimit = value.limits.remainingOtp.let(::formatAmount)
val noOtpLimit = value.limits.remainingNoOtp.let(::formatAmount)

View file

@ -4,6 +4,8 @@ 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.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -57,7 +59,7 @@ internal class SingleWalletMarketPriceConverter(
private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String {
val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true)
return priceChange.format { percent() }
}
private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType {

View file

@ -6,6 +6,8 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryItem.*
@ -15,7 +17,6 @@ import com.tangem.utils.StringsSigns.MINUS
import com.tangem.utils.StringsSigns.PLUS
import com.tangem.utils.converter.Converter
import com.tangem.utils.toBriefAddressFormat
import com.tangem.utils.toFormattedCurrencyString
internal class TxHistoryItemStateConverter(
private val symbol: String,
@ -129,6 +130,6 @@ internal class TxHistoryItemStateConverter(
this.amount.isZero() -> ""
else -> if (isOutgoing) MINUS else PLUS
}
return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals)
return prefix + amount.format { crypto(symbol = symbol, decimals = decimals) }
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.VisaCurrency
@ -66,11 +68,7 @@ internal class VisaTxDetailsBottomSheetConverter(
}
private fun formatNetworkAmount(amount: BigDecimal): String {
return BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = amount,
cryptoCurrency = visaCurrency.symbol,
decimals = visaCurrency.decimals,
)
return amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }
}
private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String {

View file

@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.VisaCurrency
@ -25,11 +27,7 @@ internal class VisaTxHistoryItemStateConverter(
return TransactionState.Content(
txHash = value.id,
amount = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = value.amount,
cryptoCurrency = visaCurrency.symbol,
decimals = visaCurrency.decimals,
),
amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) },
// Show tx fiat amount instead of tx time
time = BigDecimalFormatter.formatFiatAmount(
fiatAmount = value.fiatAmount,

View file

@ -8,7 +8,6 @@ import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.NetworkGroup
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
@ -41,11 +40,11 @@ internal abstract class BasicTokenListSubscriber(
private val sendAnalyticsJobHolder = JobHolder()
private val onTokenListReceivedJobHolder = JobHolder()
protected abstract fun tokenListFlow(): LceFlow<TokenListError, TokenList>
protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList>
override fun create(coroutineScope: CoroutineScope): Flow<*> {
return combine(
flow = tokenListFlow()
flow = tokenListFlow(coroutineScope)
.onEach { maybeTokenList ->
coroutineScope.launch {
sendTokenListAnalytics(maybeTokenList)
@ -99,15 +98,14 @@ internal abstract class BasicTokenListSubscriber(
private suspend fun startCheck(maybeTokenList: Lce<TokenListError, TokenList>) {
// Run Polkadot account health check
maybeTokenList.getOrNull()?.let { tokenList ->
val cryptoCurrencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
cryptoCurrencies.forEach {
runPolkadotAccountHealthCheckUseCase(userWallet.walletId, it.currency.network)
}
tokenList
.flattenCurrencies()
.forEach {
runPolkadotAccountHealthCheckUseCase(
userWalletId = userWallet.walletId,
network = it.currency.network,
)
}
}
}

View file

@ -4,7 +4,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -12,14 +11,16 @@ import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.CoroutineScope
@Suppress("LongParameterList")
internal class MultiWalletTokenListSubscriber(
private val userWallet: UserWallet,
private val getTokenListUseCase: GetTokenListUseCase,
private val tokenListStore: MultiWalletTokenListStore,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
@ -37,8 +38,10 @@ internal class MultiWalletTokenListSubscriber(
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): LceFlow<TokenListError, TokenList> {
return getTokenListUseCase.launch(userWallet.walletId)
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> {
tokenListStore.addIfNot(userWallet.walletId, coroutineScope)
return tokenListStore.getOrThrow(userWallet.walletId)
}
override suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) {
@ -67,12 +70,6 @@ internal class MultiWalletTokenListSubscriber(
}
private fun getCurrenciesIds(tokenList: TokenList): List<CryptoCurrency.ID> {
return when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { group ->
group.currencies.map { it.currency.id }
}
is TokenList.Ungrouped -> tokenList.currencies.map { it.currency.id }
is TokenList.Empty -> emptyList()
}
return tokenList.flattenCurrencies().map { it.currency.id }
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAn
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
@Suppress("LongParameterList")
@ -34,6 +35,9 @@ internal class SingleWalletWithTokenListSubscriber(
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
) {
override fun tokenListFlow(): LceFlow<TokenListError, TokenList> = getNodlTokenListUseCase(userWallet.walletId)
.map { it.toLce() }
override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow<TokenListError, TokenList> =
getNodlTokenListUseCase(
userWallet.walletId,
)
.map { it.toLce() }
}

View file

@ -387,7 +387,6 @@ private inline fun BaseScaffoldWithMarkets(
sheetContainerColor = backgroundColor.value,
scaffoldState = scaffoldState,
sheetPeekHeight = peekHeight,
sheetShadowElevation = 8.dp,
sheetShape = TangemTheme.shapes.bottomSheetLarge,
sheetContent = {
// hide bottom sheet when back pressed

View file

@ -1,8 +1,11 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -36,5 +39,11 @@ internal fun MultiCurrencyContentItem(
modifier = modifierWithBackground,
)
}
is TokensListItemState.SearchBar -> {
SearchBar(
state = state.searchBarUM,
modifier = modifierWithBackground.padding(all = 12.dp),
)
}
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
@ -69,6 +70,7 @@ internal class WalletViewModel @Inject constructor(
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val marketsFeatureToggles: MarketsFeatureToggles,
private val walletImageResolver: WalletImageResolver,
private val tokenListStore: MultiWalletTokenListStore,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -110,6 +112,8 @@ internal class WalletViewModel @Inject constructor(
override fun onCleared() {
super.onCleared()
tokenListStore.clear()
stateHolder.clear()
walletScreenContentLoader.cancelAll()
}
@ -280,7 +284,7 @@ internal class WalletViewModel @Inject constructor(
stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name))
}
is WalletsUpdateActionResolver.Action.Unknown -> {
Timber.w("Unable to perfom action: $action")
Timber.w("Unable to perform action: $action")
}
}
}
@ -318,6 +322,7 @@ internal class WalletViewModel @Inject constructor(
private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) {
walletScreenContentLoader.cancel(action.prevWalletId)
tokenListStore.remove(action.prevWalletId)
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
@ -357,6 +362,7 @@ internal class WalletViewModel @Inject constructor(
private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
walletScreenContentLoader.cancel(action.deletedWalletId)
tokenListStore.remove(action.deletedWalletId)
walletScreenContentLoader.load(
userWallet = action.selectedWallet,

View file

@ -136,7 +136,9 @@ internal class WalletsUpdateActionResolver @Inject constructor(
unlockedWallets = wallets.filterNot(UserWallet::isLocked),
)
}
isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet)
isSelectedWalletCardsCountChanged(state, selectedWallet) -> {
Action.UpdateWalletCardCount(selectedWallet)
}
else -> Action.Unknown
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState
@ -37,6 +38,7 @@ internal interface WalletCardClickIntents {
@Suppress("LongParameterList")
internal class WalletCardClickIntentsImplementor @Inject constructor(
private val stateHolder: WalletStateController,
private val tokenListStore: MultiWalletTokenListStore,
private val walletEventSender: WalletEventSender,
private val walletScreenContentLoader: WalletScreenContentLoader,
private val renameWalletUseCase: RenameWalletUseCase,
@ -99,6 +101,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) {
viewModelScope.launch(dispatchers.main) {
walletScreenContentLoader.cancel(userWalletId)
tokenListStore.remove(userWalletId)
val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch
val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse {
@ -117,6 +120,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor(
reduxStateHolder.onUserWalletSelected(selectedWallet)
} else {
tokenListStore.clear()
stateHolder.clear()
appRouter.replaceAll(AppRoute.Home)
}

View file

@ -8,6 +8,7 @@ import com.tangem.domain.settings.NeverToShowWalletsScrollPreview
import com.tangem.domain.tokens.FetchCardTokenListUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
@ -117,7 +118,7 @@ internal class WalletClickIntents @Inject constructor(
val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) {
fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true)
} else {
fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true)
fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL)
}
maybeFetchResult.onLeft {

View file

@ -13,6 +13,7 @@ import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent
import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType
@ -124,18 +125,20 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main))
analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped)
viewModelScope.launch(dispatchers.main) {
viewModelScope.launch {
val userWallet = getSelectedUserWallet() ?: return@launch
derivePublicKeysUseCase(
userWalletId = userWallet.walletId,
currencies = missedAddressCurrencies,
).fold(
ifLeft = { Timber.e(it, "Failed to derive public keys") },
ifRight = {
fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES).onLeft {
Timber.e("Unable to refresh token list: $it")
}
},
)
.onRight {
// Refresh must be set to true to ensure that yield balances are updated
fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true)
}
.onLeft { Timber.e("Failed to derive public keys: $it") }
}
}