Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-19 13:00:40 +05:00
parent 4e4e360ae1
commit 9adbde6e55
12 changed files with 454 additions and 75 deletions

View file

@ -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)
}

View file

@ -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<Throwable, Boolean> = 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
}
}

View file

@ -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<Throwable, Unit> = try {
val currentTime = System.currentTimeMillis()
hotWalletRepository.setShouldShowUpgradeBanner(walletId, false)
hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, currentTime)
Unit.right()
} catch (e: Exception) {
e.left()
}
}

View file

@ -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<Boolean> =
hotWalletRepository.shouldShowUpgradeBanner(userWalletId)
}

View file

@ -17,13 +17,9 @@ interface HotWalletRepository {
suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean)
fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow<Boolean>
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)
}

View file

@ -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()
}
}

View file

@ -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)
}
}