Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-28 16:22:08 +03:00
parent fb08f7c4e1
commit 4860b1fd8b
10 changed files with 425 additions and 63 deletions

View file

@ -41,6 +41,8 @@ dependencies {
api(projects.domain.walletManager)
api(projects.domain.core)
implementation(projects.domain.legacy)
implementation(projects.domain.notifications)
implementation(projects.domain.pushNotificationPreferences)
// endregion
// region Domain models

View file

@ -0,0 +1,55 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.flow.first
/**
* First-activation rule: on the first push permission grant, enables all three categories for a wallet.
* on the backend yet) is retried by the next trigger wallet screen or notification settings screen.
*/
class ApplyPushNotificationFirstActivationUseCase(
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
private val preferencesRepository: WalletPushNotificationPreferencesRepository,
private val notificationsRepository: NotificationsRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
return applyRule(userWalletId).onLeft {
TangemLogger.e("Push first activation failed for $userWalletId", it)
}
}
private suspend fun applyRule(userWalletId: UserWalletId): Either<Throwable, Unit> {
if (preferencesRepository.isFirstActivationDone(userWalletId)) return Unit.right()
// Wallets auto-enabled by the legacy flow are adopted as activated to not force-enable extra categories.
if (userWalletId.stringValue in notificationsRepository.getWalletAutomaticallyEnabledList()) {
preferencesRepository.markFirstActivationDone(userWalletId)
return Unit.right()
}
// The wallet may not be created on the backend yet (onboarding) — probe before any side effects.
val probe = Either.catch { preferencesRepository.observePreferences(userWalletId).first() }
if (probe is Either.Left) return probe.value.left()
// Tokens (address re-subscription) first, then preferences.
val tokensResult = setNotificationsEnabledUseCase(userWalletId, isEnabled = true)
if (tokensResult is Either.Left) return tokensResult
return preferencesRepository.setAllPreferences(
userWalletId = userWalletId,
transactionAlerts = true,
offersUpdates = true,
priceAlerts = true,
)
.onRight { preferencesRepository.markFirstActivationDone(userWalletId) }
.onLeft { setNotificationsEnabledUseCase(userWalletId, isEnabled = false) }
}
}

View file

@ -0,0 +1,150 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.notifications.repository.NotificationsRepository
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference
import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class ApplyPushNotificationFirstActivationUseCaseTest {
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase = mockk()
private val preferencesRepository: WalletPushNotificationPreferencesRepository = mockk()
private val notificationsRepository: NotificationsRepository = mockk()
private val useCase = ApplyPushNotificationFirstActivationUseCase(
setNotificationsEnabledUseCase = setNotificationsEnabledUseCase,
preferencesRepository = preferencesRepository,
notificationsRepository = notificationsRepository,
)
private val userWalletId = UserWalletId("0A0B0C0D")
@BeforeEach
fun resetMocks() {
clearMocks(setNotificationsEnabledUseCase, preferencesRepository, notificationsRepository)
coEvery { preferencesRepository.isFirstActivationDone(userWalletId) } returns false
coEvery { preferencesRepository.markFirstActivationDone(userWalletId) } just runs
coEvery { notificationsRepository.getWalletAutomaticallyEnabledList() } returns emptyList()
coEvery { preferencesRepository.observePreferences(userWalletId) } returns flowOf(allFalse())
coEvery { setNotificationsEnabledUseCase(userWalletId, any()) } returns Either.Right(Unit)
coEvery {
preferencesRepository.setAllPreferences(userWalletId, any(), any(), any())
} returns Either.Right(Unit)
}
@Test
fun `GIVEN activation already done WHEN invoke THEN returns Right without any writes`() = runTest {
// Arrange
coEvery { preferencesRepository.isFirstActivationDone(userWalletId) } returns true
// Act
val result = useCase(userWalletId)
// Assert
assertThat(result.isRight()).isTrue()
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
coVerify(exactly = 0) { preferencesRepository.setAllPreferences(any(), any(), any(), any()) }
coVerify(exactly = 0) { preferencesRepository.markFirstActivationDone(any()) }
}
@Test
fun `GIVEN wallet enabled by legacy flow WHEN invoke THEN flag adopted without enabling categories`() = runTest {
// Arrange
coEvery { notificationsRepository.getWalletAutomaticallyEnabledList() } returns
listOf(userWalletId.stringValue)
// Act
val result = useCase(userWalletId)
// Assert
assertThat(result.isRight()).isTrue()
coVerify(exactly = 1) { preferencesRepository.markFirstActivationDone(userWalletId) }
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
coVerify(exactly = 0) { preferencesRepository.setAllPreferences(any(), any(), any(), any()) }
}
@Test
fun `GIVEN preferences unavailable WHEN invoke THEN returns Left without side effects`() = runTest {
// Arrange: the wallet is not created on the backend yet (e.g. right after onboarding).
coEvery { preferencesRepository.observePreferences(userWalletId) } returns
flow { throw RuntimeException("404") }
// Act
val result = useCase(userWalletId)
// Assert
assertThat(result.isLeft()).isTrue()
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
coVerify(exactly = 0) { preferencesRepository.setAllPreferences(any(), any(), any(), any()) }
coVerify(exactly = 0) { preferencesRepository.markFirstActivationDone(any()) }
}
@Test
fun `GIVEN token subscription fails WHEN invoke THEN returns Left and preferences not written`() = runTest {
// Arrange
coEvery { setNotificationsEnabledUseCase(userWalletId, isEnabled = true) } returns
Either.Left(RuntimeException("net"))
// Act
val result = useCase(userWalletId)
// Assert
assertThat(result.isLeft()).isTrue()
coVerify(exactly = 0) { preferencesRepository.setAllPreferences(any(), any(), any(), any()) }
coVerify(exactly = 0) { preferencesRepository.markFirstActivationDone(any()) }
}
@Test
fun `GIVEN preferences write fails WHEN invoke THEN tokens undone and flag not marked`() = runTest {
// Arrange
coEvery { preferencesRepository.setAllPreferences(userWalletId, true, true, true) } returns
Either.Left(RuntimeException("net"))
// Act
val result = useCase(userWalletId)
// Assert
assertThat(result.isLeft()).isTrue()
coVerifyOrder {
setNotificationsEnabledUseCase(userWalletId, isEnabled = true)
preferencesRepository.setAllPreferences(userWalletId, true, true, true)
setNotificationsEnabledUseCase(userWalletId, isEnabled = false)
}
coVerify(exactly = 0) { preferencesRepository.markFirstActivationDone(any()) }
}
@Test
fun `GIVEN whole chain succeeds WHEN invoke THEN all three enabled and flag marked`() = runTest {
// Act
val result = useCase(userWalletId)
// Assert
assertThat(result.isRight()).isTrue()
coVerifyOrder {
setNotificationsEnabledUseCase(userWalletId, isEnabled = true)
preferencesRepository.setAllPreferences(userWalletId, true, true, true)
preferencesRepository.markFirstActivationDone(userWalletId)
}
}
private fun allFalse() = WalletPushNotificationPreferences(
transactionAlerts = PushNotificationPreference(isEnabled = false),
offersUpdates = PushNotificationPreference(isEnabled = false),
priceAlerts = PushNotificationPreference(isEnabled = false),
)
}