Updated on 2026-08-14
This commit is contained in:
parent
0939f4b85e
commit
0e382ab739
3 changed files with 91 additions and 5 deletions
|
|
@ -3,7 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.domain
|
||||||
import com.tangem.common.TangemSiteUrlBuilder
|
import com.tangem.common.TangemSiteUrlBuilder
|
||||||
import com.tangem.common.ui.notifications.NotificationId
|
import com.tangem.common.ui.notifications.NotificationId
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
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.card.common.util.cardTypesResolver
|
||||||
|
import com.tangem.domain.models.TotalFiatBalance
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||||
|
|
@ -20,12 +23,14 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.conflate
|
import kotlinx.coroutines.flow.conflate
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating a list of notifications that can be shown on the wallet screen.
|
* 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.
|
* These notifications are not critical and can be stacked with each other.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
@ModelScoped
|
@ModelScoped
|
||||||
internal class GetWalletNotificationsCarouselFactory @Inject constructor(
|
internal class GetWalletNotificationsCarouselFactory @Inject constructor(
|
||||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||||
|
|
@ -34,8 +39,15 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
|
||||||
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase,
|
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase,
|
||||||
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
|
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
|
||||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
|
||||||
|
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
|
||||||
) {
|
) {
|
||||||
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
|
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
|
||||||
|
val isBalanceResolvedFlow = singleAccountStatusListSupplier(
|
||||||
|
SingleAccountStatusListProducer.Params(userWallet.walletId),
|
||||||
|
)
|
||||||
|
.map { it.totalFiatBalance !is TotalFiatBalance.Loading }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
return combine(
|
return combine(
|
||||||
flow = notificationsRepository.getShouldShowNotification(
|
flow = notificationsRepository.getShouldShowNotification(
|
||||||
NotificationId.EnablePushesReminderNotification.key,
|
NotificationId.EnablePushesReminderNotification.key,
|
||||||
|
|
@ -43,11 +55,15 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
|
||||||
flow2 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
|
flow2 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
|
||||||
flow3 = getWalletsUseCase().conflate(),
|
flow3 = getWalletsUseCase().conflate(),
|
||||||
flow4 = yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(),
|
flow4 = yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(),
|
||||||
) { showPushesNotification, showRateAppPromo, wallets, shouldShowYieldPromoLocal ->
|
flow5 = isBalanceResolvedFlow,
|
||||||
|
) { showPushesNotification, showRateAppPromo, wallets, shouldShowYieldPromoLocal, isBalanceResolved ->
|
||||||
|
|
||||||
buildList {
|
buildList {
|
||||||
addNoteMigrationNotification(userWallet, wallets, clickIntents)
|
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(
|
addPushNotification(
|
||||||
shouldShow = showPushesNotification,
|
shouldShow = showPushesNotification,
|
||||||
|
|
|
||||||
|
|
@ -338,20 +338,22 @@ internal class GetWalletNotificationsFactory @Inject constructor(
|
||||||
) {
|
) {
|
||||||
if (userWallet !is UserWallet.Hot) return
|
if (userWallet !is UserWallet.Hot) return
|
||||||
|
|
||||||
|
if (totalFiatBalance is TotalFiatBalance.Loading) return
|
||||||
|
|
||||||
val isBackupExists = userWallet.backedUp
|
val isBackupExists = userWallet.backedUp
|
||||||
val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword &&
|
val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword &&
|
||||||
!shouldAccessCodeSkipped
|
!shouldAccessCodeSkipped
|
||||||
val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired
|
val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired
|
||||||
|
|
||||||
val messageEffect = when (totalFiatBalance) {
|
val messageEffect = when (totalFiatBalance) {
|
||||||
TotalFiatBalance.Failed,
|
|
||||||
TotalFiatBalance.Loading,
|
|
||||||
-> TangemMessageEffect.None
|
|
||||||
is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) {
|
is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) {
|
||||||
TangemMessageEffect.Warning
|
TangemMessageEffect.Warning
|
||||||
} else {
|
} else {
|
||||||
TangemMessageEffect.None
|
TangemMessageEffect.None
|
||||||
}
|
}
|
||||||
|
TotalFiatBalance.Loading,
|
||||||
|
TotalFiatBalance.Failed,
|
||||||
|
-> TangemMessageEffect.None
|
||||||
}
|
}
|
||||||
|
|
||||||
addIf(
|
addIf(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.domain
|
||||||
|
|
||||||
import arrow.core.Either
|
import arrow.core.Either
|
||||||
import com.google.common.truth.Truth.assertThat
|
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.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
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.api.TestInstance
|
||||||
import org.junit.jupiter.params.ParameterizedTest
|
import org.junit.jupiter.params.ParameterizedTest
|
||||||
import org.junit.jupiter.params.provider.MethodSource
|
import org.junit.jupiter.params.provider.MethodSource
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
internal class GetWalletNotificationsCarouselFactoryTest {
|
internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
|
|
@ -35,6 +43,7 @@ internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase = mockk()
|
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase = mockk()
|
||||||
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase = mockk()
|
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase = mockk()
|
||||||
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles = mockk()
|
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles = mockk()
|
||||||
|
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk(relaxed = true)
|
||||||
private val clickIntents: WalletClickIntents = mockk(relaxed = true)
|
private val clickIntents: WalletClickIntents = mockk(relaxed = true)
|
||||||
private val userWallet: UserWallet.Hot = mockk(relaxed = true)
|
private val userWallet: UserWallet.Hot = mockk(relaxed = true)
|
||||||
|
|
||||||
|
|
@ -45,6 +54,7 @@ internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
shouldShowYieldBoostMainBannerUseCase = shouldShowYieldBoostMainBannerUseCase,
|
shouldShowYieldBoostMainBannerUseCase = shouldShowYieldBoostMainBannerUseCase,
|
||||||
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
|
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
|
||||||
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
|
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
|
||||||
|
singleAccountStatusListSupplier = singleAccountStatusListSupplier,
|
||||||
)
|
)
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
|
@ -56,6 +66,7 @@ internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
shouldShowYieldBoostMainBannerUseCase,
|
shouldShowYieldBoostMainBannerUseCase,
|
||||||
yieldSupplyGetShouldShowMainPromoUseCase,
|
yieldSupplyGetShouldShowMainPromoUseCase,
|
||||||
yieldSupplyFeatureToggles,
|
yieldSupplyFeatureToggles,
|
||||||
|
singleAccountStatusListSupplier,
|
||||||
clickIntents,
|
clickIntents,
|
||||||
userWallet,
|
userWallet,
|
||||||
)
|
)
|
||||||
|
|
@ -68,6 +79,10 @@ internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(true)
|
every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(true)
|
||||||
every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns true
|
every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns true
|
||||||
coEvery { shouldShowYieldBoostMainBannerUseCase(any()) } returns Either.Right(true)
|
coEvery { shouldShowYieldBoostMainBannerUseCase(any()) } returns Either.Right(true)
|
||||||
|
// Balance is loaded by default, so banners gated on balance are not suppressed.
|
||||||
|
every {
|
||||||
|
singleAccountStatusListSupplier(any<SingleAccountStatusListProducer.Params>())
|
||||||
|
} returns flowOf(accountStatusList(TotalFiatBalance.Loaded(BigDecimal.ZERO, StatusSource.ACTUAL)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
|
|
@ -85,6 +100,24 @@ internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
assertThat(result.any { it is WalletNotificationUM.YieldBoostPromo }).isEqualTo(model.expectedShown)
|
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<SingleAccountStatusListProducer.Params>())
|
||||||
|
} 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
|
@Test
|
||||||
fun `GIVEN banner shown WHEN buttons clicked THEN routes to click intents`() = runTest {
|
fun `GIVEN banner shown WHEN buttons clicked THEN routes to click intents`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
@ -101,6 +134,16 @@ internal class GetWalletNotificationsCarouselFactoryTest {
|
||||||
verify { clickIntents.onDismissYieldBoostBanner(WALLET_ID) }
|
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(
|
internal data class Model(
|
||||||
val toggleEnabled: Boolean,
|
val toggleEnabled: Boolean,
|
||||||
val shouldShowLocal: 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 {
|
private companion object {
|
||||||
val WALLET_ID = UserWalletId("01")
|
val WALLET_ID = UserWalletId("01")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue