From 0e382ab739676ccc3d053d5d4f5d08ccde249b29 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jun 2026 20:08:49 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../GetWalletNotificationsCarouselFactory.kt | 20 +++++- .../domain/GetWalletNotificationsFactory.kt | 8 ++- ...tWalletNotificationsCarouselFactoryTest.kt | 68 +++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt index ac71135be8..82610ab8c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt @@ -3,7 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.TangemSiteUrlBuilder import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.settings.IsReadyToShowRateAppUseCase @@ -20,12 +23,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import javax.inject.Inject /** * Factory for creating a list of notifications that can be shown on the wallet screen. * These notifications are not critical and can be stacked with each other. */ +@Suppress("LongParameterList") @ModelScoped internal class GetWalletNotificationsCarouselFactory @Inject constructor( private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, @@ -34,8 +39,15 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor( private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + val isBalanceResolvedFlow = singleAccountStatusListSupplier( + SingleAccountStatusListProducer.Params(userWallet.walletId), + ) + .map { it.totalFiatBalance !is TotalFiatBalance.Loading } + .distinctUntilChanged() + return combine( flow = notificationsRepository.getShouldShowNotification( NotificationId.EnablePushesReminderNotification.key, @@ -43,11 +55,15 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor( flow2 = isReadyToShowRateAppUseCase().distinctUntilChanged(), flow3 = getWalletsUseCase().conflate(), flow4 = yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(), - ) { showPushesNotification, showRateAppPromo, wallets, shouldShowYieldPromoLocal -> + flow5 = isBalanceResolvedFlow, + ) { showPushesNotification, showRateAppPromo, wallets, shouldShowYieldPromoLocal, isBalanceResolved -> buildList { addNoteMigrationNotification(userWallet, wallets, clickIntents) - addRateAppNotification(showRateAppPromo, clickIntents) + + // isBalanceResolved gates Rate App on the balance leaving the loading state, so it does not + // flash during loading and then get replaced once balance-dependent banners are resolved. + addRateAppNotification(showRateAppPromo && isBalanceResolved, clickIntents) addPushNotification( shouldShow = showPushesNotification, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index 6909483f63..5085257879 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -338,20 +338,22 @@ internal class GetWalletNotificationsFactory @Inject constructor( ) { if (userWallet !is UserWallet.Hot) return + if (totalFiatBalance is TotalFiatBalance.Loading) return + val isBackupExists = userWallet.backedUp val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && !shouldAccessCodeSkipped val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired val messageEffect = when (totalFiatBalance) { - TotalFiatBalance.Failed, - TotalFiatBalance.Loading, - -> TangemMessageEffect.None is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) { TangemMessageEffect.Warning } else { TangemMessageEffect.None } + TotalFiatBalance.Loading, + TotalFiatBalance.Failed, + -> TangemMessageEffect.None } addIf( diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactoryTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactoryTest.kt index fb3b3d3ad7..5ce92507a5 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactoryTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactoryTest.kt @@ -2,6 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either import com.google.common.truth.Truth.assertThat +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.notifications.repository.NotificationsRepository @@ -25,6 +32,7 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource +import java.math.BigDecimal @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class GetWalletNotificationsCarouselFactoryTest { @@ -35,6 +43,7 @@ internal class GetWalletNotificationsCarouselFactoryTest { private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase = mockk() private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase = mockk() private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles = mockk() + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk(relaxed = true) private val clickIntents: WalletClickIntents = mockk(relaxed = true) private val userWallet: UserWallet.Hot = mockk(relaxed = true) @@ -45,6 +54,7 @@ internal class GetWalletNotificationsCarouselFactoryTest { shouldShowYieldBoostMainBannerUseCase = shouldShowYieldBoostMainBannerUseCase, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, ) @BeforeEach @@ -56,6 +66,7 @@ internal class GetWalletNotificationsCarouselFactoryTest { shouldShowYieldBoostMainBannerUseCase, yieldSupplyGetShouldShowMainPromoUseCase, yieldSupplyFeatureToggles, + singleAccountStatusListSupplier, clickIntents, userWallet, ) @@ -68,6 +79,10 @@ internal class GetWalletNotificationsCarouselFactoryTest { every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(true) every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns true coEvery { shouldShowYieldBoostMainBannerUseCase(any()) } returns Either.Right(true) + // Balance is loaded by default, so banners gated on balance are not suppressed. + every { + singleAccountStatusListSupplier(any()) + } returns flowOf(accountStatusList(TotalFiatBalance.Loaded(BigDecimal.ZERO, StatusSource.ACTUAL))) } @ParameterizedTest @@ -85,6 +100,24 @@ internal class GetWalletNotificationsCarouselFactoryTest { assertThat(result.any { it is WalletNotificationUM.YieldBoostPromo }).isEqualTo(model.expectedShown) } + @ParameterizedTest + @MethodSource("provideRateAppTestModels") + fun `GIVEN ready to show rate app and balance state WHEN create THEN rate app banner visibility matches`( + model: RateAppModel, + ) = runTest { + // Arrange + every { isReadyToShowRateAppUseCase() } returns flowOf(model.isReadyToShow) + every { + singleAccountStatusListSupplier(any()) + } returns flowOf(accountStatusList(model.balance)) + + // Act + val result = factory.create(userWallet, clickIntents).first() + + // Assert + assertThat(result.any { it is WalletNotificationUM.RateApp }).isEqualTo(model.expectedShown) + } + @Test fun `GIVEN banner shown WHEN buttons clicked THEN routes to click intents`() = runTest { // Arrange @@ -101,6 +134,16 @@ internal class GetWalletNotificationsCarouselFactoryTest { verify { clickIntents.onDismissYieldBoostBanner(WALLET_ID) } } + private fun accountStatusList(balance: TotalFiatBalance) = AccountStatusList( + userWalletId = WALLET_ID, + accountStatuses = emptyList(), + totalAccounts = 0, + totalArchivedAccounts = 0, + totalFiatBalance = balance, + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) + internal data class Model( val toggleEnabled: Boolean, val shouldShowLocal: Boolean, @@ -121,6 +164,31 @@ internal class GetWalletNotificationsCarouselFactoryTest { ), ) + internal data class RateAppModel( + val isReadyToShow: Boolean, + val balance: TotalFiatBalance, + val expectedShown: Boolean, + ) + + private fun provideRateAppTestModels() = listOf( + // Ready to show, but the balance is still loading — don't flash before Add Funds may appear. + RateAppModel(isReadyToShow = true, balance = TotalFiatBalance.Loading, expectedShown = false), + // Ready to show and the balance is loaded — the banner can appear. + RateAppModel( + isReadyToShow = true, + balance = TotalFiatBalance.Loaded(BigDecimal.ZERO, StatusSource.ACTUAL), + expectedShown = true, + ), + // Ready to show and the balance failed — terminal state, only loading suppresses the banner. + RateAppModel(isReadyToShow = true, balance = TotalFiatBalance.Failed, expectedShown = true), + // Not ready to show — the banner stays hidden regardless of the balance state. + RateAppModel( + isReadyToShow = false, + balance = TotalFiatBalance.Loaded(BigDecimal.ZERO, StatusSource.ACTUAL), + expectedShown = false, + ), + ) + private companion object { val WALLET_ID = UserWalletId("01") }