diff --git a/core/ui/src/main/res/drawable/ic_yield_32.xml b/core/ui/src/main/res/drawable/ic_yield_32.xml new file mode 100644 index 0000000000..e3ed6f4694 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_yield_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 0b097d38b8..8c1eaa8cdf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -143,6 +143,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotificationUM.CloreMigration, is WalletNotificationUM.TangemPayRefreshNeeded, WalletNotificationUM.TangemPayUnreachable, + is WalletNotificationUM.YieldBoostPromo, -> null } } 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 05fde4c1ac..ac71135be8 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 @@ -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> { 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.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.addRateAppNotification( isReadyToShowRating: Boolean, clickIntents: WalletClickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index c366fb05ac..3ae343814d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -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 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 new file mode 100644 index 0000000000..fb3b3d3ad7 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactoryTest.kt @@ -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() + .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, + 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") + } +} \ No newline at end of file