From 0029b514bd8ba797eafdafdfa03a4e051cb5ef51 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 22:44:22 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../status/di/AccountStatusUseCaseModule.kt | 4 +- .../usecase/IsAccountsModeEnabledUseCase.kt | 41 ++-- .../IsAccountsModeEnabledUseCaseTest.kt | 182 +++++++----------- 3 files changed, 90 insertions(+), 137 deletions(-) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 859541ef01..9be91f042e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -10,12 +10,12 @@ import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.card.IsWalletBackupProblematicUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.nft.utils.NFTCleaner +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher @@ -71,10 +71,12 @@ internal object AccountStatusUseCaseModule { fun provideIsAccountsModeEnabledUseCase( multiAccountListSupplier: MultiAccountListSupplier, paymentAccountStatusSupplier: PaymentAccountStatusSupplier, + appCoroutineScope: AppCoroutineScope, ): IsAccountsModeEnabledUseCase { return IsAccountsModeEnabledUseCase( multiAccountListSupplier = multiAccountListSupplier, paymentAccountStatusSupplier = paymentAccountStatusSupplier, + appCoroutineScope = appCoroutineScope, ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt index fc345758ff..5433fc0409 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt @@ -4,11 +4,13 @@ import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.PaymentAccountStatusValue -import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import java.util.concurrent.TimeUnit /** * Use case to determine if the accounts mode is enabled. @@ -24,10 +26,23 @@ import kotlinx.coroutines.flow.* class IsAccountsModeEnabledUseCase( private val multiAccountListSupplier: MultiAccountListSupplier, private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, + appCoroutineScope: AppCoroutineScope, ) { + private val flow = createFlow() + .retry { + delay(timeMillis = TimeUnit.SECONDS.toMillis(1)) + true + } + .distinctUntilChanged() + .shareIn( + scope = appCoroutineScope, + started = SharingStarted.WhileSubscribed(0, 0), + replay = 1, + ) + @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(): Flow { + private fun createFlow(): Flow { TangemLogger.i("$TAG: invoke() started") val cryptoMode = multiAccountListSupplier.invoke() @@ -73,25 +88,14 @@ class IsAccountsModeEnabledUseCase( TangemLogger.i("$TAG: final combine crypto=$crypto, payment=$payment, result=$isEnabled") isEnabled } - .distinctUntilChanged() + } + + operator fun invoke(): Flow { + return flow } suspend fun invokeSync(): Boolean { - val accountLists = multiAccountListSupplier.getSyncOrNull(Unit).orEmpty() - if (accountLists.any { it.hasMultipleCryptoPortfolios() }) return true - - val walletIdsWithPayment = accountLists.mapNotNull { list -> - if (list.accounts.any { it is Account.Payment }) list.userWalletId else null - } - return walletIdsWithPayment.any { walletId -> - paymentAccountStatusSupplier - .getSyncOrNull( - params = PaymentAccountStatusProducer.Params(walletId), - timeMillis = PAYMENT_STATUS_SYNC_TIMEOUT_MS, - ) - ?.value - ?.isActivePayment() == true - } + return flow.first() } private fun AccountList.hasMultipleCryptoPortfolios(): Boolean = @@ -113,7 +117,6 @@ class IsAccountsModeEnabledUseCase( } private companion object { - const val PAYMENT_STATUS_SYNC_TIMEOUT_MS = 1_000L const val TAG = "IsAccountsModeEnabledUseCase" } } \ No newline at end of file diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt index 1bbc3b9ff4..c679f4bcee 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCaseTest.kt @@ -7,14 +7,16 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import com.tangem.test.core.TestAppCoroutineScope +import com.tangem.test.core.getEmittedValues import io.mockk.clearMocks -import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.last +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Nested @@ -27,11 +29,6 @@ class IsAccountsModeEnabledUseCaseTest { private val multiAccountListSupplier: MultiAccountListSupplier = mockk() private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier = mockk() - private val useCase = IsAccountsModeEnabledUseCase( - multiAccountListSupplier = multiAccountListSupplier, - paymentAccountStatusSupplier = paymentAccountStatusSupplier, - ) - @AfterEach fun tearDown() { clearMocks(multiAccountListSupplier, paymentAccountStatusSupplier) @@ -43,9 +40,9 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns false when supplier emits empty list`() = runTest { - every { multiAccountListSupplier.invoke() } returns flowOf(emptyList()) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(emptyList()) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isFalse() } @@ -53,9 +50,9 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns false when single crypto portfolio account`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isFalse() } @@ -63,9 +60,9 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when two crypto portfolio accounts`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockCryptoPortfolio())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -73,10 +70,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when payment account is Loaded`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, mockk()) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -84,10 +81,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns false when payment account is NotCreated`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.NotCreated) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isFalse() } @@ -95,10 +92,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns false when payment account is Empty`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.Empty) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isFalse() } @@ -106,10 +103,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when payment account is UnderReview`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, mockk()) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -117,10 +114,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when payment account is IssuingCard`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, mockk()) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -128,10 +125,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when payment account is Loading`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.Loading) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -139,10 +136,10 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when payment account is Deactivated`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) mockPaymentStatus(WALLET_ID_1, mockk()) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -151,9 +148,9 @@ class IsAccountsModeEnabledUseCaseTest { fun `returns true when multiple wallets and one has two crypto portfolios`() = runTest { val list1 = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio())) val list2 = createAccountList(WALLET_ID_2, listOf(mockCryptoPortfolio(), mockCryptoPortfolio())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list1, list2)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list1, list2)) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -162,10 +159,10 @@ class IsAccountsModeEnabledUseCaseTest { fun `returns true when multiple wallets and one has active payment`() = runTest { val list1 = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio())) val list2 = createAccountList(WALLET_ID_2, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - every { multiAccountListSupplier.invoke() } returns flowOf(listOf(list1, list2)) + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list1, list2)) mockPaymentStatus(WALLET_ID_2, mockk()) - val actual = useCase.invoke().last() + val actual = settledValue(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -176,29 +173,9 @@ class IsAccountsModeEnabledUseCaseTest { inner class InvokeSync { @Test - fun `returns false when getSyncOrNull returns null`() = runTest { - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns null - - val actual = useCase.invokeSync() - - Truth.assertThat(actual).isFalse() - } - - @Test - fun `returns false when getSyncOrNull returns empty list`() = runTest { - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns emptyList() - - val actual = useCase.invokeSync() - - Truth.assertThat(actual).isFalse() - } - - @Test - fun `returns false when single crypto portfolio account`() = runTest { - val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - - val actual = useCase.invokeSync() + fun `returns false when supplier emits empty list`() = runTest { + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(emptyList()) + val actual = invokeSyncSettled(createUseCase()) Truth.assertThat(actual).isFalse() } @@ -206,9 +183,8 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when two crypto portfolio accounts`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockCryptoPortfolio())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - - val actual = useCase.invokeSync() + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) + val actual = invokeSyncSettled(createUseCase()) Truth.assertThat(actual).isTrue() } @@ -216,69 +192,48 @@ class IsAccountsModeEnabledUseCaseTest { @Test fun `returns true when payment account is Loaded`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - mockPaymentStatusSync(WALLET_ID_1, mockk()) - - val actual = useCase.invokeSync() + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) + mockPaymentStatus(WALLET_ID_1, mockk()) + val actual = invokeSyncSettled(createUseCase()) Truth.assertThat(actual).isTrue() } - @Test - fun `returns false when payment account is NotCreated`() = runTest { - val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - mockPaymentStatusSync(WALLET_ID_1, PaymentAccountStatusValue.NotCreated) - - val actual = useCase.invokeSync() - - Truth.assertThat(actual).isFalse() - } - @Test fun `returns false when payment account is Empty`() = runTest { val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - mockPaymentStatusSync(WALLET_ID_1, PaymentAccountStatusValue.Empty) - - val actual = useCase.invokeSync() + every { multiAccountListSupplier.invoke() } returns MutableStateFlow(listOf(list)) + mockPaymentStatus(WALLET_ID_1, PaymentAccountStatusValue.Empty) + val actual = invokeSyncSettled(createUseCase()) Truth.assertThat(actual).isFalse() } + } - @Test - fun `returns true when payment account is UnderReview`() = runTest { - val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - mockPaymentStatusSync(WALLET_ID_1, mockk()) + @OptIn(ExperimentalCoroutinesApi::class) + private fun TestScope.createUseCase(): IsAccountsModeEnabledUseCase = IsAccountsModeEnabledUseCase( + multiAccountListSupplier = multiAccountListSupplier, + paymentAccountStatusSupplier = paymentAccountStatusSupplier, + appCoroutineScope = TestAppCoroutineScope( + backgroundScope.coroutineContext + UnconfinedTestDispatcher(testScheduler), + ), + ) - val actual = useCase.invokeSync() + /** + * Returns the resolved value of the shared [IsAccountsModeEnabledUseCase.invoke] flow. The flow is + * a hot [kotlinx.coroutines.flow.SharedFlow] (replay = 1) that never completes, so we read the + * last value emitted while a subscriber is active. + */ + private fun TestScope.settledValue(useCase: IsAccountsModeEnabledUseCase): Boolean = + getEmittedValues(useCase.invoke()).last() - Truth.assertThat(actual).isTrue() - } - - @Test - fun `returns true when payment account is Deactivated`() = runTest { - val list = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list) - mockPaymentStatusSync(WALLET_ID_1, mockk()) - - val actual = useCase.invokeSync() - - Truth.assertThat(actual).isTrue() - } - - @Test - fun `returns true when multiple wallets and one has loaded payment`() = runTest { - val list1 = createAccountList(WALLET_ID_1, listOf(mockCryptoPortfolio())) - val list2 = createAccountList(WALLET_ID_2, listOf(mockCryptoPortfolio(), mockPaymentAccount())) - coEvery { multiAccountListSupplier.getSyncOrNull(Unit, any()) } returns listOf(list1, list2) - mockPaymentStatusSync(WALLET_ID_2, mockk()) - - val actual = useCase.invokeSync() - - Truth.assertThat(actual).isTrue() - } + /** + * Samples via [IsAccountsModeEnabledUseCase.invokeSync] while a subscriber keeps the shared flow + * warm — mirroring production, where a screen already observes the flow before invokeSync reads it. + */ + private suspend fun TestScope.invokeSyncSettled(useCase: IsAccountsModeEnabledUseCase): Boolean { + getEmittedValues(useCase.invoke()) + return useCase.invokeSync() } private fun mockCryptoPortfolio(): Account.CryptoPortfolio = mockk() @@ -287,14 +242,7 @@ class IsAccountsModeEnabledUseCaseTest { private fun mockPaymentStatus(walletId: UserWalletId, value: PaymentAccountStatusValue) { val status = mockk { every { this@mockk.value } returns value } - every { paymentAccountStatusSupplier.invoke(walletId) } returns flowOf(status) - } - - private fun mockPaymentStatusSync(walletId: UserWalletId, value: PaymentAccountStatusValue) { - val status = mockk { every { this@mockk.value } returns value } - coEvery { - paymentAccountStatusSupplier.getSyncOrNull(PaymentAccountStatusProducer.Params(walletId), any()) - } returns status + every { paymentAccountStatusSupplier.invoke(walletId) } returns MutableStateFlow(status) } private fun createAccountList(walletId: UserWalletId, accounts: List): AccountList = mockk {