Updated on 2026-08-14
This commit is contained in:
parent
407cfd491f
commit
646349ff69
7 changed files with 359 additions and 11 deletions
|
|
@ -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<SearchQuery>
|
||||
val fullPortfolioBlock: StateFlow<ChooseTokenPortfolioFullBlockUM?>
|
||||
|
||||
/** Currently selected wallet tab. Used to constrain feature blocks (e.g. market block) to the wallet's type. */
|
||||
val selectedWalletFlow: SharedFlow<UserWallet>
|
||||
|
||||
fun onSearchQuery(query: SearchQuery)
|
||||
fun onSearchQuery(query: String) = onSearchQuery(SearchQuery(query))
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SearchQuery> = Channel()
|
||||
override val searchQueryState: StateFlow<SearchQuery> = 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<ChooseTokenPortfolioFullBlockUM?>
|
||||
get() = portfolioFullBlockDelegate.fullPortfolioBlock
|
||||
|
||||
override val selectedWalletFlow: SharedFlow<UserWallet>
|
||||
get() = portfolioFullBlockDelegate.selectedWalletFlow
|
||||
|
||||
init {
|
||||
portfolioListBlockDelegate.onTokenChosen.receiveAsFlow()
|
||||
.onEach { chooseResult -> onCurrencyChosen(chooseResult) }
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ internal class ChooseTokenModel @Inject constructor(
|
|||
screensSourcesName = bridge.analyticsPayload
|
||||
.filterIsInstance<ChooseTokenAnalyticsPayload.ScreensSources>()
|
||||
.firstOrNull()?.value.orEmpty(),
|
||||
selectedWalletFlow = bridge.selectedWalletFlow,
|
||||
shouldShowSingleCurrencyWallets = bridge.settings.isShowSingleCurrencyWallets,
|
||||
)
|
||||
|
||||
val bottomSheetNavigation get() = marketBlockDelegate.addToPortfolioSlot
|
||||
|
|
|
|||
|
|
@ -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<SearchQuery>,
|
||||
@Assisted private val screensSourcesName: String,
|
||||
@Assisted private val selectedWalletFlow: SharedFlow<UserWallet>,
|
||||
@Assisted private val shouldShowSingleCurrencyWallets: Boolean,
|
||||
) {
|
||||
|
||||
private val visibleMarketItemIds = MutableStateFlow<List<CryptoCurrency.RawID>>(emptyList())
|
||||
|
|
@ -52,7 +56,7 @@ internal class MarketBlockDelegate @AssistedInject constructor(
|
|||
analyticsParams = AddToPortfolioManager.AnalyticsParams(source = screensSourcesName),
|
||||
)
|
||||
|
||||
val marketsStateFlow: Flow<SwapMarketState> = searchQueryState
|
||||
private val baseMarketsStateFlow: Flow<SwapMarketState> = 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<SwapMarketState?> = 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<SwapMarketState?> {
|
||||
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<String> =
|
||||
flattenCurrencies().mapTo(hashSetOf()) { it.currency.network.rawId }
|
||||
|
||||
private fun filterStateByNetwork(state: SwapMarketState, allowedNetworkIds: Set<String>): 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<SearchQuery>,
|
||||
modelScope: CoroutineScope,
|
||||
screensSourcesName: String,
|
||||
selectedWalletFlow: SharedFlow<UserWallet>,
|
||||
shouldShowSingleCurrencyWallets: Boolean,
|
||||
): MarketBlockDelegate
|
||||
}
|
||||
}
|
||||
|
|
@ -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<SearchQuery>,
|
||||
@Assisted private val featureSettings: ChooseTokenBridge.Settings,
|
||||
) {
|
||||
|
||||
private val isSearchingState: Boolean get() = searchQueryState.isSearchingState
|
||||
private val isOnlyMultiCurrency: Boolean get() = !featureSettings.isShowSingleCurrencyWallets
|
||||
private val onWalletSelected = Channel<UserWalletId>(capacity = Channel.BUFFERED)
|
||||
|
||||
val selectedWalletFlow: SharedFlow<UserWallet> = 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<SearchQuery>,
|
||||
featureSettings: ChooseTokenBridge.Settings,
|
||||
): PortfolioFullBlockDelegate
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ImmutableList<MarketsListItemUM>>(persistentListOf())
|
||||
|
||||
// Keyed by the raw id value: CryptoCurrency.RawID is a value class, unboxed to String at the JVM boundary.
|
||||
private val tokenMarketsByRawId = mutableMapOf<String, TokenMarket>()
|
||||
|
||||
@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<String>()] }
|
||||
|
||||
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<UserWallet>(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<CryptoCurrencyStatus> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue