Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-08 21:10:03 +03:00
parent a730af0f1f
commit 98915e2507
21 changed files with 448 additions and 45 deletions

View file

@ -47,6 +47,17 @@ internal object TokensDomainModule {
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideGetCardTokensListUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideRemoveCurrencyUseCase(

View file

@ -26,6 +26,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
@Suppress("LargeClass")
internal class DefaultCurrenciesRepository(
private val tangemTechApi: TangemTechApi,
private val userTokensStore: UserTokensStore,
@ -152,6 +153,29 @@ internal class DefaultCurrenciesRepository(
}
}
override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
}
}
override suspend fun getSingleCurrencyWalletWithCardCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency {
return withContext(dispatchers.io) {
val userWallet = getUserWallet(userWalletId)
ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false)
val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse)
.find { it.id == id }
requireNotNull(currency) { "Unable to find currency with provided ID: $id" }
}
}
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return channelFlow {
val userWallet = getUserWallet(userWalletId)

View file

@ -58,4 +58,28 @@ internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) {
return primaryToken ?: coin
}
fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List<CryptoCurrency> {
val cardDerivationStyleProvider = scanResponse.derivationStyleProvider
val resolver = scanResponse.cardTypesResolver
val blockchain = resolver.getBlockchain()
val coin = cryptoCurrencyFactory.createCoin(
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
)
requireNotNull(coin) { "Coin for the single currency card cannot be null" }
val primaryToken = resolver.getPrimaryToken()?.let { token ->
cryptoCurrencyFactory.createToken(
sdkToken = token,
blockchain = blockchain,
extraDerivationPath = null,
derivationStyleProvider = cardDerivationStyleProvider,
)
}
return listOfNotNull(coin, primaryToken)
}
}

View file

@ -0,0 +1,70 @@
package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.left
import com.tangem.domain.tokens.error.TokenListError
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.CurrenciesStatusesOperations
import com.tangem.domain.tokens.operations.TokenListOperations
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 com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
class GetCardTokensListUseCase(
internal val currenciesRepository: CurrenciesRepository,
internal val quotesRepository: QuotesRepository,
internal val networksRepository: NetworksRepository,
internal val dispatchers: CoroutineDispatcherProvider,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(userWalletId: UserWalletId): Flow<Either<TokenListError, TokenList>> {
return getTokensStatuses(userWalletId).transformLatest { maybeTokens ->
maybeTokens.fold(
ifLeft = { error ->
emit(error.left())
},
ifRight = { tokens ->
emitAll(createTokenList(userWalletId, tokens))
},
)
}
}
private fun getTokensStatuses(
userWalletId: UserWalletId,
): Flow<Either<TokenListError, List<CryptoCurrencyStatus>>> {
val operations = CurrenciesStatusesOperations(
userWalletId = userWalletId,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
)
return operations.getCardCurrenciesStatusesFlow()
.map { maybeCurrenciesStatuses ->
maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError)
}
}
private fun createTokenList(
userWalletId: UserWalletId,
tokens: List<CryptoCurrencyStatus>,
): Flow<Either<TokenListError, TokenList>> {
val operations = TokenListOperations(
userWalletId = userWalletId,
tokens = tokens,
currenciesRepository = currenciesRepository,
)
return operations.getTokenListForSingleCurrencyFlow().map { maybeTokenList ->
maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError)
}
}
}

View file

@ -34,15 +34,24 @@ class GetCurrencyStatusUpdatesUseCase(
* @param userWalletId The unique identifier of the user's wallet.
* @param currencyId The unique identifier of the cryptocurrency.
* @param derivationPath currency derivation path.
* @param isSingleWalletWithTokens Indicates whether the user wallet contains only one token on card (old cards)
* @return A [Flow] emitting either a [CurrencyStatusError] or a [CryptoCurrencyStatus], indicating the result of the fetch operation.
*/
operator fun invoke(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(getCurrency(userWalletId, currencyId, derivationPath))
emitAll(
getCurrency(
userWalletId,
currencyId,
derivationPath,
isSingleWalletWithTokens,
),
)
}.flowOn(dispatchers.io)
}
@ -50,6 +59,7 @@ class GetCurrencyStatusUpdatesUseCase(
userWalletId: UserWalletId,
currencyId: CryptoCurrency.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
@ -58,7 +68,12 @@ class GetCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
)
return operations.getCurrencyStatusFlow(currencyId, derivationPath).map { maybeCurrency ->
val currencyFlow = if (isSingleWalletWithTokens) {
operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId)
} else {
operations.getCurrencyStatusFlow(currencyId, derivationPath)
}
return currencyFlow.map { maybeCurrency ->
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
}
}

View file

@ -25,6 +25,7 @@ class GetCurrencyWarningsUseCase(
userWalletId: UserWalletId,
currency: CryptoCurrency,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<Set<CryptoCurrencyWarning>> {
return combine(
getFeeWarningFlow(
@ -32,6 +33,7 @@ class GetCurrencyWarningsUseCase(
networkId = currency.network.id,
currencyId = currency.id,
derivationPath = derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens,
),
flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)),
flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)),
@ -54,6 +56,7 @@ class GetCurrencyWarningsUseCase(
networkId: Network.ID,
currencyId: CryptoCurrency.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<CryptoCurrencyWarning?> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
@ -62,9 +65,19 @@ class GetCurrencyWarningsUseCase(
userWalletId = userWalletId,
)
val currencyFlow = if (isSingleWalletWithTokens) {
operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId)
} else {
operations.getCurrencyStatusFlow(currencyId, derivationPath)
}
val networkFlow = if (isSingleWalletWithTokens) {
operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId)
} else {
operations.getNetworkCoinFlow(networkId, derivationPath)
}
return combine(
operations.getCurrencyStatusFlow(currencyId, derivationPath).map { it.getOrNull() },
operations.getNetworkCoinFlow(networkId, derivationPath).map { it.getOrNull() },
currencyFlow.map { it.getOrNull() },
networkFlow.map { it.getOrNull() },
) { tokenStatus, coinStatus ->
when {
tokenStatus != null && coinStatus != null -> {

View file

@ -24,6 +24,7 @@ class GetNetworkCoinStatusUseCase(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
return flow {
emitAll(
@ -31,6 +32,7 @@ class GetNetworkCoinStatusUseCase(
userWalletId = userWalletId,
networkId = networkId,
derivationPath = derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens,
),
)
}
@ -41,6 +43,7 @@ class GetNetworkCoinStatusUseCase(
userWalletId: UserWalletId,
networkId: Network.ID,
derivationPath: Network.DerivationPath,
isSingleWalletWithTokens: Boolean,
): Flow<Either<CurrencyStatusError, CryptoCurrencyStatus>> {
val operations = CurrenciesStatusesOperations(
currenciesRepository = currenciesRepository,
@ -48,8 +51,12 @@ class GetNetworkCoinStatusUseCase(
networksRepository = networksRepository,
userWalletId = userWalletId,
)
return operations.getNetworkCoinFlow(networkId, derivationPath).map { maybeCurrency ->
val networkFlow = if (isSingleWalletWithTokens) {
operations.getNetworkCoinForSingleWalletWithTokenFlow(networkId)
} else {
operations.getNetworkCoinFlow(networkId, derivationPath)
}
return networkFlow.map { maybeCurrency ->
maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError)
}
}

View file

@ -67,6 +67,44 @@ internal class CurrenciesStatusesOperations(
}
}
fun getCardCurrenciesStatusesFlow(): Flow<Either<Error, List<CryptoCurrencyStatus>>> {
return flow {
val nonEmptyCurrencies = recover(
block = { getCurrenciesFromCard(userWalletId) },
recover = {
emit(it.left())
return@flow
},
).toNonEmptyListOrNull()
if (nonEmptyCurrencies == null) {
val emptyCurrenciesStatuses = emptyList<CryptoCurrencyStatus>()
emit(emptyCurrenciesStatuses.right())
return@flow
}
val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses(
currencies = nonEmptyCurrencies,
maybeNetworkStatuses = null,
maybeQuotes = null,
)
emit(maybeLoadingCurrenciesStatuses)
val (networks, currenciesIds) = getIds(nonEmptyCurrencies)
val currenciesFlow = combine(
getQuotes(currenciesIds),
getNetworksStatuses(networks),
) { maybeQuotes, maybeNetworksStatuses ->
createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses)
}
emitAll(currenciesFlow)
}
}
suspend fun getCurrencyStatusFlow(
currencyId: CryptoCurrency.ID,
derivationPath: Network.DerivationPath,
@ -79,6 +117,17 @@ internal class CurrenciesStatusesOperations(
return getCurrencyStatusFlow(currency)
}
suspend fun getCurrencyStatusSingleWalletWithTokensFlow(
currencyId: CryptoCurrency.ID,
): Flow<Either<Error, CryptoCurrencyStatus>> {
val currency = recover(
block = { getSingleCurrencyWalletWithCardTokensCurrency(currencyId) },
recover = { return flowOf(it.left()) },
)
return getCurrencyStatusFlow(currency)
}
suspend fun getNetworkCoinFlow(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
@ -91,6 +140,18 @@ internal class CurrenciesStatusesOperations(
return getCurrencyStatusFlow(currency)
}
suspend fun getNetworkCoinForSingleWalletWithTokenFlow(
networkId: Network.ID,
): Flow<Either<Error,
CryptoCurrencyStatus,>,> {
val currency = recover(
block = { getNetworkCoinForSingleWalletWithToken(networkId) },
recover = { return flowOf(it.left()) },
)
return getCurrencyStatusFlow(currency)
}
suspend fun getPrimaryCurrencyStatusFlow(): Flow<Either<Error, CryptoCurrencyStatus>> {
val currency = recover(
block = { getPrimaryCurrency() },
@ -199,6 +260,14 @@ internal class CurrenciesStatusesOperations(
.bind()
}
private suspend fun Raise<Error>.getSingleCurrencyWalletWithCardTokensCurrency(
currencyId: CryptoCurrency.ID,
): CryptoCurrency {
return Either.catch { currenciesRepository.getSingleCurrencyWalletWithCardCurrency(userWalletId, currencyId) }
.mapLeft { Error.DataError(it) }
.bind()
}
private suspend fun Raise<Error>.getNetworkCoin(
networkId: Network.ID,
derivationPath: Network.DerivationPath,
@ -208,6 +277,16 @@ internal class CurrenciesStatusesOperations(
.bind()
}
private suspend fun Raise<Error>.getNetworkCoinForSingleWalletWithToken(networkId: Network.ID): CryptoCurrency {
return Either.catch {
currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId)
.find { it.network.id == networkId && it is CryptoCurrency.Coin }
?: raise(Error.DataError(IllegalStateException("Coin with network $networkId not found for this card")))
}
.mapLeft { Error.DataError(it) }
.bind()
}
private suspend fun Raise<Error>.getPrimaryCurrency(): CryptoCurrency {
return catch(
block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) },
@ -215,6 +294,12 @@ internal class CurrenciesStatusesOperations(
)
}
private suspend fun Raise<Error>.getCurrenciesFromCard(userWalletId: UserWalletId): List<CryptoCurrency> {
return catch({ currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) }) {
raise(Error.DataError(it))
}
}
private fun getQuotes(tokensIds: NonEmptySet<CryptoCurrency.ID>): Flow<Either<Error, Set<Quote>>> {
return quotesRepository.getQuotesUpdates(tokensIds)
.map<Set<Quote>, Either<Error, Set<Quote>>> { quotes ->

View file

@ -39,6 +39,16 @@ internal class TokenListOperations(
}
}
fun getTokenListForSingleCurrencyFlow(): Flow<Either<Error, TokenList>> {
return flow {
emit(
either {
createTokenList()
},
)
}
}
private fun Raise<Error>.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList {
val nonEmptyCurrencies = tokens.toNonEmptyListOrNull()
?: return TokenList.Empty
@ -55,6 +65,22 @@ internal class TokenListOperations(
)
}
private fun Raise<Error>.createTokenList(): TokenList {
val nonEmptyCurrencies = tokens.toNonEmptyListOrNull()
?: return TokenList.Empty
val isAnyTokenLoading = nonEmptyCurrencies.any { it.value is CryptoCurrencyStatus.Loading }
val fiatBalanceOperations = TokenListFiatBalanceOperations(nonEmptyCurrencies, isAnyTokenLoading)
return createTokenList(
currencies = nonEmptyCurrencies,
fiatBalance = fiatBalanceOperations.calculateFiatBalance(),
isAnyTokenLoading = isAnyTokenLoading,
isGrouped = false,
isSortedByBalance = false,
)
}
private fun Raise<Error>.createTokenList(
currencies: NonEmptyList<CryptoCurrencyStatus>,
fiatBalance: TokenList.FiatBalance,

View file

@ -68,6 +68,31 @@ interface CurrenciesRepository {
*/
suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency
/**
* Retrieves the cryptocurrencies for a specific single-currency user wallet with tokens on the card.
*
* @param userWalletId The unique identifier of the user wallet.
* @return The primary cryptocurrency associated with the user wallet.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If multi-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
/**
* Retrieves the cryptocurrency for a specific single-currency user old wallet
* that stores token on card
*
* @param userWalletId The unique identifier of the user wallet.
* @param id The unique identifier of the cryptocurrency to be retrieved.
* @return The cryptocurrency associated with the user wallet and ID.
* @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet
* ID provided.
*/
suspend fun getSingleCurrencyWalletWithCardCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency
/**
* Retrieves updates of the list of cryptocurrencies within a multi-currency wallet.
*

View file

@ -60,6 +60,17 @@ internal class MockCurrenciesRepository(
return token.getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
return tokens.first().getOrElse { e -> throw e }
}
override suspend fun getSingleCurrencyWalletWithCardCurrency(
userWalletId: UserWalletId,
id: CryptoCurrency.ID,
): CryptoCurrency {
return token.getOrElse { e -> throw e }
}
override fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow<List<CryptoCurrency>> {
return tokens.map { it.getOrElse { e -> throw e } }
}

View file

@ -24,6 +24,7 @@ import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
@ -133,10 +134,12 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun updateWarnings() {
viewModelScope.launch(dispatchers.io) {
val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }
getCurrencyWarningsUseCase.invoke(
userWalletId = userWalletId,
currency = cryptoCurrency,
derivationPath = cryptoCurrency.network.derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens(wallet),
)
.distinctUntilChanged()
.onEach { uiState = stateFactory.getStateWithNotifications(it) }
@ -145,22 +148,30 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun updateMarketPrice() {
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
derivationPath = cryptoCurrency.network.derivationPath,
)
.distinctUntilChanged()
.onEach { either ->
uiState = stateFactory.getCurrencyLoadedBalanceState(either)
either.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(userWalletId = userWalletId, currencyStatus = status)
viewModelScope.launch(dispatchers.io) {
val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }
getCurrencyStatusUpdatesUseCase(
userWalletId = userWalletId,
currencyId = cryptoCurrency.id,
derivationPath = cryptoCurrency.network.derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens(wallet),
)
.distinctUntilChanged()
.onEach { either ->
uiState = stateFactory.getCurrencyLoadedBalanceState(either)
either.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(userWalletId = userWalletId, currencyStatus = status)
}
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(marketPriceJobHolder)
}
}
private fun isSingleWalletWithTokens(userWallet: UserWallet): Boolean {
return userWallet.scanResponse.walletData?.token != null && !userWallet.isMultiCurrency
}
/**
@ -251,10 +262,12 @@ internal class TokenDetailsViewModel @Inject constructor(
private fun sendToken(status: CryptoCurrencyStatus) {
viewModelScope.launch(dispatchers.io) {
val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch }
val maybeCoinStatus = getNetworkCoinStatusUseCase(
userWalletId = userWalletId,
networkId = status.currency.network.id,
derivationPath = status.currency.network.derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens(wallet),
).firstOrNull()
maybeCoinStatus?.onRight { coinStatus ->

View file

@ -27,6 +27,7 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() {
override val tokensListState: WalletTokensListState,
override val event: StateEvent<WalletEvent> = consumedEvent(),
override val isBalanceHidden: Boolean,
val isManageTokensAvailable: Boolean = true,
val tokenActionsBottomSheet: ActionsBottomSheetConfig?,
val onManageTokensClick: () -> Unit,
) : WalletMultiCurrencyState()

View file

@ -0,0 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state.factory
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.UserWallet
data class TokenListWithWallet(
val tokenList: TokenList,
val wallet: UserWallet,
)

View file

@ -31,7 +31,7 @@ internal class WalletLoadedTokensListConverter(
appCurrencyProvider: Provider<AppCurrency>,
currentWalletProvider: Provider<UserWallet>,
clickIntents: WalletClickIntents,
) : Converter<Either<TokenListError, TokenList>, WalletState> {
) : Converter<Either<TokenListError, TokenListWithWallet>, WalletState> {
private val tokenListStateConverter = TokenListToWalletStateConverter(
currentStateProvider = currentStateProvider,
@ -40,7 +40,7 @@ internal class WalletLoadedTokensListConverter(
clickIntents = clickIntents,
)
override fun convert(value: Either<TokenListError, TokenList>): WalletState {
override fun convert(value: Either<TokenListError, TokenListWithWallet>): WalletState {
return value.fold(
ifLeft = tokenListErrorConverter::convert,
ifRight = tokenListStateConverter::convert,

View file

@ -38,7 +38,9 @@ internal class WalletSkeletonStateConverter(
override fun convert(value: SkeletonModel): WalletState.ContentState {
val selectedWallet = value.wallets[value.selectedWalletIndex]
return if (selectedWallet.isMultiCurrency) {
val isSingleWalletWithToken = !selectedWallet.isMultiCurrency &&
selectedWallet.scanResponse.walletData?.token != null
return if (selectedWallet.isMultiCurrency || isSingleWalletWithToken) {
createMultiCurrencyState(value = value)
} else {
createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName())

View file

@ -13,7 +13,6 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
@ -160,8 +159,8 @@ internal class WalletStateFactory(
)
}
fun getStateByTokensList(maybeTokenList: Either<TokenListError, TokenList>): WalletState {
return loadedTokensListConverter.convert(maybeTokenList)
fun getStateByTokensList(maybeTokenListWithWallet: Either<TokenListError, TokenListWithWallet>): WalletState {
return loadedTokensListConverter.convert(maybeTokenListWithWallet)
}
fun getStateByTokenListError(error: TokenListError): WalletState {

View file

@ -217,7 +217,7 @@ private fun BaseScaffold(
topBar = { WalletTopBar(config = state.topBarConfig) },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
floatingActionButton = {
if (state is WalletMultiCurrencyState.Content) {
if (state is WalletMultiCurrencyState.Content && state.isManageTokensAvailable) {
ManageTokensButton(onManageTokensClick = state.onManageTokensClick)
}
},

View file

@ -9,6 +9,7 @@ import com.tangem.domain.tokens.model.TokenList
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState
import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.PersistentList
@ -18,23 +19,25 @@ import kotlinx.collections.immutable.persistentListOf
internal class TokenListToContentItemsConverter(
appCurrencyProvider: Provider<AppCurrency>,
private val clickIntents: WalletClickIntents,
) : Converter<TokenList, WalletTokensListState> {
) : Converter<TokenListWithWallet, WalletTokensListState> {
private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter(
appCurrencyProvider = appCurrencyProvider,
clickIntents = clickIntents,
)
override fun convert(value: TokenList): WalletTokensListState {
return when (value) {
override fun convert(value: TokenListWithWallet): WalletTokensListState {
val isSingleCurrencyWalletWithToken = !value.wallet.isMultiCurrency &&
value.wallet.scanResponse.walletData?.token != null
return when (val tokenList = value.tokenList) {
is TokenList.Empty -> WalletTokensListState.Empty
is TokenList.GroupedByNetwork -> WalletTokensListState.Content(
items = value.mapToMultiCurrencyItems(),
organizeTokensButton = value.mapToOrganizeTokensButtonState(),
items = tokenList.mapToMultiCurrencyItems(),
organizeTokensButton = tokenList.mapToOrganizeTokensButtonState(isSingleCurrencyWalletWithToken),
)
is TokenList.Ungrouped -> WalletTokensListState.Content(
items = value.mapToMultiCurrencyItems(),
organizeTokensButton = value.mapToOrganizeTokensButtonState(),
items = tokenList.mapToMultiCurrencyItems(),
organizeTokensButton = tokenList.mapToOrganizeTokensButtonState(isSingleCurrencyWalletWithToken),
)
}
}
@ -51,17 +54,23 @@ internal class TokenListToContentItemsConverter(
}
}
private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState {
private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(
isSingleCurrencyWithTokenWallet: Boolean,
): OrganizeTokensButtonState {
return getOrganizeTokensButtonState(
isLoading = totalFiatBalance is TokenList.FiatBalance.Loading,
currenciesSize = groups.flatMap(NetworkGroup::currencies).size,
isSingleCurrencyWithTokenWallet = isSingleCurrencyWithTokenWallet,
)
}
private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState {
private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(
isSingleCurrencyWithTokenWallet: Boolean,
): OrganizeTokensButtonState {
return getOrganizeTokensButtonState(
isLoading = totalFiatBalance is TokenList.FiatBalance.Loading,
currenciesSize = currencies.size,
isSingleCurrencyWithTokenWallet = isSingleCurrencyWithTokenWallet,
)
}
@ -88,8 +97,12 @@ internal class TokenListToContentItemsConverter(
return this
}
private fun getOrganizeTokensButtonState(isLoading: Boolean, currenciesSize: Int): OrganizeTokensButtonState {
return if (currenciesSize > 1) {
private fun getOrganizeTokensButtonState(
isLoading: Boolean,
currenciesSize: Int,
isSingleCurrencyWithTokenWallet: Boolean,
): OrganizeTokensButtonState {
return if (currenciesSize > 1 && !isSingleCurrencyWithTokenWallet) {
OrganizeTokensButtonState.Visible(
isEnabled = !isLoading,
onClick = clickIntents::onOrganizeTokensClick,

View file

@ -8,6 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencySt
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toPersistentList
@ -18,19 +19,23 @@ internal class TokenListToWalletStateConverter(
private val currentWalletProvider: Provider<UserWallet>,
private val appCurrencyProvider: Provider<AppCurrency>,
clickIntents: WalletClickIntents,
) : Converter<TokenList, WalletState> {
) : Converter<TokenListWithWallet, WalletState> {
private val tokenListToContentConverter = TokenListToContentItemsConverter(
appCurrencyProvider = appCurrencyProvider,
clickIntents = clickIntents,
)
override fun convert(value: TokenList): WalletState {
override fun convert(value: TokenListWithWallet): WalletState {
val tokenList = value.tokenList
val isSingleCurrencyWalletWithToken = !value.wallet.isMultiCurrency &&
value.wallet.scanResponse.walletData?.token != null
return when (val state = currentStateProvider()) {
is WalletMultiCurrencyState.Content -> {
state.copy(
walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance),
walletsListConfig = state.updateSelectedWallet(fiatBalance = tokenList.totalFiatBalance),
tokensListState = tokenListToContentConverter.convert(value = value),
isManageTokensAvailable = !isSingleCurrencyWalletWithToken,
)
}
is WalletMultiCurrencyState.Locked,

View file

@ -62,6 +62,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.feature.wallet.presentation.wallet.state.*
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.factory.TokenListWithWallet
import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -92,6 +93,7 @@ internal class WalletViewModel @Inject constructor(
private val updateWalletUseCase: UpdateWalletUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val getTokenListUseCase: GetTokenListUseCase,
private val getCardTokensListUseCase: GetCardTokensListUseCase,
private val fetchTokenListUseCase: FetchTokenListUseCase,
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
@ -592,10 +594,14 @@ internal class WalletViewModel @Inject constructor(
viewModelScope.launch(dispatchers.io) {
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
val isSingleWalletWithTokens = !userWallet.isMultiCurrency &&
userWallet.scanResponse.walletData?.token != null
getNetworkCoinStatusUseCase(
userWalletId = userWallet.walletId,
networkId = cryptoCurrencyStatus.currency.network.id,
derivationPath = cryptoCurrencyStatus.currency.network.derivationPath,
isSingleWalletWithTokens = isSingleWalletWithTokens,
)
.take(count = 1)
.collectLatest {
@ -916,6 +922,9 @@ internal class WalletViewModel @Inject constructor(
uiState = stateFactory.getLockedState()
}
wallet.isMultiCurrency -> getMultiCurrencyContent(wallet, index)
isSingleWalletWithTokens(wallet) -> {
getSingleCurrencyWithTokenContent(index)
}
!wallet.isMultiCurrency -> getSingleCurrencyContent(index)
}
}
@ -933,7 +942,7 @@ internal class WalletViewModel @Inject constructor(
tokenListFlow
.distinctUntilChanged()
.onEach { maybeTokenList ->
uiState = stateFactory.getStateByTokensList(maybeTokenList)
uiState = stateFactory.getStateByTokensList(maybeTokenList.getTokenListWithWallet(wallet))
maybeTokenList.onRight { checkMultiWalletWithFunds(it) }
@ -978,6 +987,10 @@ internal class WalletViewModel @Inject constructor(
.saveIn(updateWcJobHolder)
}
private fun isSingleWalletWithTokens(userWallet: UserWallet): Boolean {
return userWallet.scanResponse.walletData?.token != null && !userWallet.isMultiCurrency
}
private fun List<CryptoCurrencyStatus>.isAllCurrenciesLoaded(): Boolean {
return !this.any { it.value is CryptoCurrencyStatus.Loading }
}
@ -1008,6 +1021,14 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun Either<TokenListError, TokenList>.getTokenListWithWallet(
userWallet: UserWallet,
): Either<TokenListError, TokenListWithWallet> {
return this.map {
TokenListWithWallet(it, userWallet)
}
}
private fun getSingleCurrencyContent(index: Int) {
val wallet = getWallet(index)
getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId)
@ -1032,6 +1053,30 @@ internal class WalletViewModel @Inject constructor(
.saveIn(marketPriceJobHolder)
}
private fun getSingleCurrencyWithTokenContent(walletIndex: Int) {
val state = requireNotNull(uiState as? WalletMultiCurrencyState) {
"Impossible to get a token list updates if state isn't WalletMultiCurrencyState"
}
val wallet = getWallet(walletIndex)
getCardTokensListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id)
.distinctUntilChanged()
.onEach { maybeTokenList ->
uiState = stateFactory.getStateByTokensList(maybeTokenList.getTokenListWithWallet(wallet))
maybeTokenList.onRight { checkMultiWalletWithFunds(it) }
updateNotifications(
index = walletIndex,
tokenList = maybeTokenList.fold(ifLeft = { null }, ifRight = { it }),
)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
.saveIn(tokensJobHolder)
}
private fun updateTxHistory(userWalletId: UserWalletId, currency: CryptoCurrency, refresh: Boolean) {
viewModelScope.launch(dispatchers.io) {
val txHistoryItemsCountEither = txHistoryItemsCountUseCase(
@ -1095,10 +1140,15 @@ internal class WalletViewModel @Inject constructor(
val wallet = getWallet(walletIndex)
viewModelScope.launch(dispatchers.io) {
val result = fetchTokenListUseCase(wallet.walletId, refresh = true)
if (isSingleWalletWithTokens(wallet)) {
// TODO add refresh for nodl cards ([REDACTED_JIRA])
delay(timeMillis = 1000)
} else {
val result = fetchTokenListUseCase(wallet.walletId, refresh = true)
uiState = result.fold(stateFactory::getStateByTokenListError) { uiState }
}
uiState = stateFactory.getRefreshedState()
uiState = result.fold(stateFactory::getStateByTokenListError) { uiState }
}.saveIn(refreshContentJobHolder)
}