Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-10 12:32:14 +05:00
parent 95ca24f18d
commit 73a6042277
5 changed files with 96 additions and 209 deletions

View file

@ -6,9 +6,9 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase
import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.domain.account.usecase.*
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.feature.referral.data.ExternalReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -94,13 +94,9 @@ internal object AccountDomainModule {
@Provides
@Singleton
fun provideIsAccountsModeEnabledUseCase(
userWalletsListRepository: UserWalletsListRepository,
accountsCRUDRepository: AccountsCRUDRepository,
multiAccountListSupplier: MultiAccountListSupplier,
): IsAccountsModeEnabledUseCase {
return IsAccountsModeEnabledUseCase(
userWalletsListRepository = userWalletsListRepository,
crudRepository = accountsCRUDRepository,
)
return IsAccountsModeEnabledUseCase(multiAccountListSupplier = multiAccountListSupplier)
}
@Provides

View file

@ -4,6 +4,7 @@ import arrow.core.Option
import arrow.core.some
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.producer.MultiAccountListProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.loadAndGet
import com.tangem.domain.core.flow.FlowProducerTools
@ -22,7 +23,7 @@ import kotlinx.coroutines.flow.*
* @property params params
* @property flowProducerTools tools for producing flows
* @property userWalletsListRepository repository for getting user wallets
* @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet
* @property singleAccountListSupplier supplier for getting [AccountList] per wallet
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
@ -31,7 +32,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
@Assisted val params: Unit,
override val flowProducerTools: FlowProducerTools,
private val userWalletsListRepository: UserWalletsListRepository,
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val dispatchers: CoroutineDispatcherProvider,
) : MultiAccountListProducer {
@ -44,7 +45,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor(
.distinctUntilChanged()
.flatMapLatest { ids ->
combine(
flows = ids.map(walletAccountListFlowFactory::create),
flows = ids.map(singleAccountListSupplier::invoke),
transform = ::listOf,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.account.producer
import com.google.common.truth.Truth
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.core.flow.FlowProducerTools
import com.tangem.domain.models.TokensSortType
@ -28,14 +29,14 @@ import org.junit.jupiter.api.TestInstance
class DefaultMultiAccountListProducerTest {
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk()
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
private val flowProducerTools: FlowProducerTools = mockk()
private val producer = DefaultMultiAccountListProducer(
params = Unit,
flowProducerTools = flowProducerTools,
userWalletsListRepository = userWalletsListRepository,
walletAccountListFlowFactory = walletAccountListFlowFactory,
singleAccountListSupplier = singleAccountListSupplier,
dispatchers = TestingCoroutineDispatcherProvider(),
)
@ -46,7 +47,7 @@ class DefaultMultiAccountListProducerTest {
@AfterEach
fun tearDownEach() {
clearMocks(userWalletsListRepository, walletAccountListFlowFactory)
clearMocks(userWalletsListRepository, singleAccountListSupplier)
}
@Test
@ -56,7 +57,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { singleAccountListSupplier.invoke(userWalletId) } returns flowOf(accountList)
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -68,7 +69,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -82,7 +83,7 @@ class DefaultMultiAccountListProducerTest {
val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
every { singleAccountListSupplier.invoke(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -101,10 +102,10 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -117,7 +118,7 @@ class DefaultMultiAccountListProducerTest {
val accountList = AccountList.empty(userWalletId)
val factoryFlow = MutableStateFlow<AccountList?>(null)
every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull()
every { singleAccountListSupplier.invoke(userWalletId) } returns factoryFlow.filterNotNull()
// Act (first emission)
factoryFlow.value = accountList
@ -136,10 +137,10 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -151,7 +152,7 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val exception = RuntimeException("Converter error")
every { walletAccountListFlowFactory.create(userWalletId) } throws exception
every { singleAccountListSupplier.invoke(userWalletId) } throws exception
// Act
val actual = producer.produceWithFallback().let(::getEmittedValues)
@ -163,7 +164,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -183,7 +184,7 @@ class DefaultMultiAccountListProducerTest {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) }
coVerify(inverse = true) { singleAccountListSupplier.invoke(any<UserWalletId>()) }
}
@Test
@ -192,7 +193,7 @@ class DefaultMultiAccountListProducerTest {
val userWalletsFlow = MutableStateFlow(value = listOf(userWallet))
every { userWalletsListRepository.userWallets } returns userWalletsFlow
every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow()
every { singleAccountListSupplier.invoke(userWalletId) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -203,7 +204,7 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
singleAccountListSupplier.invoke(userWalletId)
}
}
@ -219,8 +220,8 @@ class DefaultMultiAccountListProducerTest {
every { userWalletsListRepository.userWallets } returns userWalletsFlow
val accountList = AccountList.empty(userWalletId)
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
every { walletAccountListFlowFactory.create(userWalletId2) } returns emptyFlow()
every { singleAccountListSupplier.invoke(userWalletId) } returns flowOf(accountList)
every { singleAccountListSupplier.invoke(userWalletId2) } returns emptyFlow()
// Act
val actual = producer.produce().let(::getEmittedValues)
@ -231,8 +232,8 @@ class DefaultMultiAccountListProducerTest {
coVerifySequence {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
walletAccountListFlowFactory.create(userWalletId)
walletAccountListFlowFactory.create(userWalletId2)
singleAccountListSupplier.invoke(userWalletId)
singleAccountListSupplier.invoke(userWalletId2)
}
}
}

View file

@ -1,65 +1,35 @@
package com.tangem.domain.account.usecase
import arrow.core.Option
import arrow.core.getOrElse
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.common.wallets.loadAndGet
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
/**
* Use case to determine if the accounts mode is enabled.
* Accounts mode is considered enabled if there are at least two accounts in any of the user wallets that support
* multiple currencies.
* Accounts mode is considered enabled if any [AccountList] produced for the user's wallets contains at least two
* active accounts.
*
* @property crudRepository repository to perform CRUD operations on accounts.
* @property userWalletsListRepository repository to get the list of user wallets.
* @property multiAccountListSupplier supplier that provides a list of [AccountList]s for all user wallets
*
[REDACTED_AUTHOR]
*/
class IsAccountsModeEnabledUseCase(
private val crudRepository: AccountsCRUDRepository,
private val userWalletsListRepository: UserWalletsListRepository,
private val multiAccountListSupplier: MultiAccountListSupplier,
) {
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<Boolean> {
return userWalletsListRepository.loadAndGet()
.flatMapLatest { userWallets ->
val totalAccountsCountList = getTotalAccountsCountList(userWallets)
combine(flows = totalAccountsCountList) { it.toList().isModeEnabled() }
}
.onEmpty { emit(false) }
return multiAccountListSupplier.invoke()
.map { accountsList -> accountsList.map(AccountList::activeAccounts).isModeEnabled() }
.distinctUntilChanged()
}
suspend fun invokeSync(): Boolean {
return userWalletsListRepository.userWallets.value.orEmpty()
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0
if (!userWallet.isMultiCurrency) return@map 0
crudRepository.getTotalActiveAccountsCountSync(userWalletId = userWallet.walletId).getOrZero()
}
.isModeEnabled()
return multiAccountListSupplier.getSyncOrNull(Unit)
?.map(AccountList::activeAccounts)
?.isModeEnabled() == true
}
private fun getTotalAccountsCountList(userWallets: List<UserWallet>): List<Flow<Int>> {
return userWallets
.map { userWallet ->
// If the wallet does not support multiple currencies, we consider its account count as 0
if (!userWallet.isMultiCurrency) return@map flowOf(0)
crudRepository.getTotalActiveAccountsCount(userWalletId = userWallet.walletId)
.map { maybeCount -> maybeCount.getOrZero() }
}
}
private fun Option<Int>.getOrZero(): Int = getOrElse { 0 }
private fun List<Int>.isModeEnabled(): Boolean = any { it >= 2 }
}

View file

@ -1,15 +1,12 @@
package com.tangem.domain.account.usecase
import arrow.core.none
import arrow.core.some
import com.google.common.truth.Truth
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isMultiCurrency
import io.mockk.*
import kotlinx.coroutines.flow.MutableStateFlow
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
@ -18,21 +15,16 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@Suppress("UnusedFlow")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IsAccountsModeEnabledUseCaseTest {
private val accountsCRUDRepository: AccountsCRUDRepository = mockk()
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true)
private val multiAccountListSupplier: MultiAccountListSupplier = mockk()
private val useCase = IsAccountsModeEnabledUseCase(
crudRepository = accountsCRUDRepository,
userWalletsListRepository = userWalletsListRepository,
)
private val useCase = IsAccountsModeEnabledUseCase(multiAccountListSupplier = multiAccountListSupplier)
@AfterEach
fun tearDown() {
clearMocks(userWalletsListRepository, accountsCRUDRepository)
clearMocks(multiAccountListSupplier)
}
@Nested
@ -40,90 +32,55 @@ class IsAccountsModeEnabledUseCaseTest {
inner class Invoke {
@Test
fun `returns false when loadAndGet emits one wallet with isMultiCurrency false`() = runTest {
fun `returns false when supplier emits empty list`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { multiAccountListSupplier.invoke() } returns flowOf(emptyList())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
}
verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(any()) }
}
@Test
fun `returns true when loadAndGet emits one wallet with isMultiCurrency true`() = runTest {
fun `returns false when supplier emits account list with one account`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some())
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId)
}
}
@Test
fun `returns false when loadAndGet emits one wallet with isMultiCurrency true and None counts`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none())
val accountList = createAccountList(activeAccounts = 1)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId)
}
}
@Test
fun `returns true when loadAndGet emits two wallets, one isMultiCurrency false, one true`() = runTest {
fun `returns true when supplier emits account list with two accounts`() = runTest {
// Arrange
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2))
every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some())
val accountList = createAccountList(activeAccounts = 2)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList))
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
}
coVerifyOrder {
userWalletsListRepository.load()
userWalletsListRepository.userWallets
accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId)
}
@Test
fun `returns true when supplier emits multiple account lists, one with two accounts`() = runTest {
// Arrange
val accountList1 = createAccountList(activeAccounts = 1)
val accountList2 = createAccountList(activeAccounts = 2)
every { multiAccountListSupplier.invoke() } returns flowOf(listOf(accountList1, accountList2))
verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(wallet1.walletId) }
// Act
val actual = useCase.invoke().first()
// Assert
Truth.assertThat(actual).isTrue()
}
}
@ -132,109 +89,71 @@ class IsAccountsModeEnabledUseCaseTest {
inner class InvokeSync {
@Test
fun `returns false when getUserWalletsSync returns empty list`() = runTest {
fun `returns false when getSyncOrNull returns null`() = runTest {
// Arrange
every { userWalletsListRepository.userWallets.value } returns emptyList()
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns null
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
userWalletsListRepository.userWallets.value
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) }
}
@Test
fun `returns false when getUserWalletsSync returns one wallet with isMultiCurrency false`() = runTest {
fun `returns false when getSyncOrNull returns empty list`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = false)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns emptyList()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
verifyOrder {
userWalletsListRepository.userWallets.value
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) }
}
@Test
fun `returns true when getUserWalletsSync returns one wallet with isMultiCurrency true`() = runTest {
fun `returns false when getSyncOrNull returns account list with one account`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
val accountList = createAccountList(activeAccounts = 1)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns 2.some()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
}
@Test
fun `returns true when getSyncOrNull returns account list with two accounts`() = runTest {
// Arrange
val accountList = createAccountList(activeAccounts = 2)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId)
}
}
@Test
fun `returns false when getUserWalletsSync returns multi wallet with None counts`() = runTest {
fun `returns true when getSyncOrNull returns multiple account lists, one with two accounts`() = runTest {
// Arrange
val wallet = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns none()
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isFalse()
coVerifyOrder {
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId)
}
}
@Test
fun `returns true when getUserWalletsSync returns multi and single wallets`() = runTest {
// Arrange
val wallet1 = createUserWallet(isMultiCurrency = false)
val wallet2 = createUserWallet(isMultiCurrency = true)
every { userWalletsListRepository.userWallets.value } returns listOf(wallet1, wallet2)
coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } returns 2.some()
val accountList1 = createAccountList(activeAccounts = 1)
val accountList2 = createAccountList(activeAccounts = 2)
coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(accountList1, accountList2)
// Act
val actual = useCase.invokeSync()
// Assert
Truth.assertThat(actual).isTrue()
coVerifyOrder {
userWalletsListRepository.userWallets.value
accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId)
}
coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet1.walletId) }
}
}
private fun createUserWallet(isMultiCurrency: Boolean): UserWallet = mockk {
every { this@mockk.walletId } returns UserWalletId(stringValue = "011")
every { this@mockk.isMultiCurrency } returns isMultiCurrency
private fun createAccountList(activeAccounts: Int): AccountList = mockk {
every { this@mockk.activeAccounts } returns activeAccounts
}
}