Updated on 2026-08-14
This commit is contained in:
parent
fb08f7c4e1
commit
4860b1fd8b
10 changed files with 425 additions and 63 deletions
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
|
||||
|
|
@ -7,6 +8,8 @@ import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificati
|
|||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificationPreferenceUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
|
||||
import com.tangem.domain.wallets.usecase.ApplyPushNotificationFirstActivationUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -64,4 +67,18 @@ internal object PushNotificationPreferencesDomainModule {
|
|||
): MarkPushNotificationFirstActivationDoneUseCase {
|
||||
return MarkPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesApplyPushNotificationFirstActivationUseCase(
|
||||
setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
notificationsRepository: NotificationsRepository,
|
||||
): ApplyPushNotificationFirstActivationUseCase {
|
||||
return ApplyPushNotificationFirstActivationUseCase(
|
||||
setNotificationsEnabledUseCase = setNotificationsEnabledUseCase,
|
||||
preferencesRepository = repository,
|
||||
notificationsRepository = notificationsRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ dependencies {
|
|||
/* Project - Domain */
|
||||
api(projects.domain.pushNotificationPreferences)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.notifications)
|
||||
implementation(projects.domain.wallets)
|
||||
|
||||
/* AndroidX */
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
|
||||
|
|
@ -23,6 +24,7 @@ import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificatio
|
|||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory
|
||||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference
|
||||
import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences
|
||||
import com.tangem.domain.wallets.usecase.ApplyPushNotificationFirstActivationUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
|
||||
|
|
@ -50,6 +52,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -67,6 +70,8 @@ internal class PushNotificationSettingsModel @Inject constructor(
|
|||
private val setNotificationsEnabled: SetNotificationsEnabledUseCase,
|
||||
private val isFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase,
|
||||
private val markFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase,
|
||||
private val applyFirstActivation: ApplyPushNotificationFirstActivationUseCase,
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
) : Model() {
|
||||
|
||||
private val params: PushNotificationSettingsComponent.Params = paramsContainer.require()
|
||||
|
|
@ -76,6 +81,7 @@ internal class PushNotificationSettingsModel @Inject constructor(
|
|||
private val osNotificationsEnabled = MutableStateFlow(systemNotificationsStateProvider.areNotificationsEnabled())
|
||||
|
||||
private var pendingPermissionToggle: ToggleSpec? = null
|
||||
private val wasAutoActivationAttempted = AtomicBoolean(false)
|
||||
private val preferencesJobHolder = JobHolder()
|
||||
|
||||
private val cachedPrefs: WalletPushNotificationPreferences?
|
||||
|
|
@ -122,6 +128,7 @@ internal class PushNotificationSettingsModel @Inject constructor(
|
|||
|
||||
fun onResume() {
|
||||
osNotificationsEnabled.value = systemNotificationsStateProvider.areNotificationsEnabled()
|
||||
if (cachedPrefs != null) autoApplyFirstActivationIfNeeded()
|
||||
}
|
||||
|
||||
fun onPermissionResult(isGranted: Boolean) {
|
||||
|
|
@ -156,11 +163,34 @@ internal class PushNotificationSettingsModel @Inject constructor(
|
|||
// Fall to Failed only when nothing is cached yet; otherwise keep showing the last value.
|
||||
if (loadState.value !is LoadState.Content) loadState.value = LoadState.Failed
|
||||
}
|
||||
.onEach { value -> loadState.value = LoadState.Content(value) }
|
||||
.onEach { value ->
|
||||
loadState.value = LoadState.Content(value)
|
||||
autoApplyFirstActivationIfNeeded()
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.saveIn(preferencesJobHolder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Silently re-applies the first-activation rule once preferences are loaded: the grant-time attempt
|
||||
|
||||
*/
|
||||
private fun autoApplyFirstActivationIfNeeded() {
|
||||
if (!wasAutoActivationAttempted.compareAndSet(false, true)) return
|
||||
modelScope.launch(dispatchers.io) {
|
||||
val areGatesPassed = osNotificationsEnabled.value &&
|
||||
notificationsRepository.isUserAllowToSubscribeOnPushNotifications()
|
||||
if (areGatesPassed) {
|
||||
// Deliberately not retried on failure within this screen instance: a retry fired by the
|
||||
// cache echo of a manual toggle write would force-enable all three against the user's choice.
|
||||
applyFirstActivation(userWalletId)
|
||||
} else {
|
||||
// A gate miss is not an attempt — onResume may retry after the OS state changes.
|
||||
wasAutoActivationAttempted.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildContent(
|
||||
prefs: WalletPushNotificationPreferences,
|
||||
osEnabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.tangem.core.decompose.ui.UiMessageSender
|
|||
import com.tangem.core.navigation.notifications.SystemNotificationsStateProvider
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
|
||||
|
|
@ -17,6 +18,7 @@ import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificatio
|
|||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory
|
||||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference
|
||||
import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences
|
||||
import com.tangem.domain.wallets.usecase.ApplyPushNotificationFirstActivationUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
|
||||
import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
|
||||
|
|
@ -45,6 +47,8 @@ class PushNotificationSettingsModelTest {
|
|||
private val setNotificationsEnabled: SetNotificationsEnabledUseCase = mockk()
|
||||
private val isFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase = mockk()
|
||||
private val markFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase = mockk(relaxed = true)
|
||||
private val applyFirstActivation: ApplyPushNotificationFirstActivationUseCase = mockk()
|
||||
private val notificationsRepository: NotificationsRepository = mockk()
|
||||
private val systemNotificationsStateProvider: SystemNotificationsStateProvider = mockk()
|
||||
private val settingsManager: SettingsManager = mockk(relaxed = true)
|
||||
private val messageSender: UiMessageSender = mockk(relaxed = true)
|
||||
|
|
@ -53,6 +57,8 @@ class PushNotificationSettingsModelTest {
|
|||
private fun model(
|
||||
osEnabled: Boolean = true,
|
||||
firstActivationDone: Boolean = true,
|
||||
consentGiven: Boolean = false,
|
||||
activationResult: Either<Throwable, Unit> = Either.Right(Unit),
|
||||
preferencesFlow: MutableSharedFlow<WalletPushNotificationPreferences> = MutableSharedFlow(replay = 1),
|
||||
): PushNotificationSettingsModel {
|
||||
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns osEnabled
|
||||
|
|
@ -60,6 +66,8 @@ class PushNotificationSettingsModelTest {
|
|||
coEvery { isFirstActivationDone(userWalletId) } returns firstActivationDone
|
||||
coEvery { setNotificationsEnabled(any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { setAllPreferences(any(), any(), any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { notificationsRepository.isUserAllowToSubscribeOnPushNotifications() } returns consentGiven
|
||||
coEvery { applyFirstActivation(any()) } returns activationResult
|
||||
return PushNotificationSettingsModel(
|
||||
paramsContainer = MutableParamsContainer(PushNotificationSettingsComponent.Params(userWalletId)),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
|
|
@ -73,6 +81,8 @@ class PushNotificationSettingsModelTest {
|
|||
setNotificationsEnabled = setNotificationsEnabled,
|
||||
isFirstActivationDone = isFirstActivationDone,
|
||||
markFirstActivationDone = markFirstActivationDone,
|
||||
applyFirstActivation = applyFirstActivation,
|
||||
notificationsRepository = notificationsRepository,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +105,8 @@ class PushNotificationSettingsModelTest {
|
|||
coEvery { isFirstActivationDone(userWalletId) } returns true
|
||||
coEvery { setNotificationsEnabled(any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { setAllPreferences(any(), any(), any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { notificationsRepository.isUserAllowToSubscribeOnPushNotifications() } returns false
|
||||
coEvery { applyFirstActivation(any()) } returns Either.Right(Unit)
|
||||
|
||||
val model = PushNotificationSettingsModel(
|
||||
paramsContainer = MutableParamsContainer(PushNotificationSettingsComponent.Params(userWalletId)),
|
||||
|
|
@ -109,6 +121,8 @@ class PushNotificationSettingsModelTest {
|
|||
setNotificationsEnabled = setNotificationsEnabled,
|
||||
isFirstActivationDone = isFirstActivationDone,
|
||||
markFirstActivationDone = markFirstActivationDone,
|
||||
applyFirstActivation = applyFirstActivation,
|
||||
notificationsRepository = notificationsRepository,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -469,6 +483,102 @@ class PushNotificationSettingsModelTest {
|
|||
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN consent and OS enabled WHEN preferences loaded THEN first activation reapplied`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
|
||||
// Act
|
||||
model(osEnabled = true, consentGiven = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { applyFirstActivation(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN preferences emitted twice WHEN loaded THEN first activation attempted once`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
|
||||
// Act
|
||||
model(osEnabled = true, consentGiven = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
flow.tryEmit(anyOn())
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { applyFirstActivation(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no consent WHEN preferences loaded THEN first activation not reapplied`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
|
||||
// Act
|
||||
model(osEnabled = true, consentGiven = false, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { applyFirstActivation(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN OS disabled WHEN preferences loaded THEN first activation not reapplied`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
|
||||
// Act
|
||||
model(osEnabled = false, consentGiven = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { applyFirstActivation(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation failed WHEN preferences emitted again THEN not retried`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
|
||||
// Act
|
||||
model(
|
||||
osEnabled = true,
|
||||
consentGiven = true,
|
||||
activationResult = Either.Left(RuntimeException("net")),
|
||||
preferencesFlow = flow,
|
||||
)
|
||||
advanceUntilIdle()
|
||||
flow.tryEmit(anyOn())
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { applyFirstActivation(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN OS enabled after pause WHEN onResume THEN first activation reapplied`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
val model = model(osEnabled = false, consentGiven = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act: the user enabled notifications in the OS settings and returned to the screen.
|
||||
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
|
||||
model.onResume()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { applyFirstActivation(userWalletId) }
|
||||
}
|
||||
|
||||
private fun allFalse() = WalletPushNotificationPreferences(
|
||||
transactionAlerts = PushNotificationPreference(isEnabled = false),
|
||||
offersUpdates = PushNotificationPreference(isEnabled = false),
|
||||
|
|
|
|||
|
|
@ -5,18 +5,15 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import arrow.core.Either
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import com.tangem.domain.wallets.usecase.ApplyPushNotificationFirstActivationUseCase
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
|
|
@ -42,10 +39,8 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
private val analyticHandler: AnalyticsEventHandler,
|
||||
private val notificationsRepository: NotificationsRepository,
|
||||
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
|
||||
private val setAllWalletPushNotificationPreferences: SetAllWalletPushNotificationPreferencesUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
|
||||
private val isPushNotificationFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase,
|
||||
private val applyPushNotificationFirstActivation: ApplyPushNotificationFirstActivationUseCase,
|
||||
private val markPushNotificationFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase,
|
||||
private val getPushNotificationsDoubleAskVariantUseCase: GetPushNotificationsDoubleAskVariantUseCase,
|
||||
) : Model(), PushNotificationsClickIntents {
|
||||
|
|
@ -162,20 +157,7 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
/** On the first grant, enable all three categories for every not-yet-activated wallet (guarded once per wallet). */
|
||||
private suspend fun applyFirstActivationRule() {
|
||||
userWalletsListRepository.userWalletsSync().forEach { wallet ->
|
||||
if (isPushNotificationFirstActivationDone(wallet.walletId)) return@forEach
|
||||
|
||||
// Tokens (address re-subscription) first, then preferences; mark only on success, undo tokens on failure.
|
||||
val tokensResult = setNotificationsEnabledUseCase(wallet.walletId, isEnabled = true)
|
||||
if (tokensResult is Either.Left) return@forEach
|
||||
|
||||
setAllWalletPushNotificationPreferences(
|
||||
userWalletId = wallet.walletId,
|
||||
transactionAlerts = true,
|
||||
offersUpdates = true,
|
||||
priceAlerts = true,
|
||||
)
|
||||
.onRight { markPushNotificationFirstActivationDone(wallet.walletId) }
|
||||
.onLeft { setNotificationsEnabledUseCase(wallet.walletId, isEnabled = false) }
|
||||
applyPushNotificationFirstActivation(wallet.walletId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,12 +11,10 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository
|
|||
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.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import com.tangem.domain.wallets.usecase.ApplyPushNotificationFirstActivationUseCase
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
|
|
@ -47,12 +45,8 @@ internal class PushNotificationsModelTest {
|
|||
private val analyticHandler: AnalyticsEventHandler = mockk(relaxed = true)
|
||||
private val notificationsRepository: NotificationsRepository = mockk(relaxed = true)
|
||||
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles = mockk(relaxed = true)
|
||||
private val setAllWalletPushNotificationPreferences: SetAllWalletPushNotificationPreferencesUseCase =
|
||||
mockk(relaxed = true)
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxed = true)
|
||||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase = mockk(relaxed = true)
|
||||
private val isPushNotificationFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase =
|
||||
mockk(relaxed = true)
|
||||
private val applyPushNotificationFirstActivation: ApplyPushNotificationFirstActivationUseCase = mockk()
|
||||
private val markPushNotificationFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase =
|
||||
mockk(relaxed = true)
|
||||
private val getDoubleAskVariantUseCase: GetPushNotificationsDoubleAskVariantUseCase = mockk()
|
||||
|
|
@ -174,34 +168,43 @@ internal class PushNotificationsModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN settings enabled WHEN onAllowPermission THEN fresh wallets get all-three and done wallets skipped`() =
|
||||
runTest {
|
||||
fun `GIVEN settings enabled WHEN onAllowPermission THEN first activation applied for every wallet`() = runTest {
|
||||
every { pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled } returns true
|
||||
val fresh = UserWalletId("aa")
|
||||
val done = UserWalletId("bb")
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(fresh), wallet(done))
|
||||
coEvery { isPushNotificationFirstActivationDone(fresh) } returns false
|
||||
coEvery { isPushNotificationFirstActivationDone(done) } returns true
|
||||
coEvery { setNotificationsEnabledUseCase(any(), any()) } returns Either.Right(Unit)
|
||||
coEvery {
|
||||
setAllWalletPushNotificationPreferences(any(), any(), any(), any())
|
||||
} returns Either.Right(Unit)
|
||||
val w1 = UserWalletId("aa")
|
||||
val w2 = UserWalletId("bb")
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(w1), wallet(w2))
|
||||
coEvery { applyPushNotificationFirstActivation(any()) } returns Either.Right(Unit)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAllowPermission()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Fresh wallet: tokens (re-subscription) before preferences, then flag marked.
|
||||
coVerifyOrder {
|
||||
setNotificationsEnabledUseCase(fresh, isEnabled = true)
|
||||
setAllWalletPushNotificationPreferences(fresh, true, true, true)
|
||||
applyPushNotificationFirstActivation(w1)
|
||||
applyPushNotificationFirstActivation(w2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN activation fails for first wallet WHEN onAllowPermission THEN second wallet still attempted`() =
|
||||
runTest {
|
||||
every { pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled } returns true
|
||||
val w1 = UserWalletId("aa")
|
||||
val w2 = UserWalletId("bb")
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(w1), wallet(w2))
|
||||
coEvery { applyPushNotificationFirstActivation(w1) } returns Either.Left(RuntimeException("404"))
|
||||
coEvery { applyPushNotificationFirstActivation(w2) } returns Either.Right(Unit)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAllowPermission()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerifyOrder {
|
||||
applyPushNotificationFirstActivation(w1)
|
||||
applyPushNotificationFirstActivation(w2)
|
||||
}
|
||||
coVerify(exactly = 1) { markPushNotificationFirstActivationDone(fresh) }
|
||||
// Already-activated wallet: skipped entirely.
|
||||
coVerify(exactly = 0) { setAllWalletPushNotificationPreferences(done, any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setNotificationsEnabledUseCase(done, any()) }
|
||||
coVerify(exactly = 0) { markPushNotificationFirstActivationDone(done) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -218,28 +221,23 @@ internal class PushNotificationsModelTest {
|
|||
|
||||
coVerify(exactly = 1) { markPushNotificationFirstActivationDone(w1) }
|
||||
coVerify(exactly = 1) { markPushNotificationFirstActivationDone(w2) }
|
||||
// Deny only fixes the flag — it must not enable any category or re-subscribe tokens.
|
||||
coVerify(exactly = 0) { setAllWalletPushNotificationPreferences(any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
|
||||
// Deny only fixes the flag — it must not enable any category.
|
||||
coVerify(exactly = 0) { applyPushNotificationFirstActivation(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN settings disabled WHEN onAllowPermission THEN no first-activation writes`() = runTest {
|
||||
every { pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled } returns false
|
||||
// Non-empty wallet list + not-done + successful stubs so that, if the toggle gate were removed,
|
||||
// applyFirstActivationRule WOULD iterate and write — making the exactly=0 assertions actually protect the gate.
|
||||
// Non-empty wallet list + successful stubs, so the exactly=0 assertions actually protect the toggle gate.
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(UserWalletId("aa")))
|
||||
coEvery { isPushNotificationFirstActivationDone(any()) } returns false
|
||||
coEvery { setNotificationsEnabledUseCase(any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { setAllWalletPushNotificationPreferences(any(), any(), any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { applyPushNotificationFirstActivation(any()) } returns Either.Right(Unit)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAllowPermission()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { setAllWalletPushNotificationPreferences(any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
|
||||
coVerify(exactly = 0) { applyPushNotificationFirstActivation(any()) }
|
||||
coVerify(exactly = 0) { markPushNotificationFirstActivationDone(any()) }
|
||||
}
|
||||
|
||||
|
|
@ -267,10 +265,8 @@ internal class PushNotificationsModelTest {
|
|||
analyticHandler = analyticHandler,
|
||||
notificationsRepository = notificationsRepository,
|
||||
pushNotificationSettingsFeatureToggles = pushNotificationSettingsFeatureToggles,
|
||||
setAllWalletPushNotificationPreferences = setAllWalletPushNotificationPreferences,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
setNotificationsEnabledUseCase = setNotificationsEnabledUseCase,
|
||||
isPushNotificationFirstActivationDone = isPushNotificationFirstActivationDone,
|
||||
applyPushNotificationFirstActivation = applyPushNotificationFirstActivation,
|
||||
markPushNotificationFirstActivationDone = markPushNotificationFirstActivationDone,
|
||||
getPushNotificationsDoubleAskVariantUseCase = getDoubleAskVariantUseCase,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.core.analytics.utils.TrackingContextProxy
|
|||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.notifications.SystemNotificationsStateProvider
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
|
|
@ -107,6 +108,8 @@ internal class WalletModel @Inject constructor(
|
|||
private val notificationsRepository: NotificationsRepository,
|
||||
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
|
||||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
|
||||
private val applyPushNotificationFirstActivationUseCase: ApplyPushNotificationFirstActivationUseCase,
|
||||
private val systemNotificationsStateProvider: SystemNotificationsStateProvider,
|
||||
private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase,
|
||||
|
|
@ -887,8 +890,10 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun enableNotificationsIfNeeded() {
|
||||
// New first-activation owns auto-enable when the feature is on; skip the legacy path.
|
||||
if (pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled) return
|
||||
if (pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled) {
|
||||
applyPushFirstActivationIfNeeded()
|
||||
return
|
||||
}
|
||||
modelScope.launch {
|
||||
val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications()
|
||||
if (isUserAllowToEnableNotifications) {
|
||||
|
|
@ -907,6 +912,20 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
* on the backend (onboarding). See [ApplyPushNotificationFirstActivationUseCase].
|
||||
*/
|
||||
private fun applyPushFirstActivationIfNeeded() {
|
||||
modelScope.launch {
|
||||
if (!systemNotificationsStateProvider.areNotificationsEnabled()) return@launch
|
||||
if (!notificationsRepository.isUserAllowToSubscribeOnPushNotifications()) return@launch
|
||||
userWalletsListRepository.userWalletsSync().forEach { wallet ->
|
||||
applyPushNotificationFirstActivationUseCase(wallet.walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
|
||||
override fun onAllowed() {
|
||||
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue