Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-17 14:38:33 +04:00
commit dfd7076c2e
31 changed files with 345 additions and 648 deletions

View file

@ -6,6 +6,7 @@ import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import com.tangem.datasource.local.preferences.AppPreferencesStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
@ -20,7 +21,7 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
null
}
}
}
}.distinctUntilChanged()
}
/**
@ -38,7 +39,7 @@ inline fun <reified T> AppPreferencesStore.getObject(key: Preferences.Key<String
} catch (e: JsonDataException) {
default
}
}
}.distinctUntilChanged()
}
/**
@ -100,7 +101,7 @@ suspend inline fun <reified T> AppPreferencesStore.storeObjectList(key: Preferen
/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */
inline fun <reified T> AppPreferencesStore.getObjectList(key: Preferences.Key<String>): Flow<List<T>?> {
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
return data.map { it[key]?.let(adapter::fromJson) }
return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged()
}
/** Get list of data [T] by string [key], or empty if data is not found */

View file

@ -19,10 +19,6 @@
"name": "WC_SOLANA_TX_SIGN_ENABLED",
"version": "undefined"
},
{
"name": "TOKEN_LIST_LCE_ENABLED",
"version": "5.12.0"
},
{
"name": "CARDANO_TOKENS_SUPPORT_ENABLED",
"version": "5.12.0"

View file

@ -32,6 +32,6 @@ val TangemBlockCardColors: CardColors
get() = CardColors(
containerColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledContainerColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
disabledContainerColor = TangemTheme.colors.background.primary,
disabledContentColor = TangemTheme.colors.text.primary1,
)

View file

@ -261,7 +261,7 @@ internal class DefaultCurrenciesRepository(
launch(dispatchers.io) {
combine(
getMultiCurrencyWalletCurrencies(userWallet).distinctUntilChanged(),
getMultiCurrencyWalletCurrencies(userWallet),
isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } },
) { currencies, isFetching ->
send(currencies, isStillLoading = isFetching)
@ -442,21 +442,23 @@ internal class DefaultCurrenciesRepository(
}
private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) {
try {
isMultiCurrencyWalletCurrenciesFetching.update {
it + (userWallet.walletId to true)
}
cacheRegistry.invokeOnExpire(
key = getTokensCacheKey(userWallet.walletId),
skipCache = refresh,
block = {
isMultiCurrencyWalletCurrenciesFetching.update {
it + (userWallet.walletId to true)
}
cacheRegistry.invokeOnExpire(
key = getTokensCacheKey(userWallet.walletId),
skipCache = refresh,
block = { fetchTokens(userWallet) },
)
} finally {
isMultiCurrencyWalletCurrenciesFetching.update {
it - userWallet.walletId
}
}
try {
fetchTokens(userWallet)
} finally {
isMultiCurrencyWalletCurrenciesFetching.update {
it - userWallet.walletId
}
}
},
)
}
private suspend fun fetchTokens(userWallet: UserWallet) {

View file

@ -183,24 +183,22 @@ internal class DefaultNetworksRepository(
networks: Set<Network>,
refresh: Boolean,
) {
try {
isNetworkStatusesFetching.update {
it + (userWalletId to true)
}
val currencies = getCurrencies(userWalletId, networks)
val networksDeferred = networks.mapNotNull { network ->
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
}
val currencies = getCurrencies(userWalletId, networks)
coroutineScope {
networks
.map { network ->
async {
fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh)
}
}
.awaitAll()
}
} finally {
isNetworkStatusesFetching.update {
it - userWalletId
if (networksDeferred.isNotEmpty()) {
try {
isNetworkStatusesFetching.update {
it + (userWalletId to true)
}
networksDeferred.awaitAll()
} finally {
isNetworkStatusesFetching.update {
it - userWalletId
}
}
}
}
@ -226,12 +224,19 @@ internal class DefaultNetworksRepository(
network: Network,
currencies: Sequence<CryptoCurrency>,
refresh: Boolean,
) {
cacheRegistry.invokeOnExpire(
key = getNetworksStatusesCacheKey(userWalletId, network),
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, network, currencies) },
)
): Deferred<Unit>? = coroutineScope {
val key = getNetworksStatusesCacheKey(userWalletId, network)
if (refresh || cacheRegistry.isExpired(key)) {
async {
cacheRegistry.invokeOnExpire(
key = key,
skipCache = refresh,
block = { fetchNetworkStatus(userWalletId, network, currencies) },
)
}
} else {
null
}
}
private suspend fun fetchNetworkStatus(

View file

@ -2,7 +2,7 @@ package com.tangem.domain.balancehiding.error
sealed class HideBalancesError {
object HidingDisabled : HideBalancesError()
data object HidingDisabled : HideBalancesError()
data class DataError(val cause: Throwable) : HideBalancesError()
}

View file

@ -107,4 +107,43 @@ sealed class Lce<out E : Any, out C : Any> {
ifContent = { null },
ifError = ::identity,
)
/**
* Returns `true` if this [Lce] is a [Lce.Loading] state and the given predicate is `true`.
*
* @param predicate The predicate to apply to the partial content.
* By default, the predicate is `true` for any partial content.
* @return `true` if this [Lce] is a [Lce.Loading] state and the given predicate is `true`, `false` otherwise.
*/
fun isLoading(predicate: (maybeContent: C?) -> Boolean = { true }): Boolean = fold(
ifLoading = { predicate(it) },
ifContent = { false },
ifError = { false },
)
/**
* Returns `true` if this [Lce] is a [Lce.Error] state and the given predicate is `true`.
*
* @param predicate The predicate to apply to the error.
* By default, the predicate is `true` for any error.
* @return `true` if this [Lce] is a [Lce.Error] state and the given predicate is `true`, `false` otherwise.
*/
fun isError(predicate: (error: E) -> Boolean = { true }): Boolean = fold(
ifLoading = { false },
ifContent = { false },
ifError = { predicate(it) },
)
/**
* Returns `true` if this [Lce] is a [Lce.Content] state and the given predicate is `true`.
*
* @param predicate The predicate to apply to the content.
* By default, the predicate is `true` for any content.
* @return `true` if this [Lce] is a [Lce.Content] state and the given predicate is `true`, `false` otherwise.
*/
fun isContent(predicate: (content: C) -> Boolean = { true }): Boolean = fold(
ifLoading = { false },
ifContent = { predicate(it) },
ifError = { false },
)
}

View file

@ -46,7 +46,7 @@ class LceRaise<E : Any> @PublishedApi internal constructor(
* */
@RaiseDSL
@OptIn(ExperimentalTypeInference::class)
inline fun <OtherError : Any, C : Any> withError(
inline fun <OtherError : Any, C> withError(
transform: (OtherError) -> E,
@BuilderInference block: LceRaise<OtherError>.() -> C,
): C = recover(

View file

@ -1,8 +1,6 @@
package com.tangem.domain.tokens
import arrow.core.left
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.core.utils.lceError
import com.tangem.domain.core.utils.lceLoading
import com.tangem.domain.core.utils.toLce
@ -12,7 +10,6 @@ import com.tangem.domain.tokens.error.mapper.mapToTokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.operations.CurrenciesStatusesLceOperations
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.TokenListOperations
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
@ -31,29 +28,7 @@ class GetTokenListUseCase(
) {
@OptIn(ExperimentalCoroutinesApi::class)
fun launch(userWalletId: UserWalletId): EitherFlow<TokenListError, TokenList> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
stakingRepository = stakingRepository,
)
return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens ->
maybeTokens.fold(
ifLeft = { error ->
emit(error.mapToTokenListError().left())
},
ifRight = { tokens ->
emitAll(createTokenList(userWalletId, tokens))
},
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun launchLce(userWalletId: UserWalletId): LceFlow<TokenListError, TokenList> {
fun launch(userWalletId: UserWalletId): LceFlow<TokenListError, TokenList> {
val operations = CurrenciesStatusesLceOperations(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
@ -78,21 +53,6 @@ class GetTokenListUseCase(
}
}
private fun createTokenList(
userWalletId: UserWalletId,
tokens: List<CryptoCurrencyStatus>,
): EitherFlow<TokenListError, TokenList> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = tokens,
currenciesRepository = currenciesRepository,
)
return operations.getTokenListFlow().map { maybeTokenList ->
maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError)
}
}
private fun createTokenListLce(
userWalletId: UserWalletId,
currencies: List<CryptoCurrencyStatus>,

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens
import arrow.atomic.update
import arrow.core.raise.ensureNotNull
import arrow.core.toNonEmptyListOrNull
import com.tangem.domain.core.lce.Lce
@ -16,9 +17,10 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.transform
import kotlinx.coroutines.flow.transformLatest
class GetWalletTotalBalanceUseCase(
private val currenciesRepository: CurrenciesRepository,
@ -28,9 +30,9 @@ class GetWalletTotalBalanceUseCase(
) {
suspend operator fun invoke(
userWallestIds: Collection<UserWalletId>,
userTallestIds: Collection<UserWalletId>,
): LceFlow<TokenListError, Map<UserWalletId, TotalFiatBalance>> {
val flows = userWallestIds.distinct()
val flows = userTallestIds.distinct()
.map { userWalletId ->
invoke(userWalletId).map { maybeBalance ->
userWalletId to maybeBalance
@ -39,17 +41,23 @@ class GetWalletTotalBalanceUseCase(
return combine(flows) { balances ->
lce {
balances.associate { (userWalletId, maybeBalance) ->
userWalletId to maybeBalance.bind()
balances.fold(mutableMapOf()) { acc, (userWalletId, maybeBalance) ->
val balance = maybeBalance.bindOrNull() ?: TotalFiatBalance.Loading
isLoading.update { it || balance is TotalFiatBalance.Loading }
acc[userWalletId] = balance
acc
}
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
suspend operator fun invoke(userWalletId: UserWalletId): LceFlow<TokenListError, TotalFiatBalance> {
val currenciesStatuses = getStatuses(userWalletId)
return currenciesStatuses.transform { maybeStatuses ->
return currenciesStatuses.transformLatest { maybeStatuses ->
val balance = createBalance(maybeStatuses)
emit(balance)

View file

@ -33,7 +33,7 @@ internal class CurrenciesStatusesLceOperations(
return transformToCurrenciesStatuses(
userWalletId = userWalletId,
flow = if (isSingleCurrencyWalletsAllowed) {
getWalletCurrenies(userWalletId)
getWalletCurrencies(userWalletId)
} else {
getMultiCurrencyWalletCurrencies(userWalletId)
},
@ -105,7 +105,7 @@ internal class CurrenciesStatusesLceOperations(
return statuses
}
private fun getWalletCurrenies(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrency>> {
private fun getWalletCurrencies(userWalletId: UserWalletId): LceFlow<TokenListError, List<CryptoCurrency>> {
return currenciesRepository.getWalletCurrenciesUpdates(userWalletId)
.map { maybeCurrencies ->
maybeCurrencies.mapError { TokenListError.DataError(it) }

View file

@ -24,52 +24,6 @@ internal class CurrenciesStatusesOperations(
private val userWalletId: UserWalletId,
) {
@OptIn(ExperimentalCoroutinesApi::class)
fun getCurrenciesStatusesFlow(): EitherFlow<Error, List<CryptoCurrencyStatus>> {
return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies ->
val nonEmptyCurrencies = maybeCurrencies.fold(
ifLeft = { error ->
emit(error.left())
return@transformLatest
},
ifRight = List<CryptoCurrency>::toNonEmptyListOrNull,
)
if (nonEmptyCurrencies == null) {
val emptyCurrenciesStatuses = emptyList<CryptoCurrencyStatus>()
emit(emptyCurrenciesStatuses.right())
return@transformLatest
}
val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
maybeYieldBalances = null,
)
emit(maybeLoadingCurrenciesStatuses)
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networks),
getYieldBalances(),
) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances ->
createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeQuotes = maybeQuotes,
maybeNetworkStatuses = maybeNetworksStatuses,
maybeYieldBalances = maybeYieldBalances,
)
}
emitAll(currenciesFlow)
}
}
suspend fun getCurrenciesStatusesSync(): Either<Error, List<CryptoCurrencyStatus>> {
return either {
catch(
@ -360,13 +314,6 @@ internal class CurrenciesStatusesOperations(
return currencyStatusOperations.createTokenStatus()
}
private fun getMultiCurrencyWalletCurrencies(): Flow<Either<Error, List<CryptoCurrency>>> {
return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId)
.map<List<CryptoCurrency>, Either<Error, List<CryptoCurrency>>> { it.right() }
.catch { emit(Error.DataError(it).left()) }
.onEmpty { emit(Error.EmptyCurrencies.left()) }
}
private suspend fun Raise<Error>.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency {
return Either.catch {
currenciesRepository.getMultiCurrencyWalletCurrency(

View file

@ -1,322 +0,0 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.core.error.DataError
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.mock.MockNetworks
import com.tangem.domain.tokens.mock.MockQuotes
import com.tangem.domain.tokens.mock.MockTokenLists
import com.tangem.domain.tokens.mock.MockTokens
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.repository.MockCurrenciesRepository
import com.tangem.domain.tokens.repository.MockNetworksRepository
import com.tangem.domain.tokens.repository.MockQuotesRepository
import com.tangem.domain.tokens.repository.MockStakingRepository
import com.tangem.domain.wallets.models.UserWalletId
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import org.junit.Ignore
import org.junit.Test
internal class GetTokenListUseCaseTest {
private val userWalletId = UserWalletId(value = null)
@Ignore
@Test
fun `when list ungrouped and unsorted then correct token list should be returned`() = runTest {
// Given
val expectedResult = listOf(
MockTokenLists.loadingUngroupedTokenList.right(),
MockTokenLists.failedUngroupedTokenList.right(),
)
val useCase = getUseCase(
isGrouped = flowOf(false.right()),
isSortedByBalance = flowOf(false.right()),
)
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when tokens getting failed then error should be received`() = runTest {
// Given
val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes getting failed then token list without quotes should be received`() = runTest {
// Given
val expectedResult = listOf(
MockTokenLists.loadingUngroupedTokenList.right(),
MockTokenLists.noQuotesUngroupedTokenList.right(),
)
val useCase = getUseCase(
quotes = flowOf(DataError.NetworkError.NoInternetConnection.left()),
statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
)
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when grouping type getting failed then error should be received`() = runTest {
// Given
val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when sorting type getting failed then error should be received`() = runTest {
// Given
val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left()
val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left()))
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Ignore
@Test
fun `when tokens getting failed on second emit then error should be received`() = runTest {
// Given
val error = DataError.NetworkError.NoInternetConnection.left()
val expectedResult = listOf(
MockTokenLists.loadingUngroupedTokenList.right(),
MockTokenLists.failedUngroupedTokenList.right(),
TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left(),
)
val useCase = getUseCase(
tokens = flowOf(
MockTokens.tokens.right(),
error,
).map { delay(timeMillis = 1_000); it },
)
// When
val result = useCase.launch(userWalletId)
.take(count = 3)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Ignore
@Test
fun `when list grouped then correct token list should be received`() = runTest {
val expectedResult = listOf(
MockTokenLists.loadingGroupedTokenList.right(),
MockTokenLists.failedGroupedTokenList.right(),
)
val useCase = getUseCase(isGrouped = flowOf(true.right()))
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when list is sorted and ungrouped then correct token list should be received`() = runTest {
val expectedResult = listOf(
MockTokenLists.loadingUngroupedTokenList.copy(sortedBy = TokenList.SortType.BALANCE).right(),
MockTokenLists.sortedUngroupedTokenList.right(),
)
val useCase = getUseCase(
statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
isGrouped = flowOf(false.right()),
isSortedByBalance = flowOf(true.right()),
)
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when list is sorted and grouped then correct token list should be received`() = runTest {
val expectedResult = listOf(
MockTokenLists.loadingGroupedTokenList.copy(sortedBy = TokenList.SortType.BALANCE).right(),
MockTokenLists.sortedGroupedTokenList.right(),
)
val useCase = getUseCase(
statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
isGrouped = flowOf(true.right()),
isSortedByBalance = flowOf(true.right()),
)
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when tokens is empty then not initialized token list should be received`() = runTest {
val expectedResult = MockTokenLists.emptyTokenList.right()
val useCase = getUseCase(tokens = flowOf(emptyList<CryptoCurrency>().right()))
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when tokens flow is empty then error should be received`() = runTest {
val expectedResult = TokenListError.EmptyTokens.left()
val useCase = getUseCase(tokens = flowOf())
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when networks statuses flow is empty then error should be received`() = runTest {
val expectedResult = listOf(
MockTokenLists.loadingUngroupedTokenList.right(),
TokenListError.EmptyTokens.left(),
)
val useCase = getUseCase(statuses = flowOf())
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when networks statuses is empty then loading token list should be received`() = runTest {
val expectedResult = MockTokenLists.loadingUngroupedTokenList.right()
val useCase = getUseCase(statuses = flowOf(emptySet<NetworkStatus>().right()))
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes flow is empty then list without quotes should be received`() = runTest {
val expectedResult = listOf(
MockTokenLists.loadingUngroupedTokenList.right(),
MockTokenLists.noQuotesUngroupedTokenList.right(),
)
val useCase = getUseCase(
statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
quotes = flowOf(emptySet<Quote>().right()),
)
// When
val result = useCase.launch(userWalletId)
.take(count = 2)
.toList()
// Then
assertEquals(expectedResult, result)
}
@Test
fun `when quotes is empty and statuses verified then loading token list should be received`() = runTest {
val expectedResult = MockTokenLists.loadingUngroupedTokenList.right()
val useCase = getUseCase(
statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()),
quotes = flowOf(emptySet<Quote>().right()),
)
// When
val result = useCase.launch(userWalletId).first()
// Then
assertEquals(expectedResult, result)
}
private fun getUseCase(
tokens: Flow<Either<DataError, List<CryptoCurrency>>> = flowOf(MockTokens.tokens.right()),
quotes: Flow<Either<DataError, Set<Quote>>> = flowOf(MockQuotes.quotes.right()),
statuses: Flow<Either<DataError, Set<NetworkStatus>>> = flowOf(MockNetworks.errorNetworksStatuses.right()),
isGrouped: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isGrouped.right()),
isSortedByBalance: Flow<Either<DataError, Boolean>> = flowOf(MockTokenLists.isSortedByBalance.right()),
) = GetTokenListUseCase(
currenciesRepository = MockCurrenciesRepository(
sortTokensResult = Unit.right(),
removeCurrencyResult = Unit.right(),
token = MockTokens.token1.right(),
tokens = tokens,
isGrouped = isGrouped,
isSortedByBalance = isSortedByBalance,
),
quotesRepository = MockQuotesRepository(quotes),
networksRepository = MockNetworksRepository(statuses),
stakingRepository = MockStakingRepository(),
)
}

View file

@ -37,11 +37,15 @@ dependencies {
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.walletConnect)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.legacy)
/* SDK */
// TODO: For TangemError model, should be removed after card domain scanning refactoring
implementation(deps.tangem.card.core)
// For image resolving
implementation(deps.tangem.blockchain)
/* AndroidX */
implementation(deps.androidx.fragment.ktx)
@ -55,6 +59,7 @@ dependencies {
implementation(deps.compose.foundation)
implementation(deps.compose.material3)
implementation(deps.compose.shimmer)
implementation(deps.compose.coil)
/* DI */
implementation(deps.hilt.android)

View file

@ -19,23 +19,26 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
userWallets = persistentListOf(
UserWalletListUM.UserWalletUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = "My Wallet",
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
imageResId = R.drawable.ill_card_wallet_2_211_343,
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = "Old wallet",
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
imageResId = R.drawable.ill_card_note_eth_211_343,
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletListUM.UserWalletUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = "Multi Card",
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
imageResId = R.drawable.ill_card_note_bnb_211_343,
imageUrl = "",
isEnabled = false,
onClick = {},
),
),

View file

@ -1,10 +1,11 @@
package com.tangem.features.details.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class UserWalletListUM(
val userWallets: ImmutableList<UserWalletUM>,
val isWalletSavingInProgress: Boolean,
@ -12,12 +13,13 @@ internal data class UserWalletListUM(
val onAddNewWalletClick: () -> Unit,
) {
@Immutable
data class UserWalletUM(
val id: UserWalletId,
val name: String,
val name: TextReference,
val information: TextReference,
@DrawableRes
val imageResId: Int,
val imageUrl: String,
val isEnabled: Boolean,
val onClick: () -> Unit,
)
}

View file

@ -10,15 +10,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.ui.coil.RotationTransformation
@Composable
internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) {
@ -46,6 +52,7 @@ private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modif
BlockCard(
modifier = modifier,
onClick = model.onClick,
enabled = model.isEnabled,
) {
Row(
modifier = Modifier
@ -55,39 +62,81 @@ private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modif
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Image(
modifier = Modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36),
painter = painterResource(id = model.imageResId),
contentScale = ContentScale.FillBounds,
contentDescription = null,
Image(imageUrl = model.imageUrl)
NameAndInfo(
name = model.name,
information = model.information,
)
Column(
modifier = Modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = model.name,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = model.information.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.SpaceEvenly,
) {
Text(
text = name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Text(
text = information,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
private fun Image(imageUrl: String, modifier: Modifier = Modifier) {
val imageModifier = modifier
.width(TangemTheme.dimens.size24)
.height(TangemTheme.dimens.size36)
.clip(TangemTheme.shapes.roundedCornersSmall)
SubcomposeAsyncImage(
modifier = imageModifier,
model = ImageRequest.Builder(LocalContext.current)
.transformations(RotationTransformation(angle = 90f))
.size(
width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() },
height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() },
)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = {
RectangleShimmer(
modifier = imageModifier,
radius = TangemTheme.dimens.size2,
)
},
error = {
Image(
modifier = imageModifier,
painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36),
contentDescription = null,
)
},
contentDescription = null,
)
}
@Composable
private fun AddWalletButton(
text: TextReference,
@ -110,6 +159,7 @@ private fun AddWalletButton(
AnimatedContent(
modifier = Modifier.size(TangemTheme.dimens.size24),
targetState = isInProgress,
label = "Add wallet progress",
) { isInProgress ->
if (isInProgress) {
CircularProgressIndicator(

View file

@ -0,0 +1,22 @@
package com.tangem.features.details.ui.coil
import android.graphics.Bitmap
import android.graphics.Matrix
import coil.size.Size
import coil.transform.Transformation
internal class RotationTransformation(private val angle: Float) : Transformation {
override val cacheKey: String = "rotate:$angle"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
val matrix = Matrix().apply {
val centerX = input.width / 2f
val centerY = input.height / 2f
postRotate(angle, centerX, centerY)
}
return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true)
}
}

View file

@ -1,9 +1,6 @@
package com.tangem.features.details.utils
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.CardDTO
@ -12,6 +9,7 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.utils.Strings.STARS
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -19,11 +17,15 @@ internal fun List<UserWallet>.toUiModels(
onClick: (UserWalletId) -> Unit,
appCurrency: AppCurrency? = null,
balances: Map<UserWalletId, TotalFiatBalance> = emptyMap(),
isLoading: Boolean = true,
isBalancesHidden: Boolean = false,
): ImmutableList<UserWalletUM> = this.map { model ->
val balance = balances[model.walletId]
model.mapToUiModel(
balance = balance,
appCurrency = appCurrency,
isLoading = isLoading,
isBalanceHidden = isBalancesHidden,
onClick = { onClick(model.walletId) },
)
}.toImmutableList()
@ -31,22 +33,52 @@ internal fun List<UserWallet>.toUiModels(
private fun UserWallet.mapToUiModel(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
isLoading: Boolean,
isBalanceHidden: Boolean,
onClick: () -> Unit,
): UserWalletUM = UserWalletUM(
id = walletId,
name = name,
information = getInfo(appCurrency, balance),
imageResId = resolveImage(),
name = stringReference(name),
information = getInfo(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
isLoading = isLoading,
),
imageUrl = artworkUrl,
isEnabled = !isLocked,
onClick = onClick,
)
private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBalance?): TextReference {
private fun UserWallet.getInfo(
appCurrency: AppCurrency?,
balance: TotalFiatBalance?,
isBalanceHidden: Boolean,
isLoading: Boolean,
): TextReference {
val dividerRef = stringReference(value = "")
val cardCount = getCardCount()
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return when {
isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked))
isLoading -> cardCountRef
isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS))
else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef)
}
}
private fun getBalanceInfo(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
cardCountRef: TextReference,
dividerRef: TextReference,
): TextReference {
val amount = when (balance) {
is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized }
is TotalFiatBalance.Failed,
@ -56,16 +88,15 @@ private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBala
}
return if (amount != null && appCurrency != null) {
val divider = stringReference(value = "")
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val amountRef = stringReference(formattedAmount)
TextReference.Combined(wrappedList(cardCountRef, divider, amountRef))
combinedReference(cardCountRef, dividerRef, amountRef)
} else {
cardCountRef
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
}
}
@ -75,10 +106,4 @@ private fun UserWallet.getCardCount() = when (val status = scanResponse.card.bac
is CardDTO.BackupStatus.NoBackup,
null,
-> 1
}
@DrawableRes
private fun UserWallet.resolveImage(): Int {
// TODO: Implement image resolving [REDACTED_JIRA]
return R.drawable.ill_card_wallet_2_211_343
}

View file

@ -10,6 +10,8 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.getOrElse
@ -24,10 +26,7 @@ import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ComponentScoped
@ -35,6 +34,7 @@ internal class UserWalletsFetcher @Inject constructor(
getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val router: Router,
private val messageSender: UiMessageSender,
) {
@ -44,10 +44,16 @@ internal class UserWalletsFetcher @Inject constructor(
emit(wallets.toUiModels(onClick = ::navigateToWalletSettings))
combine(
getSelectedAppCurrencyUseCase(),
getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)),
) { maybeAppCurrency, maybeBalances ->
val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse(
getSelectedAppCurrencyUseCase().distinctUntilChanged(),
getBalanceHidingSettingsUseCase().distinctUntilChanged(),
getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(),
) { maybeAppCurrency, balanceHidingSettings, maybeBalances ->
val models = createUiModels(
wallets = wallets,
maybeAppCurrency = maybeAppCurrency,
maybeBalances = maybeBalances,
balanceHidingSettings = balanceHidingSettings,
).getOrElse(
ifLoading = { return@combine },
ifError = {
val message = resourceReference(R.string.common_unknown_error)
@ -65,10 +71,11 @@ internal class UserWalletsFetcher @Inject constructor(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
balanceHidingSettings: BalanceHidingSettings,
): Lce<Error, ImmutableList<UserWalletUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
block = { maybeBalances.bind() },
block = { maybeBalances.bindOrNull().orEmpty() },
)
val appCurrency = withError(
transform = { Error.UnableToGetAppCurrency },
@ -79,6 +86,8 @@ internal class UserWalletsFetcher @Inject constructor(
appCurrency = appCurrency,
balances = balances,
onClick = ::navigateToWalletSettings,
isBalancesHidden = balanceHidingSettings.isBalanceHidden,
isLoading = maybeBalances.isLoading(),
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="36dp"
android:viewportWidth="22"
android:viewportHeight="36">
<path
android:fillColor="#C9C9CA"
android:pathData="M22,2L22,34A2,2 0,0 1,20 36L2,36A2,2 0,0 1,0 34L0,2A2,2 0,0 1,2 0L20,0A2,2 0,0 1,22 2z" />
<path
android:fillColor="#A1A1A1"
android:pathData="M10.92,22.14H10.96C11.52,22.14 11.85,21.83 11.85,21.35C11.85,20.84 11.48,20.55 10.95,20.55H10.91C10.36,20.55 10.03,20.89 10.03,21.32C10.03,21.79 10.36,22.14 10.92,22.14ZM9.29,19.89V20.58C9,20.63 8.83,20.85 8.83,21.3C8.83,21.82 9.09,22.11 9.62,22.11H10.05C9.77,21.95 9.51,21.59 9.51,21.16C9.51,20.42 10.06,19.86 10.9,19.86H10.94C11.76,19.86 12.38,20.42 12.38,21.17C12.38,21.65 12.16,21.95 11.87,22.11H12.32V22.79H9.61C8.75,22.78 8.32,22.21 8.32,21.3C8.32,20.39 8.73,19.97 9.29,19.89ZM11.17,7.82V9.23H9.41C9.06,9.23 8.88,9.23 8.75,9.16C8.63,9.1 8.54,9 8.48,8.89C8.41,8.75 8.41,8.58 8.41,8.23V7.82H11.17ZM13.69,8.23V6C13.69,5.65 13.69,5.48 13.62,5.34C13.56,5.22 13.47,5.13 13.35,5.07C13.21,5 13.04,5 12.69,5H12.31V9.23H12.69H12.69C13.04,9.23 13.21,9.23 13.35,9.16C13.47,9.1 13.56,9 13.62,8.89C13.69,8.75 13.69,8.58 13.69,8.23ZM11.17,5V6.41H8.41V6C8.41,5.65 8.41,5.48 8.48,5.34C8.54,5.22 8.63,5.13 8.75,5.07C8.88,5 9.06,5 9.41,5L11.17,5ZM10.21,11.96H11.81V11.57H12.32V11.96H12.96L12.96,12.64H12.32V13.28H11.81V12.64H10.27C10.01,12.64 9.89,12.76 9.89,12.98C9.89,13.11 9.91,13.21 9.95,13.31H9.41C9.37,13.2 9.34,13.05 9.34,12.85C9.34,12.27 9.65,11.96 10.21,11.96ZM12.32,17.44V16.76H9.39V17.44H11.11C11.58,17.44 11.81,17.74 11.81,18.12C11.81,18.53 11.61,18.71 11.17,18.71H9.39L9.39,19.38H11.23C12.04,19.38 12.38,18.97 12.38,18.38C12.38,17.9 12.14,17.58 11.85,17.44H12.32ZM11.88,24.74C11.88,25.16 11.66,25.42 11.16,25.45V23.99C11.61,24.06 11.88,24.33 11.88,24.74ZM10.87,23.29H10.82C9.9,23.29 9.33,23.91 9.33,24.77C9.33,25.52 9.67,26.02 10.29,26.11V25.46C10,25.41 9.84,25.19 9.84,24.79C9.84,24.28 10.15,24 10.7,23.98V26.12H10.9C11.95,26.12 12.38,25.47 12.38,24.74C12.38,23.91 11.77,23.29 10.87,23.29ZM12.32,26.64V27.32H11.87C12.14,27.46 12.38,27.78 12.38,28.21C12.38,28.59 12.22,28.89 11.85,29.04C12.22,29.26 12.38,29.66 12.38,30.03C12.38,30.56 12.05,31 11.24,31H9.39V30.32H11.2C11.63,30.32 11.81,30.14 11.81,29.8C11.81,29.47 11.59,29.16 11.14,29.16H9.39V28.48H11.2C11.63,28.48 11.81,28.29 11.81,27.96C11.81,27.63 11.59,27.32 11.14,27.32H9.39V26.64H12.32ZM10.7,15.52H10.41C10.04,15.52 9.82,15.22 9.82,14.8C9.82,14.47 9.98,14.33 10.23,14.33C10.59,14.33 10.7,14.66 10.7,15.18V15.52ZM11.13,15.16C11.13,14.32 10.88,13.66 10.2,13.66C9.59,13.66 9.33,14.1 9.33,14.64C9.33,15.09 9.5,15.34 9.75,15.53H9.39V16.2H11.31C12.11,16.2 12.38,15.68 12.38,15.03C12.38,14.38 12.09,13.83 11.41,13.78V14.43C11.7,14.47 11.87,14.64 11.87,14.99C11.87,15.39 11.67,15.52 11.28,15.52H11.13V15.16Z" />
</vector>

View file

@ -1,20 +0,0 @@
package com.tangem.feature.wallet.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object FeatureTogglesModule {
@Provides
@Singleton
fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletFeatureToggles {
return WalletFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,11 +0,0 @@
package com.tangem.feature.wallet.featuretoggle
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
internal class WalletFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isTokenListLceFlowEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("TOKEN_LIST_LCE_ENABLED")
}

View file

@ -13,9 +13,7 @@ import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase
import com.tangem.domain.tokens.ToggleTokenListSortingUseCase
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles
import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState
import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState
@ -42,7 +40,6 @@ internal class OrganizeTokensViewModel @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val walletFeatureToggles: WalletFeatureToggles,
private val dispatchers: CoroutineDispatcherProvider,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents {
@ -173,34 +170,21 @@ internal class OrganizeTokensViewModel @Inject constructor(
}
private suspend fun getTokenList(): TokenList? {
return if (walletFeatureToggles.isTokenListLceFlowEnabled) {
val tokenList = getTokenListUseCase.launchLce(userWalletId)
.transform { maybeTokenList ->
val tokenList = maybeTokenList.getOrElse(
ifLoading = { return@transform },
ifError = { error ->
stateHolder.updateStateWithError(error)
val tokenList = getTokenListUseCase.launch(userWalletId)
.transform { maybeTokenList ->
val tokenList = maybeTokenList.getOrElse(
ifLoading = { return@transform },
ifError = { error ->
stateHolder.updateStateWithError(error)
return@transform
},
)
return@transform
},
)
emit(tokenList)
}
tokenList.firstOrNull()
} else {
val maybeTokenList = getTokenListUseCase.launch(userWalletId)
.first { maybeTokenList ->
maybeTokenList.getOrNull()?.totalFiatBalance !is TotalFiatBalance.Loading
}
maybeTokenList.getOrElse { error ->
stateHolder.updateStateWithError(error)
null
emit(tokenList)
}
}
return tokenList.firstOrNull()
}
private fun bootstrapDragAndDropUpdates() {

View file

@ -1,8 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.promo.PromoBanner
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
@ -23,7 +23,6 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import kotlin.collections.count
@ -47,11 +46,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) }
return combine(
flow = getTokenListUseCase.launch(userWallet.walletId).conflate(),
flow2 = isReadyToShowRateAppUseCase().conflate(),
flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(),
flow4 = shouldShowSwapPromoWalletUseCase().conflate(),
flow5 = promoFlow.conflate(),
flow = getTokenListUseCase.launch(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = shouldShowSwapPromoWalletUseCase(),
flow5 = promoFlow,
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
readyForRateAppNotification = true
@ -113,7 +112,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addInformationalNotifications(
cardTypesResolver: CardTypesResolver,
maybeTokenList: Either<TokenListError, TokenList>,
maybeTokenList: Lce<TokenListError, TokenList>,
clickIntents: WalletClickIntents,
) {
addIf(
@ -125,7 +124,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
}
private fun MutableList<WalletNotification>.addMissingAddressesNotification(
maybeTokenList: Either<TokenListError, TokenList>,
maybeTokenList: Lce<TokenListError, TokenList>,
clickIntents: WalletClickIntents,
) {
val currencies = maybeTokenList.getMissingAddressCurrencies()
@ -141,26 +140,23 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun Either<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
return fold(
ifLeft = { emptyList() },
ifRight = { tokenList ->
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
private fun Lce<TokenListError, TokenList>.getMissingAddressCurrencies(): List<CryptoCurrency> {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return emptyList()
currencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
},
)
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
return currencies
.filter { it.value is CryptoCurrencyStatus.MissedDerivation }
.map(CryptoCurrencyStatus::currency)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver,
tokenList: Either<TokenListError, TokenList>,
tokenList: Lce<TokenListError, TokenList>,
isNeedToBackup: Boolean,
clickIntents: WalletClickIntents,
) {
@ -182,19 +178,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun Either<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
return fold(
ifLeft = { false },
ifRight = { tokenList ->
val currencies = when (tokenList) {
is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies)
is TokenList.Ungrouped -> tokenList.currencies
is TokenList.Empty -> emptyList()
}
private fun Lce<TokenListError, TokenList>.hasUnreachableNetworks(): Boolean {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false
currencies.any { it.value is CryptoCurrencyStatus.Unreachable }
},
)
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 }
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(

View file

@ -5,7 +5,6 @@ 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.featuretoggle.WalletFeatureToggles
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
@ -28,7 +27,6 @@ internal class MultiWalletContentLoader(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val walletFeatureToggles: WalletFeatureToggles,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) : WalletContentLoader(id = userWallet.walletId) {
@ -42,7 +40,6 @@ internal class MultiWalletContentLoader(
walletWithFundsChecker = walletWithFundsChecker,
getTokenListUseCase = getTokenListUseCase,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
walletFeatureToggles = walletFeatureToggles,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
),

View file

@ -5,7 +5,6 @@ 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.featuretoggle.WalletFeatureToggles
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
@ -26,7 +25,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletFeatureToggles: WalletFeatureToggles,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
@ -42,7 +40,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
walletFeatureToggles = walletFeatureToggles,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.ApplyTokenListSortingUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
@ -12,19 +11,16 @@ import com.tangem.domain.tokens.model.CryptoCurrency
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.featuretoggle.WalletFeatureToggles
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
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.flow.map
@Suppress("LongParameterList")
internal class MultiWalletTokenListSubscriber(
private val userWallet: UserWallet,
private val getTokenListUseCase: GetTokenListUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletFeatureToggles: WalletFeatureToggles,
stateHolder: WalletStateController,
clickIntents: WalletClickIntents,
tokenListAnalyticsSender: TokenListAnalyticsSender,
@ -42,11 +38,7 @@ internal class MultiWalletTokenListSubscriber(
) {
override fun tokenListFlow(): LceFlow<TokenListError, TokenList> {
return if (walletFeatureToggles.isTokenListLceFlowEnabled) {
getTokenListUseCase.launchLce(userWallet.walletId)
} else {
getTokenListUseCase.launch(userWallet.walletId).map { it.toLce() }
}
return getTokenListUseCase.launch(userWallet.walletId)
}
override suspend fun onTokenListReceived(maybeTokenList: Lce<TokenListError, TokenList>) {