From 646349ff69d2c5d01172a8c06949ccc50eb1db72 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jun 2026 14:56:20 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../api/choosetoken/ChooseTokenBridge.kt | 10 + .../choosetoken/DefaultChooseTokenBridge.kt | 9 +- .../choosetoken/model/ChooseTokenModel.kt | 2 + .../choosetoken/model/MarketBlockDelegate.kt | 72 ++++- .../model/PortfolioFullBlockDelegate.kt | 15 +- .../model/MarketBlockDelegateTest.kt | 258 ++++++++++++++++++ .../state/utils/WalletLoadingStateFactory.kt | 4 +- 7 files changed, 359 insertions(+), 11 deletions(-) create mode 100644 features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt index daf50812c5..e62479cdbf 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt @@ -11,6 +11,7 @@ import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow interface ChooseTokenBridge : ChooseTokenBridgeInternal { @@ -30,6 +31,11 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal { val isShowMarketBlock: Boolean, val isShowPaymentAccount: Boolean, val isAppBarShown: Boolean = true, + /** + * When `true`, single-currency wallets (and single-currency-with-token wallets like NODL) + * are shown as selectable tabs. Swap flows keep this `false` — only multi-currency wallets apply there. + */ + val isShowSingleCurrencyWallets: Boolean = false, ) { companion object { val SwapFrom = Settings( @@ -47,6 +53,7 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal { isShowMarketBlock = true, isShowPaymentAccount = false, isAppBarShown = false, + isShowSingleCurrencyWallets = true, ) } } @@ -69,6 +76,9 @@ interface ChooseTokenBridgeInternal { val searchQueryState: StateFlow val fullPortfolioBlock: StateFlow + /** Currently selected wallet tab. Used to constrain feature blocks (e.g. market block) to the wallet's type. */ + val selectedWalletFlow: SharedFlow + fun onSearchQuery(query: SearchQuery) fun onSearchQuery(query: String) = onSearchQuery(SearchQuery(query)) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/DefaultChooseTokenBridge.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/DefaultChooseTokenBridge.kt index 3037369099..b2469a3e77 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/DefaultChooseTokenBridge.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/DefaultChooseTokenBridge.kt @@ -2,6 +2,7 @@ package com.tangem.features.commonfeatures.impl.choosetoken import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge @@ -9,9 +10,9 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge.Sett import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM +import com.tangem.features.commonfeatures.impl.choosetoken.model.ChooseTokenModel import com.tangem.features.commonfeatures.impl.choosetoken.model.PortfolioFullBlockDelegate import com.tangem.features.commonfeatures.impl.choosetoken.model.PortfolioListBlockDelegate -import com.tangem.features.commonfeatures.impl.choosetoken.model.ChooseTokenModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -32,7 +33,7 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor( private val onSearchQuery: Channel = Channel() override val searchQueryState: StateFlow = onSearchQuery.receiveAsFlow() - .debounce(ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY) + .debounce(ChooseTokenModel.DEBOUNCE_SEARCH_DELAY) .stateIn(modelScope, SharingStarted.Eagerly, initialValue = SearchQuery.Empty) private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create( @@ -45,6 +46,7 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor( modelScope = modelScope, searchQueryState = searchQueryState, portfolioListBlockDelegate = portfolioListBlockDelegate, + featureSettings = settings, ) override val tokenFilter: MutableStateFlow<(AccountStatus, CryptoCurrencyStatus) -> Boolean> @@ -53,6 +55,9 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor( override val fullPortfolioBlock: StateFlow get() = portfolioFullBlockDelegate.fullPortfolioBlock + override val selectedWalletFlow: SharedFlow + get() = portfolioFullBlockDelegate.selectedWalletFlow + init { portfolioListBlockDelegate.onTokenChosen.receiveAsFlow() .onEach { chooseResult -> onCurrencyChosen(chooseResult) } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt index e4df6cb25f..214422ed0a 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/ChooseTokenModel.kt @@ -42,6 +42,8 @@ internal class ChooseTokenModel @Inject constructor( screensSourcesName = bridge.analyticsPayload .filterIsInstance() .firstOrNull()?.value.orEmpty(), + selectedWalletFlow = bridge.selectedWalletFlow, + shouldShowSingleCurrencyWallets = bridge.settings.isShowSingleCurrencyWallets, ) val bottomSheetNavigation get() = marketBlockDelegate.addToPortfolioSlot diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt index a13fbe77a5..5703c8d4a0 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt @@ -6,7 +6,10 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains +import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketListConfig @@ -14,9 +17,9 @@ import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.commonfeatures.impl.choosetoken.AddToPortfolioRoute import com.tangem.features.commonfeatures.impl.choosetoken.market.MarketsListBatchFlowManager import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState @@ -25,11 +28,9 @@ import com.tangem.utils.Provider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* -import kotlin.collections.filter -import kotlin.collections.map -import kotlin.collections.orEmpty @Suppress("LongParameterList") internal class MarketBlockDelegate @AssistedInject constructor( @@ -37,9 +38,12 @@ internal class MarketBlockDelegate @AssistedInject constructor( private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, @Assisted private val modelScope: CoroutineScope, @Assisted private val searchQueryState: StateFlow, @Assisted private val screensSourcesName: String, + @Assisted private val selectedWalletFlow: SharedFlow, + @Assisted private val shouldShowSingleCurrencyWallets: Boolean, ) { private val visibleMarketItemIds = MutableStateFlow>(emptyList()) @@ -52,7 +56,7 @@ internal class MarketBlockDelegate @AssistedInject constructor( analyticsParams = AddToPortfolioManager.AnalyticsParams(source = screensSourcesName), ) - val marketsStateFlow: Flow = searchQueryState + private val baseMarketsStateFlow: Flow = searchQueryState // Switch between default and search market flows .map { it.value.isEmpty() } .distinctUntilChanged() @@ -66,6 +70,24 @@ internal class MarketBlockDelegate @AssistedInject constructor( } } + /** + * Market block constrained by the currently selected wallet: + * - single-currency wallet: hidden entirely (`null`) - no market tokens can be added; + * - single-currency-with-token wallet (e.g. NODL): items filtered to the wallet's network, + * block hidden when nothing remains; + * - multi-currency wallet: shown as is. + * + * When single-currency wallets aren't selectable here (e.g. swap), the wallet is always + * multi-currency, so we skip the per-wallet logic entirely and return [baseMarketsStateFlow]. + */ + val marketsStateFlow: Flow = if (!shouldShowSingleCurrencyWallets) { + baseMarketsStateFlow + } else { + selectedWalletFlow + .flatMapLatest(::marketsFlowForWallet) + .distinctUntilChanged() + } + private val defaultMarketsListManager by lazy { marketsListBatchFlowManagerFactory.create( batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, @@ -181,6 +203,44 @@ internal class MarketBlockDelegate @AssistedInject constructor( } } + private fun marketsFlowForWallet(wallet: UserWallet): Flow { + if (wallet !is UserWallet.Cold) return baseMarketsStateFlow + val resolver = wallet.scanResponse.cardTypesResolver + return when { + // Single-currency wallet can't hold market tokens - hide the whole block. + resolver.isSingleWallet() -> flowOf(null) + // Single-currency-with-token wallet (NODL) - keep only tokens available on the wallet's network(s). + resolver.isSingleWalletWithToken() -> combine( + baseMarketsStateFlow, + singleAccountStatusListSupplier(wallet.walletId), + ) { state, accountStatusList -> + filterStateByNetwork(state, accountStatusList.allowedNetworkIds()) + } + // Multi-currency wallet - the common case, no filtering needed. + else -> baseMarketsStateFlow + } + } + + private fun AccountStatusList.allowedNetworkIds(): Set = + flattenCurrencies().mapTo(hashSetOf()) { it.currency.network.rawId } + + private fun filterStateByNetwork(state: SwapMarketState, allowedNetworkIds: Set): SwapMarketState? { + if (state !is SwapMarketState.Content) return state + if (allowedNetworkIds.isEmpty()) return null + + val filteredItems = state.items.filter { item -> + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + tokenMarket?.networks?.any { allowedNetworkIds.contains(it.networkId) } == true + }.toImmutableList() + + return if (filteredItems.isEmpty()) { + null + } else { + state.copy(items = filteredItems, total = filteredItems.size) + } + } + private fun addToPortfolioItem(item: MarketsListItemUM) { val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) ?: searchMarketsListManager.getTokenMarketById(item.id) ?: return @@ -218,6 +278,8 @@ internal class MarketBlockDelegate @AssistedInject constructor( searchQueryState: StateFlow, modelScope: CoroutineScope, screensSourcesName: String, + selectedWalletFlow: SharedFlow, + shouldShowSingleCurrencyWallets: Boolean, ): MarketBlockDelegate } } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt index 7748f84f93..1b7b276b63 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/PortfolioFullBlockDelegate.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery.Companion.isSearchingState import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM @@ -34,9 +35,11 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( @Assisted private val modelScope: CoroutineScope, @Assisted private val portfolioListBlockDelegate: PortfolioListBlockDelegate, @Assisted private val searchQueryState: StateFlow, + @Assisted private val featureSettings: ChooseTokenBridge.Settings, ) { private val isSearchingState: Boolean get() = searchQueryState.isSearchingState + private val isOnlyMultiCurrency: Boolean get() = !featureSettings.isShowSingleCurrencyWallets private val onWalletSelected = Channel(capacity = Channel.BUFFERED) val selectedWalletFlow: SharedFlow = onWalletSelected.receiveAsFlow() @@ -50,9 +53,11 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( init { val globalSelectedWallet = selectedWalletUseCase.sync().getOrNull() - val allWallets = getWalletsUseCase.invokeSync().filter { it.isMultiCurrency } + val allWallets = getWalletsUseCase.invokeSync() + .filter { !isOnlyMultiCurrency || it.isMultiCurrency } val firstSelectedWallet = when { - globalSelectedWallet?.isMultiCurrency == true -> globalSelectedWallet + globalSelectedWallet != null && (!isOnlyMultiCurrency || globalSelectedWallet.isMultiCurrency) -> + globalSelectedWallet allWallets.isNotEmpty() -> allWallets.first() else -> null } @@ -60,7 +65,10 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( } private fun buildFlow() = flow { - val walletsFlow = getWalletsUseCase.invokeAsMap(filterLocked = true) + val walletsFlow = getWalletsUseCase.invokeAsMap( + isOnlyMultiCurrency = isOnlyMultiCurrency, + filterLocked = true, + ) val fullPortfolioBlockFlow = combine( flow = walletsFlow, flow2 = portfolioListBlockDelegate.portfolioList, @@ -107,6 +115,7 @@ internal class PortfolioFullBlockDelegate @AssistedInject constructor( modelScope: CoroutineScope, portfolioListBlockDelegate: PortfolioListBlockDelegate, searchQueryState: StateFlow, + featureSettings: ChooseTokenBridge.Settings, ): PortfolioFullBlockDelegate } } \ No newline at end of file diff --git a/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt new file mode 100644 index 0000000000..338914683e --- /dev/null +++ b/features/common-features/impl/src/test/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegateTest.kt @@ -0,0 +1,258 @@ +package com.tangem.features.commonfeatures.impl.choosetoken.model + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.card.WalletData +import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.card.configs.GenericCardConfig +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridgeInternal.SearchQuery +import com.tangem.features.commonfeatures.impl.choosetoken.market.MarketsListBatchFlowManager +import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState +import com.tangem.test.core.getEmittedValues +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class MarketBlockDelegateTest { + + private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory = mockk() + private val excludedBlockchains: ExcludedBlockchains = mockk(relaxed = true) + private val getUserWalletsUseCase: GetWalletsUseCase = mockk(relaxed = true) + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory = mockk() + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk() + + private val defaultManager: MarketsListBatchFlowManager = mockk(relaxed = true) + private val searchManager: MarketsListBatchFlowManager = mockk(relaxed = true) + + private val searchQueryState = MutableStateFlow(SearchQuery.Empty) + private val defaultUiItems = MutableStateFlow>(persistentListOf()) + + // Keyed by the raw id value: CryptoCurrency.RawID is a value class, unboxed to String at the JVM boundary. + private val tokenMarketsByRawId = mutableMapOf() + + @BeforeEach + fun setup() { + clearMocks( + marketsListBatchFlowManagerFactory, + addToPortfolioManagerFactory, + singleAccountStatusListSupplier, + defaultManager, + searchManager, + ) + searchQueryState.value = SearchQuery.Empty + defaultUiItems.value = persistentListOf() + tokenMarketsByRawId.clear() + + every { + marketsListBatchFlowManagerFactory.create( + GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + any(), + any(), + any() + ) + } returns defaultManager + every { + marketsListBatchFlowManagerFactory.create( + GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + any(), + any(), + any() + ) + } returns searchManager + every { addToPortfolioManagerFactory.create(any(), any(), any()) } returns mockk(relaxed = true) + + every { defaultManager.uiItems } returns defaultUiItems + every { defaultManager.isInInitialLoadingErrorState } returns MutableStateFlow(false) + every { defaultManager.totalCount } returns MutableStateFlow(null) + every { defaultManager.getTokenMarketById(any()) } answers { tokenMarketsByRawId[firstArg()] } + + every { searchManager.uiItems } returns MutableStateFlow(persistentListOf()) + every { searchManager.isInInitialLoadingErrorState } returns MutableStateFlow(false) + every { searchManager.isSearchNotFoundState } returns MutableStateFlow(false) + every { searchManager.totalCount } returns MutableStateFlow(null) + every { searchManager.getTokenMarketById(any()) } returns null + } + + @Test + fun `GIVEN multi-currency wallet WHEN trending emitted THEN all items shown unchanged`() = runTest { + // Arrange + val item1 = marketItem("token-1") + val item2 = marketItem("token-2") + defaultUiItems.value = persistentListOf(item1, item2) + val delegate = createDelegate(wallet = MockUserWalletFactory.create()) + + // Act + val result = lastMarketState(delegate) + + // Assert + assertThat(result).isInstanceOf(SwapMarketState.Content::class.java) + assertThat((result as SwapMarketState.Content).items).containsExactly(item1, item2).inOrder() + } + + @Test + fun `GIVEN single-currency wallet WHEN trending emitted THEN market block is hidden`() = runTest { + // Arrange + defaultUiItems.value = persistentListOf(marketItem("token-1")) + val delegate = createDelegate(wallet = createSingleCurrencyWallet()) + + // Act + val result = lastMarketState(delegate) + + // Assert + assertThat(result).isNull() + } + + @Test + fun `GIVEN single-currency wallets not shown WHEN trending emitted THEN base state returned without filtering`() = + runTest { + // Arrange + val item1 = marketItem("token-1") + defaultUiItems.value = persistentListOf(item1) + // Single-currency wallet would normally hide the block, but the setting short-circuits the per-wallet logic. + val delegate = createDelegate(wallet = createSingleCurrencyWallet(), showSingleCurrencyWallets = false) + + // Act + val result = lastMarketState(delegate) + + // Assert + assertThat(result).isInstanceOf(SwapMarketState.Content::class.java) + assertThat((result as SwapMarketState.Content).items).containsExactly(item1) + } + + @Test + fun `GIVEN NODL wallet WHEN trending emitted THEN only items on wallet network are shown`() = runTest { + // Arrange + val nodlWallet = MockUserWalletFactory.createSingleWalletWithToken() + val itemOnWalletNetwork = marketItem("token-stellar") + val itemOnOtherNetwork = marketItem("token-eth") + tokenMarketsByRawId["token-stellar"] = tokenMarket(STELLAR_NETWORK_ID) + tokenMarketsByRawId["token-eth"] = tokenMarket(ETHEREUM_NETWORK_ID) + defaultUiItems.value = persistentListOf(itemOnWalletNetwork, itemOnOtherNetwork) + + every { + singleAccountStatusListSupplier(nodlWallet.walletId) + } returns flowOf(accountStatusList(STELLAR_NETWORK_ID)) + + // Act + val result = lastMarketState(createDelegate(wallet = nodlWallet)) + + // Assert + assertThat(result).isInstanceOf(SwapMarketState.Content::class.java) + assertThat((result as SwapMarketState.Content).items).containsExactly(itemOnWalletNetwork) + assertThat(result.total).isEqualTo(1) + } + + @Test + fun `GIVEN NODL wallet WHEN no trending tokens on wallet network THEN market block is hidden`() = runTest { + // Arrange + val nodlWallet = MockUserWalletFactory.createSingleWalletWithToken() + val itemOnOtherNetwork = marketItem("token-eth") + tokenMarketsByRawId["token-eth"] = tokenMarket(ETHEREUM_NETWORK_ID) + defaultUiItems.value = persistentListOf(itemOnOtherNetwork) + + every { + singleAccountStatusListSupplier(nodlWallet.walletId) + } returns flowOf(accountStatusList(STELLAR_NETWORK_ID)) + + // Act + val result = lastMarketState(createDelegate(wallet = nodlWallet)) + + // Assert + assertThat(result).isNull() + } + + // region Helpers + + private fun TestScope.lastMarketState(delegate: MarketBlockDelegate): SwapMarketState? { + val emittedValues = getEmittedValues(delegate.marketsStateFlow) + advanceUntilIdle() + return emittedValues.last() + } + + private fun TestScope.createDelegate( + wallet: UserWallet, + showSingleCurrencyWallets: Boolean = true, + ): MarketBlockDelegate { + val selectedWalletFlow = MutableSharedFlow(replay = 1) + selectedWalletFlow.tryEmit(wallet) + return MarketBlockDelegate( + marketsListBatchFlowManagerFactory = marketsListBatchFlowManagerFactory, + excludedBlockchains = excludedBlockchains, + getUserWalletsUseCase = getUserWalletsUseCase, + addToPortfolioManagerFactory = addToPortfolioManagerFactory, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, + modelScope = backgroundScope, + searchQueryState = searchQueryState, + screensSourcesName = "test", + selectedWalletFlow = selectedWalletFlow, + shouldShowSingleCurrencyWallets = showSingleCurrencyWallets, + ) + } + + private fun marketItem(id: String): MarketsListItemUM = mockk { + every { this@mockk.id } returns CryptoCurrency.RawID(id) + } + + private fun tokenMarket(vararg networkIds: String): TokenMarket = mockk { + every { networks } returns networkIds.map { networkId -> + TokenMarket.Network(networkId = networkId, contractAddress = null, decimalCount = null) + } + } + + private fun accountStatusList(vararg networkIds: String): AccountStatusList = mockk { + every { flattenCurrencies() } returns networkIds.map { networkId -> + mockk { + every { currency.network.rawId } returns networkId + } + } + } + + private fun createSingleCurrencyWallet(): UserWallet.Cold = UserWallet.Cold( + name = "Single", + walletId = UserWalletId("022"), + cardsInWallet = emptySet(), + isMultiCurrency = false, + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(maxWalletCount = 2), + derivedKeys = emptyMap(), + ).copy( + productType = ProductType.Note, + walletData = WalletData(blockchain = "XLM", token = null), + ), + hasBackupError = false, + ) + + // endregion + + private companion object { + const val STELLAR_NETWORK_ID = "stellar" + const val ETHEREUM_NETWORK_ID = "ethereum" + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 9736912d08..51f9bfe3ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -185,6 +185,8 @@ internal class WalletLoadingStateFactory( } private fun createWalletActions(userWallet: UserWallet): PersistentList { + val isSingleWalletWithToken = + userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() return buildList { add( WalletActionButtons.AddFunds( @@ -193,7 +195,7 @@ internal class WalletLoadingStateFactory( ).buttonUM, ) addIf( - condition = !userWallet.isSingleWallet(), + condition = !userWallet.isSingleWallet() && !isSingleWalletWithToken, element = WalletActionButtons.Swap( isEnabled = false, onClick = {