Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-09 14:24:30 +04:00
parent 7cecc3f90c
commit 0f1a926cc4
5 changed files with 200 additions and 1 deletions

View file

@ -143,6 +143,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotificationUM.CloreMigration,
is WalletNotificationUM.TangemPayRefreshNeeded,
WalletNotificationUM.TangemPayUnreachable,
is WalletNotificationUM.YieldBoostPromo,
-> null
}
}

View file

@ -8,8 +8,11 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -28,6 +31,9 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val notificationsRepository: NotificationsRepository,
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotificationUM>> {
return combine(
@ -36,7 +42,8 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
).distinctUntilChanged(),
flow2 = isReadyToShowRateAppUseCase().distinctUntilChanged(),
flow3 = getWalletsUseCase().conflate(),
) { showPushesNotification, showRateAppPromo, wallets ->
flow4 = yieldSupplyGetShouldShowMainPromoUseCase().distinctUntilChanged(),
) { showPushesNotification, showRateAppPromo, wallets, shouldShowYieldPromoLocal ->
buildList {
addNoteMigrationNotification(userWallet, wallets, clickIntents)
@ -47,10 +54,33 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor(
isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(),
clickIntents = clickIntents,
)
addYieldBoostBannerNotification(
userWallet = userWallet,
shouldShowLocal = shouldShowYieldPromoLocal,
clickIntents = clickIntents,
)
}.sortedBy { it.type.ordinal }.toImmutableList()
}
}
private suspend fun MutableList<WalletNotificationUM>.addYieldBoostBannerNotification(
userWallet: UserWallet,
shouldShowLocal: Boolean,
clickIntents: WalletClickIntents,
) {
if (!shouldShowLocal) return
if (!yieldSupplyFeatureToggles.isYieldPromoEnabled) return
val shouldShow = shouldShowYieldBoostMainBannerUseCase(userWallet.walletId).getOrNull() == true
if (!shouldShow) return
add(
WalletNotificationUM.YieldBoostPromo(
onExploreClick = { clickIntents.onYieldBoostBannerClick(userWallet.walletId) },
onLaterClick = { clickIntents.onDismissYieldBoostBanner(userWallet.walletId) },
),
)
}
private fun MutableList<WalletNotificationUM>.addRateAppNotification(
isReadyToShowRating: Boolean,
clickIntents: WalletClickIntents,

View file

@ -8,8 +8,10 @@ import com.tangem.core.ui.ds.message.TangemMessageButtonUM
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
@ -412,6 +414,36 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t
type = WalletNotificationType.Promo,
)
data class YieldBoostPromo(
val onExploreClick: () -> Unit,
val onLaterClick: () -> Unit,
) : WalletNotificationUM(
messageUM = TangemMessageUM(
id = "YieldBoostPromoNotification",
title = combinedReference(
resourceReference(CoreResR.string.yield_apy_boost_banner_title),
stringReference(" · "),
resourceReference(CoreResR.string.yield_apy_boost_banner_title_apy_multiplied),
),
subtitle = resourceReference(CoreResR.string.yield_apy_boost_banner_subtitle),
iconUM = TangemIconUM.Image(imageRes = CoreUiR.drawable.ic_yield_32),
messageEffect = TangemMessageEffect.Magic,
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.common_later),
type = TangemButtonType.Secondary,
onClick = onLaterClick,
),
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.yield_apy_boost_banner_button_title),
type = TangemButtonType.Primary,
onClick = onExploreClick,
),
),
),
type = WalletNotificationType.Promo,
)
// endregion
// region Survey

View file

@ -0,0 +1,127 @@
package com.tangem.feature.wallet.presentation.wallet.domain
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.yield.supply.promo.usecase.ShouldShowYieldBoostMainBannerUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
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
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class GetWalletNotificationsCarouselFactoryTest {
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase = mockk()
private val getWalletsUseCase: GetWalletsUseCase = mockk()
private val notificationsRepository: NotificationsRepository = mockk()
private val shouldShowYieldBoostMainBannerUseCase: ShouldShowYieldBoostMainBannerUseCase = mockk()
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase = mockk()
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles = mockk()
private val clickIntents: WalletClickIntents = mockk(relaxed = true)
private val userWallet: UserWallet.Hot = mockk(relaxed = true)
private val factory = GetWalletNotificationsCarouselFactory(
isReadyToShowRateAppUseCase = isReadyToShowRateAppUseCase,
getWalletsUseCase = getWalletsUseCase,
notificationsRepository = notificationsRepository,
shouldShowYieldBoostMainBannerUseCase = shouldShowYieldBoostMainBannerUseCase,
yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase,
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
@BeforeEach
fun setup() {
clearMocks(
isReadyToShowRateAppUseCase,
getWalletsUseCase,
notificationsRepository,
shouldShowYieldBoostMainBannerUseCase,
yieldSupplyGetShouldShowMainPromoUseCase,
yieldSupplyFeatureToggles,
clickIntents,
userWallet,
)
// Defaults: only the yield boost banner can appear; every other notification is suppressed.
every { userWallet.walletId } returns WALLET_ID
every { notificationsRepository.getShouldShowNotification(any()) } returns flowOf(false)
coEvery { notificationsRepository.isUserAllowToSubscribeOnPushNotifications() } returns true
every { isReadyToShowRateAppUseCase() } returns flowOf(false)
every { getWalletsUseCase() } returns flowOf(emptyList())
every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(true)
every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns true
coEvery { shouldShowYieldBoostMainBannerUseCase(any()) } returns Either.Right(true)
}
@ParameterizedTest
@MethodSource("provideTestModels")
fun `GIVEN gating conditions WHEN create THEN yield boost banner visibility matches`(model: Model) = runTest {
// Arrange
every { yieldSupplyFeatureToggles.isYieldPromoEnabled } returns model.toggleEnabled
every { yieldSupplyGetShouldShowMainPromoUseCase() } returns flowOf(model.shouldShowLocal)
coEvery { shouldShowYieldBoostMainBannerUseCase(WALLET_ID) } returns model.mainBanner
// Act
val result = factory.create(userWallet, clickIntents).first()
// Assert
assertThat(result.any { it is WalletNotificationUM.YieldBoostPromo }).isEqualTo(model.expectedShown)
}
@Test
fun `GIVEN banner shown WHEN buttons clicked THEN routes to click intents`() = runTest {
// Arrange
val banner = factory.create(userWallet, clickIntents).first()
.filterIsInstance<WalletNotificationUM.YieldBoostPromo>()
.first()
// Act
banner.onExploreClick()
banner.onLaterClick()
// Assert
verify { clickIntents.onYieldBoostBannerClick(WALLET_ID) }
verify { clickIntents.onDismissYieldBoostBanner(WALLET_ID) }
}
internal data class Model(
val toggleEnabled: Boolean,
val shouldShowLocal: Boolean,
val mainBanner: Either<Throwable, Boolean>,
val expectedShown: Boolean,
)
private fun provideTestModels() = listOf(
Model(toggleEnabled = true, shouldShowLocal = true, mainBanner = Either.Right(true), expectedShown = true),
Model(toggleEnabled = false, shouldShowLocal = true, mainBanner = Either.Right(true), expectedShown = false),
Model(toggleEnabled = true, shouldShowLocal = false, mainBanner = Either.Right(true), expectedShown = false),
Model(toggleEnabled = true, shouldShowLocal = true, mainBanner = Either.Right(false), expectedShown = false),
Model(
toggleEnabled = true,
shouldShowLocal = true,
mainBanner = Either.Left(RuntimeException("boom")),
expectedShown = false,
),
)
private companion object {
val WALLET_ID = UserWalletId("01")
}
}