From 9adbde6e55b0b5683ae2e39e3721b07deb630e63 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 13:00:40 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 27 ++ .../local/preferences/PreferencesKeys.kt | 4 - .../hotwallet/DefaultHotWalletRepository.kt | 29 +- domain/hot-wallet/build.gradle.kts | 6 + .../CheckHotWalletUpgradeBannerUseCase.kt | 61 +++++ .../CloseHotWalletUpgradeBannerUseCase.kt | 20 ++ ...ShouldShowUpgradeHotWalletBannerUseCase.kt | 12 + .../repository/HotWalletRepository.kt | 10 +- .../CheckHotWalletUpgradeBannerUseCaseTest.kt | 257 ++++++++++++++++++ .../CloseHotWalletUpgradeBannerUseCaseTest.kt | 39 +++ .../intents/WalletWarningsClickIntents.kt | 16 +- .../domain/GetMultiWalletWarningsFactory.kt | 48 +--- 12 files changed, 454 insertions(+), 75 deletions(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt create mode 100644 domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt create mode 100644 domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 8c8404b2d3..c99860711a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -1,9 +1,12 @@ package com.tangem.tap.di.domain +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.hotwallet.repository.HotWalletRepository import dagger.Module import dagger.Provides @@ -40,4 +43,28 @@ internal object HotWalletDomainModule { ): IsHotWalletCreationSupported { return IsHotWalletCreationSupported(hotWalletRepository) } + + @Provides + @Singleton + fun provideCheckHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CheckHotWalletUpgradeBannerUseCase { + return CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideCloseHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CloseHotWalletUpgradeBannerUseCase { + return CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideShouldShowUpgradeHotWalletBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): ShouldShowUpgradeHotWalletBannerUseCase { + return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index eae141d861..e0c785a2e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -134,10 +134,6 @@ object PreferencesKeys { val SHOULD_SHOW_UPGRADE_BANNER_KEY by lazy { stringPreferencesKey(name = "shouldShowUpgradeBanner") } - val SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY by lazy { - stringPreferencesKey(name = "shouldShowNextTimeUpgradeBanner") - } - val UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "upgradeBannerClosureTimestamp") } val WALLET_CREATION_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "walletCreationTimestamp") } diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index 686f94579d..dbbe66db29 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -10,11 +10,14 @@ import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.util.concurrent.ConcurrentHashMap internal class DefaultHotWalletRepository( private val appPreferencesStore: AppPreferencesStore, ) : HotWalletRepository { + private val firstTopUpDetectedThisSession = ConcurrentHashMap() + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) override fun isWalletCreationSupported(): Boolean { return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q @@ -50,28 +53,12 @@ internal class DefaultHotWalletRepository( } } - override fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow = appPreferencesStore - .getObjectMap(PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY) - .map { it[userWalletId.stringValue] == true } - - override suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) { - appPreferencesStore.editData { mutablePreferences -> - mutablePreferences.setObjectMap( - key = PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, - value = mutablePreferences.getObjectMap( - PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, - ) - .plus(userWalletId.stringValue to shouldShow), - ) - } - } - override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { return appPreferencesStore .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] } - override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) { + override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { appPreferencesStore.editData { mutablePreferences -> mutablePreferences.setObjectMap( key = PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY, @@ -110,4 +97,12 @@ internal class DefaultHotWalletRepository( ) } } + + override fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean { + return firstTopUpDetectedThisSession.containsKey(userWalletId.stringValue) + } + + override fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) { + firstTopUpDetectedThisSession[userWalletId.stringValue] = Unit + } } \ No newline at end of file diff --git a/domain/hot-wallet/build.gradle.kts b/domain/hot-wallet/build.gradle.kts index 471afb3452..621b607241 100644 --- a/domain/hot-wallet/build.gradle.kts +++ b/domain/hot-wallet/build.gradle.kts @@ -14,4 +14,10 @@ dependencies { implementation(projects.domain.wallets.models) implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..1038773b7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke( + walletId: UserWalletId, + hasBalance: Boolean, + shouldShowUpgradeBanner: Boolean, + ): Either = try { + val currentTime = System.currentTimeMillis() + val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) + + val creationTimestampActual = if (creationTimestamp == null) { + // If creationTimestamp is null (wallet was created before this feature was released), + // store the current timestamp and use it below + hotWalletRepository.setWalletCreationTimestamp(walletId, currentTime) + currentTime + } else { + creationTimestamp + } + + val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) + val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) + + val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) + val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } + + // Wallet balance is positive, but the first top-up hasn't been tracked yet + if (hasBalance && !hasHadFirstTopUp) { + hotWalletRepository.setHasHadFirstTopUp(walletId, true) + hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) + hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) + } + + val shouldShow = when { + // Banner should be shown (e.g., because of the first top-up in the previous session) + shouldShowUpgradeBanner -> !hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) + // Banner was closed; it happened more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp != null && daysSinceClosure != null && daysSinceClosure >= BANNER_RESHOW_DAYS -> true + // Banner hasn't been closed; wallet was created more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp == null && daysSinceCreation >= BANNER_RESHOW_DAYS -> true + else -> false + } + shouldShow.right() + } catch (e: Exception) { + e.left() + } + + companion object { + const val BANNER_RESHOW_DAYS = 30L + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..17591a2bb1 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId + +class CloseHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke(walletId: UserWalletId): Either = try { + val currentTime = System.currentTimeMillis() + hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, currentTime) + Unit.right() + } catch (e: Exception) { + e.left() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt new file mode 100644 index 0000000000..49e17e6c7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class ShouldShowUpgradeHotWalletBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow = + hotWalletRepository.shouldShowUpgradeBanner(userWalletId) +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 2432bc3dc9..01229d8d3a 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -17,13 +17,9 @@ interface HotWalletRepository { suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow - - suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? - suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) + suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? @@ -32,4 +28,8 @@ interface HotWalletRepository { suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) + + fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean + + fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..6e415efea4 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,257 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setWalletCreationTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists and 30 days since closure WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists but less than 30 days since closure WHEN invoke THEN return false`() = + runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set but closure timestamp exists WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setHasHadFirstTopUp(walletId, true) } + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) } + verify { hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) } + } + + @Test + fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } + coVerify(exactly = 0) { hotWalletRepository.setShouldShowUpgradeBanner(any(), any()) } + } + + @Test + fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result1 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + val result2 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + val result3 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat((result1 as Either.Right).value).isTrue() + assertThat((result2 as Either.Right).value).isTrue() + assertThat((result3 as Either.Right).value).isTrue() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..d961d07617 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CloseHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `WHEN invoke THEN set banner flag to false and closure timestamp`() = runTest { + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Right::class.java) + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN repository throws exception WHEN invoke THEN return Either Left`() = runTest { + val exception = RuntimeException("Test error") + coEvery { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } throws exception + + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Left::class.java) + assertThat((result as Either.Left).value).isEqualTo(exception) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ca5a2d4caa..0cdab04aa3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -17,7 +17,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -135,7 +135,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, - private val hotWalletRepository: HotWalletRepository, + private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -554,8 +554,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() if (userWallet is UserWallet.Hot) { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) - hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWalletId, false) appRouter.push(UpgradeWallet(userWalletId)) } } @@ -565,15 +563,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() if (userWallet is UserWallet.Hot) { - val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWalletId) - val currentTime = System.currentTimeMillis() - - if (hasHadFirstTopUp) { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) - hotWalletRepository.setUpgradeBannerClosureTimestamp(userWalletId, currentTime) - } else { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, true) - } + closeHotWalletUpgradeBannerUseCase(userWalletId) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 6f8e6b1be6..6218c6257a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -13,8 +13,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase -import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency @@ -44,7 +45,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import java.util.concurrent.TimeUnit + import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -60,7 +61,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, - private val hotWalletRepository: HotWalletRepository, + private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") @@ -101,8 +103,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) .distinctUntilChanged(), - hotWalletRepository.shouldShowUpgradeBanner(userWallet.walletId).distinctUntilChanged(), - hotWalletRepository.shouldShowNextTimeUpgradeBanner(userWallet.walletId).distinctUntilChanged(), + shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } .map { array -> @@ -117,7 +119,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean - val shouldShowNextTimeUpgradeBanner = array[9] as Boolean buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -129,7 +130,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldShowUpgradeBanner = shouldShowUpgradeBanner, - shouldShowNextTimeUpgradeBanner = shouldShowNextTimeUpgradeBanner, ) addFinishWalletActivationNotification( @@ -464,45 +464,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - @Suppress("CyclomaticComplexMethod") private suspend fun MutableList.addUpgradeHotWalletPromoNotification( userWallet: UserWallet, flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, - shouldShowNextTimeUpgradeBanner: Boolean, ) { if (userWallet !is UserWallet.Hot) return - val currentTime = System.currentTimeMillis() - val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(userWallet.walletId) - val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(userWallet.walletId) - val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWallet.walletId) - - if (creationTimestamp == null) { - hotWalletRepository.setWalletCreationTimestamp(userWallet.walletId, currentTime) - return - } - - val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestamp) - val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } - val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } - if (hasBalance && !hasHadFirstTopUp) { - hotWalletRepository.setHasHadFirstTopUp(userWallet.walletId, true) - } - - val shouldShow = when { - shouldShowUpgradeBanner && hasBalance -> true - shouldShowNextTimeUpgradeBanner && daysSinceClosure != null && daysSinceClosure >= UPGRADE_BANNER_RESHOW_DAYS -> true - !shouldShowUpgradeBanner && !shouldShowNextTimeUpgradeBanner && hasHadFirstTopUp && daysSinceCreation >= UPGRADE_BANNER_RESHOW_DAYS -> { - hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWallet.walletId, true) - true - } - else -> false - } + val shouldShow = checkHotWalletUpgradeBannerUseCase( + walletId = userWallet.walletId, + hasBalance = hasBalance, + shouldShowUpgradeBanner = shouldShowUpgradeBanner, + ).getOrNull() ?: return addIf( element = WalletNotification.UpgradeHotWalletPromo( @@ -515,6 +492,5 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 - const val UPGRADE_BANNER_RESHOW_DAYS = 30 } } \ No newline at end of file