Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-08 19:28:17 +03:00
parent 8af068b74b
commit fc319f2b70
17 changed files with 518 additions and 68 deletions

View file

@ -27,9 +27,9 @@ dependencies {
implementation(projects.core.ui)
/* Project - Domain */
api(projects.domain.account)
api(projects.domain.pushNotificationPreferences)
implementation(projects.domain.models)
implementation(projects.domain.wallets)
/* AndroidX */
implementation(deps.androidx.activity)

View file

@ -1,5 +1,6 @@
package com.tangem.features.pushnotificationsettings.impl.model
import arrow.core.Either
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -13,13 +14,16 @@ import com.tangem.core.ui.extensions.TextReference
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.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificationPreferenceUseCase
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.SetNotificationsEnabledUseCase
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
import com.tangem.features.pushnotificationsettings.impl.R
@ -30,9 +34,7 @@ import com.tangem.features.pushnotificationsettings.impl.entity.ToggleId
import com.tangem.features.pushnotificationsettings.impl.entity.ToggleUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.runSuspendCatching
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.channels.Channel
@ -61,7 +63,10 @@ internal class PushNotificationSettingsModel @Inject constructor(
private val updatePreference: UpdateWalletPushNotificationPreferenceUseCase,
private val systemNotificationsStateProvider: SystemNotificationsStateProvider,
private val settingsManager: SettingsManager,
private val accountsCRUDRepository: AccountsCRUDRepository,
private val setAllPreferences: SetAllWalletPushNotificationPreferencesUseCase,
private val setNotificationsEnabled: SetNotificationsEnabledUseCase,
private val isFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase,
private val markFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase,
) : Model() {
private val params: PushNotificationSettingsComponent.Params = paramsContainer.require()
@ -123,12 +128,24 @@ internal class PushNotificationSettingsModel @Inject constructor(
val tapped = pendingPermissionToggle
pendingPermissionToggle = null
modelScope.launch {
osNotificationsEnabled.value = systemNotificationsStateProvider.areNotificationsEnabled()
// Enable all three only on a real grant that actually turned notifications on: an
// already-granted-but-OS-disabled wallet returns isGranted=true instantly, so also require
// areNotificationsEnabled() before triggering the rule; a deny routes the user to settings.
val isNotificationsEnabled = systemNotificationsStateProvider.areNotificationsEnabled()
osNotificationsEnabled.value = isNotificationsEnabled
analyticsEventHandler.send(PushNotificationAnalyticEvents.PermissionStatus(isAllowed = isGranted))
if (isGranted && tapped != null) {
applyOptimisticToggle(tapped, newValue = true)
} else if (!isGranted) {
if (!isGranted || !isNotificationsEnabled) {
markFirstActivationDone(userWalletId)
showEnableNotificationsDialog()
return@launch
}
if (isFirstActivationDone(userWalletId)) {
tapped?.let { applyOptimisticToggle(it, newValue = true) }
} else if (enableAllCategories(initiatingToggle = tapped)) {
// Fix the flag only after a successful enable, so a transient failure can retry.
markFirstActivationDone(userWalletId)
}
}
}
@ -228,23 +245,54 @@ internal class PushNotificationSettingsModel @Inject constructor(
}
private suspend fun writeToggle(spec: ToggleSpec, newValue: Boolean) {
// TODO [REDACTED_TASK_KEY] figure out and maybe swap /tokens and /preferences further calls
updatePreference(userWalletId, spec.category, newValue)
.onRight {
if (spec.category == PushNotificationCategory.TransactionAlerts) {
// Best-effort token sync after the preference write already succeeded:
// log a failure but don't surface it to the user or revert the toggle.
runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) }
.onFailure { error ->
TangemLogger.e(
messageString = "Failed to sync tokens after enabling " +
"transaction alerts for $userWalletId",
throwable = error,
)
}
}
}
.onLeft { revertOptimistic(spec, newValue) }
if (spec.category == PushNotificationCategory.TransactionAlerts) {
writeTransactionAlerts(spec, newValue)
} else {
updatePreference(userWalletId, spec.category, newValue).onLeft { revertOptimistic(spec, newValue) }
}
}
/** Tokens (address re-subscription) first, then preferences; on failure revert and undo the token subscription. */
private suspend fun writeTransactionAlerts(spec: ToggleSpec, newValue: Boolean) {
val tokensResult = setNotificationsEnabled(userWalletId, isEnabled = newValue)
if (tokensResult is Either.Left) {
revertOptimistic(spec, newValue)
return
}
updatePreference(userWalletId, spec.category, newValue).onLeft {
setNotificationsEnabled(userWalletId, isEnabled = !newValue)
revertOptimistic(spec, newValue)
}
}
/** Enables all three categories at once (first-activation): tokens first, then preferences. True on full success. */
private suspend fun enableAllCategories(initiatingToggle: ToggleSpec?): Boolean {
val previous = cachedPrefs ?: return false
loadState.value = LoadState.Content(
previous.copy(
transactionAlerts = previous.transactionAlerts.copy(isEnabled = true),
offersUpdates = previous.offersUpdates.copy(isEnabled = true),
priceAlerts = previous.priceAlerts.copy(isEnabled = true),
),
)
val tokensResult = setNotificationsEnabled(userWalletId, isEnabled = true)
if (tokensResult is Either.Left) {
revertAll(previous, initiatingToggle)
return false
}
return setAllPreferences(
userWalletId = userWalletId,
transactionAlerts = true,
offersUpdates = true,
priceAlerts = true,
).fold(
ifLeft = {
setNotificationsEnabled(userWalletId, isEnabled = false)
revertAll(previous, initiatingToggle)
false
},
ifRight = { true },
)
}
private fun revertOptimistic(spec: ToggleSpec, newValue: Boolean) {
@ -257,9 +305,18 @@ internal class PushNotificationSettingsModel @Inject constructor(
state
}
}
showWriteErrorMessage(toggleType = spec.id.analyticsValue)
}
private fun revertAll(previous: WalletPushNotificationPreferences, initiatingToggle: ToggleSpec?) {
loadState.update { state -> if (state is LoadState.Content) LoadState.Content(previous) else state }
showWriteErrorMessage(toggleType = (initiatingToggle?.id ?: ToggleId.TransactionAlerts).analyticsValue)
}
private fun showWriteErrorMessage(toggleType: String) {
analyticsEventHandler.send(
PushNotificationAnalyticEvents.NotificationSettingsErrorShown(
toggleType = spec.id.analyticsValue,
toggleType = toggleType,
errorType = ERROR_TYPE_WRITE_FAILED,
),
)

View file

@ -8,20 +8,23 @@ import com.tangem.core.decompose.model.MutableParamsContainer
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.account.repository.AccountsCRUDRepository
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificationPreferenceUseCase
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.SetNotificationsEnabledUseCase
import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
import com.tangem.features.pushnotificationsettings.impl.entity.AllowPushNotificationsBannerUM
import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
import com.tangem.features.pushnotificationsettings.impl.entity.ToggleId
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
@ -31,25 +34,32 @@ import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
class PushNotificationSettingsModelTest {
private val userWalletId = UserWalletId("0011223344556677")
private val observePreferences: ObserveWalletPushNotificationPreferencesUseCase = mockk()
private val updatePreference: UpdateWalletPushNotificationPreferenceUseCase = mockk()
private val setAllPreferences: SetAllWalletPushNotificationPreferencesUseCase = mockk()
private val setNotificationsEnabled: SetNotificationsEnabledUseCase = mockk()
private val isFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase = mockk()
private val markFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase = mockk(relaxed = true)
private val systemNotificationsStateProvider: SystemNotificationsStateProvider = mockk()
private val settingsManager: SettingsManager = mockk(relaxed = true)
private val accountsCRUDRepository: AccountsCRUDRepository = mockk(relaxed = true)
private val messageSender: UiMessageSender = mockk(relaxed = true)
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private fun model(
osEnabled: Boolean = true,
firstActivationDone: Boolean = true,
preferencesFlow: MutableSharedFlow<WalletPushNotificationPreferences> = MutableSharedFlow(replay = 1),
): PushNotificationSettingsModel {
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns osEnabled
every { observePreferences(userWalletId) } returns preferencesFlow
coEvery { isFirstActivationDone(userWalletId) } returns firstActivationDone
coEvery { setNotificationsEnabled(any(), any()) } returns Either.Right(Unit)
coEvery { setAllPreferences(any(), any(), any(), any()) } returns Either.Right(Unit)
return PushNotificationSettingsModel(
paramsContainer = MutableParamsContainer(PushNotificationSettingsComponent.Params(userWalletId)),
dispatchers = TestingCoroutineDispatcherProvider(),
@ -59,7 +69,10 @@ class PushNotificationSettingsModelTest {
updatePreference = updatePreference,
systemNotificationsStateProvider = systemNotificationsStateProvider,
settingsManager = settingsManager,
accountsCRUDRepository = accountsCRUDRepository,
setAllPreferences = setAllPreferences,
setNotificationsEnabled = setNotificationsEnabled,
isFirstActivationDone = isFirstActivationDone,
markFirstActivationDone = markFirstActivationDone,
)
}
@ -79,6 +92,9 @@ class PushNotificationSettingsModelTest {
fun `GIVEN observe throws WHEN model created THEN ui state becomes Error`() = runTest {
every { observePreferences(userWalletId) } returns flow { throw IllegalStateException("boom") }
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
coEvery { isFirstActivationDone(userWalletId) } returns true
coEvery { setNotificationsEnabled(any(), any()) } returns Either.Right(Unit)
coEvery { setAllPreferences(any(), any(), any(), any()) } returns Either.Right(Unit)
val model = PushNotificationSettingsModel(
paramsContainer = MutableParamsContainer(PushNotificationSettingsComponent.Params(userWalletId)),
@ -89,7 +105,10 @@ class PushNotificationSettingsModelTest {
updatePreference = updatePreference,
systemNotificationsStateProvider = systemNotificationsStateProvider,
settingsManager = settingsManager,
accountsCRUDRepository = accountsCRUDRepository,
setAllPreferences = setAllPreferences,
setNotificationsEnabled = setNotificationsEnabled,
isFirstActivationDone = isFirstActivationDone,
markFirstActivationDone = markFirstActivationDone,
)
advanceUntilIdle()
@ -197,6 +216,28 @@ class PushNotificationSettingsModelTest {
assertThat(toggles.first { it.id == ToggleId.OffersUpdates }.isOn).isTrue()
}
@Test
fun `GIVEN transaction alerts on WHEN written THEN tokens synced before preference`() = runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
coEvery {
updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, true)
} returns Either.Right(Unit)
val model = model(osEnabled = true, firstActivationDone = true, preferencesFlow = flow)
advanceUntilIdle()
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.TransactionAlerts }
.onCheckedChange(true)
advanceUntilIdle()
// Order matters: token re-subscription (which carries addresses) precedes the preferences write (spec §13.2).
coVerifyOrder {
setNotificationsEnabled(userWalletId, isEnabled = true)
updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, true)
}
}
@Test
fun `GIVEN OS disabled WHEN toggle ON THEN permission request is triggered`() = runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
@ -237,52 +278,195 @@ class PushNotificationSettingsModelTest {
}
@Test
fun `WHEN Allow on a single tapped toggle THEN only that toggle is enabled`() = runTest {
fun `GIVEN first activation not done WHEN Allow THEN all three categories enabled`() = runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
advanceUntilIdle()
// Tap a single toggle in not_determined state -> OS prompt fires.
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.OffersUpdates }
.onCheckedChange(true)
advanceUntilIdle()
// User taps Allow.
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
model.onPermissionResult(isGranted = true)
advanceUntilIdle()
// First-activation rule: all three enabled at once (not just the tapped one), token re-subscription, flag fixed.
coVerify(exactly = 1) { setAllPreferences(userWalletId, true, true, true) }
coVerify(exactly = 1) { setNotificationsEnabled(userWalletId, isEnabled = true) }
coVerify(exactly = 1) { markFirstActivationDone(userWalletId) }
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
}
@Test
fun `GIVEN first activation done WHEN Allow THEN only tapped toggle enabled`() = runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
coEvery {
updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true)
} returns Either.Right(Unit)
val model = model(osEnabled = false, preferencesFlow = flow)
val model = model(osEnabled = false, firstActivationDone = true, preferencesFlow = flow)
advanceUntilIdle()
val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.OffersUpdates }
offers.onCheckedChange(true)
.onCheckedChange(true)
advanceUntilIdle()
// OS prompt fires; user taps Allow.
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
model.onPermissionResult(isGranted = true)
advanceUntilIdle()
coVerify(exactly = 1) {
updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true)
}
coVerify(exactly = 0) {
updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, any())
}
coVerify(exactly = 0) {
updatePreference(userWalletId, PushNotificationCategory.PriceAlerts, any())
}
coVerify(exactly = 1) { updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) }
// Selective: exactly one preference write (the tapped one), no bulk enable, and the flag is already set.
coVerify(exactly = 1) { updatePreference(any(), any(), any()) }
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
coVerify(exactly = 0) { markFirstActivationDone(userWalletId) }
}
@Test
fun `WHEN Deny THEN Enable Notifications dialog is shown and no PUT`() = runTest {
fun `GIVEN permission granted but notifications disabled WHEN result THEN no all-three and settings dialog`() =
runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
advanceUntilIdle()
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.OffersUpdates }
.onCheckedChange(true)
advanceUntilIdle()
// POST_NOTIFICATIONS already granted (launcher returns true) but notifications stay OFF at OS level.
model.onPermissionResult(isGranted = true)
advanceUntilIdle()
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
coVerify(exactly = 1) { messageSender.send(any()) }
coVerify(exactly = 1) { markFirstActivationDone(userWalletId) }
}
@Test
fun `GIVEN transaction alerts on AND tokens write fails WHEN written THEN preference not written and reverted`() =
runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
val model = model(osEnabled = true, firstActivationDone = true, preferencesFlow = flow)
// Registered AFTER model() so this specific stub wins over model()'s any()-> Right default.
coEvery {
setNotificationsEnabled(userWalletId, isEnabled = true)
} returns Either.Left(RuntimeException("net"))
advanceUntilIdle()
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.TransactionAlerts }
.onCheckedChange(true)
advanceUntilIdle()
coVerify(exactly = 0) { updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, any()) }
coVerify(atLeast = 1) { messageSender.send(any()) }
val tx = (model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.TransactionAlerts }
assertThat(tx.isOn).isFalse()
}
@Test
fun `GIVEN transaction alerts on AND preference write fails WHEN written THEN token subscription undone`() =
runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
coEvery {
updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, true)
} returns Either.Left(RuntimeException("net"))
val model = model(osEnabled = true, firstActivationDone = true, preferencesFlow = flow)
advanceUntilIdle()
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.TransactionAlerts }
.onCheckedChange(true)
advanceUntilIdle()
// tokens on -> preferences fail -> compensating tokens off; toggle reverted.
coVerifyOrder {
setNotificationsEnabled(userWalletId, isEnabled = true)
updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, true)
setNotificationsEnabled(userWalletId, isEnabled = false)
}
val tx = (model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.TransactionAlerts }
assertThat(tx.isOn).isFalse()
}
@Test
fun `GIVEN first activation AND tokens write fails WHEN Allow THEN reverted and flag not marked`() = runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
val model = model(osEnabled = false, preferencesFlow = flow)
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
// Registered AFTER model() so this specific stub wins over model()'s any()-> Right default.
coEvery { setNotificationsEnabled(userWalletId, isEnabled = true) } returns Either.Left(RuntimeException("net"))
advanceUntilIdle()
val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.OffersUpdates }
offers.onCheckedChange(true)
.onCheckedChange(true)
advanceUntilIdle()
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
model.onPermissionResult(isGranted = true)
advanceUntilIdle()
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
coVerify(exactly = 0) { markFirstActivationDone(userWalletId) }
coVerify(atLeast = 1) { messageSender.send(any()) }
}
@Test
fun `GIVEN first activation AND preferences write fails WHEN Allow THEN tokens undone and flag not marked`() =
runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
// Registered AFTER model() so this specific stub wins over model()'s any()-> Right default.
coEvery {
setAllPreferences(userWalletId, true, true, true)
} returns Either.Left(RuntimeException("net"))
advanceUntilIdle()
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.OffersUpdates }
.onCheckedChange(true)
advanceUntilIdle()
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
model.onPermissionResult(isGranted = true)
advanceUntilIdle()
coVerifyOrder {
setNotificationsEnabled(userWalletId, isEnabled = true)
setAllPreferences(userWalletId, true, true, true)
setNotificationsEnabled(userWalletId, isEnabled = false)
}
coVerify(exactly = 0) { markFirstActivationDone(userWalletId) }
}
@Test
fun `WHEN Deny THEN dialog shown AND flag marked AND no writes`() = runTest {
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
flow.tryEmit(allFalse())
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
advanceUntilIdle()
(model.uiState.value as PushNotificationSettingsUM.Content)
.toggles.first { it.id == ToggleId.OffersUpdates }
.onCheckedChange(true)
advanceUntilIdle()
model.onPermissionResult(isGranted = false)
advanceUntilIdle()
coVerify(exactly = 1) { messageSender.send(any()) }
coVerify(exactly = 1) { markFirstActivationDone(userWalletId) }
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
}
private fun allFalse() = WalletPushNotificationPreferences(