Updated on 2026-08-14
This commit is contained in:
parent
4e4e360ae1
commit
9adbde6e55
12 changed files with 454 additions and 75 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue