Updated on 2026-08-14
This commit is contained in:
parent
457e8870a4
commit
7c82be8da5
2 changed files with 151 additions and 20 deletions
|
|
@ -128,32 +128,48 @@ internal class PushNotificationSettingsModel @Inject constructor(
|
|||
|
||||
fun onResume() {
|
||||
osNotificationsEnabled.value = systemNotificationsStateProvider.areNotificationsEnabled()
|
||||
if (cachedPrefs != null) autoApplyFirstActivationIfNeeded()
|
||||
if (cachedPrefs == null) return
|
||||
|
||||
// An explicitly tapped toggle wins over the silent first-activation rule.
|
||||
if (pendingPermissionToggle != null) {
|
||||
modelScope.launch { applyPendingToggle() }
|
||||
} else {
|
||||
autoApplyFirstActivationIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPermissionResult(isGranted: Boolean) {
|
||||
val tapped = pendingPermissionToggle
|
||||
pendingPermissionToggle = null
|
||||
modelScope.launch {
|
||||
// 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.
|
||||
// isGranted alone is unreliable: an already-granted-but-OS-disabled wallet returns true
|
||||
// instantly, and pre-Android 13 there is no runtime permission at all.
|
||||
val isNotificationsEnabled = systemNotificationsStateProvider.areNotificationsEnabled()
|
||||
osNotificationsEnabled.value = isNotificationsEnabled
|
||||
analyticsEventHandler.send(PushNotificationAnalyticEvents.PermissionStatus(isAllowed = isGranted))
|
||||
|
||||
if (!isGranted || !isNotificationsEnabled) {
|
||||
markFirstActivationDone(userWalletId)
|
||||
if (isNotificationsEnabled) {
|
||||
applyPendingToggle()
|
||||
} else {
|
||||
// The tapped toggle stays pending until the user comes back from the system settings.
|
||||
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)
|
||||
}
|
||||
/**
|
||||
* Applies the toggle the user tapped before the permission ask, once notifications are actually enabled.
|
||||
* A refusal never fixes the first-activation flag, so the rule survives until a real grant.
|
||||
*/
|
||||
private suspend fun applyPendingToggle() {
|
||||
val tapped = pendingPermissionToggle ?: return
|
||||
if (!osNotificationsEnabled.value || cachedPrefs == null) return
|
||||
// Consumed before the first suspension point, so a concurrent resume cannot apply it twice.
|
||||
pendingPermissionToggle = null
|
||||
|
||||
if (isFirstActivationDone(userWalletId)) {
|
||||
applyOptimisticToggle(tapped, newValue = true)
|
||||
} else if (enableAllCategories(initiatingToggle = tapped)) {
|
||||
// Fix the flag only after a successful enable, so a transient failure can retry.
|
||||
markFirstActivationDone(userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -373,7 +389,8 @@ internal class PushNotificationSettingsModel @Inject constructor(
|
|||
),
|
||||
secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.push_notifications_permission_alert_negative_button),
|
||||
onClick = {},
|
||||
// Declining is a final answer: the tapped toggle is dropped.
|
||||
onClick = { pendingPermissionToggle = null },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
|||
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.ui.message.DialogMessage
|
||||
import com.tangem.core.navigation.settings.SettingsManager
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
|
|
@ -29,6 +30,7 @@ import io.mockk.coVerify
|
|||
import io.mockk.coVerifyOrder
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
|
@ -341,7 +343,7 @@ class PushNotificationSettingsModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN permission granted but notifications disabled WHEN result THEN no all-three and settings dialog`() =
|
||||
fun `GIVEN permission granted but notifications disabled WHEN result THEN dialog shown AND flag not marked`() =
|
||||
runTest {
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
|
|
@ -359,7 +361,7 @@ class PushNotificationSettingsModelTest {
|
|||
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) }
|
||||
coVerify(exactly = 0) { markFirstActivationDone(userWalletId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -464,7 +466,7 @@ class PushNotificationSettingsModelTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN Deny THEN dialog shown AND flag marked AND no writes`() = runTest {
|
||||
fun `WHEN Deny THEN dialog shown AND flag not marked AND no writes`() = runTest {
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
|
||||
|
|
@ -478,7 +480,119 @@ class PushNotificationSettingsModelTest {
|
|||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { messageSender.send(any()) }
|
||||
coVerify(exactly = 1) { markFirstActivationDone(userWalletId) }
|
||||
coVerify(exactly = 0) { markFirstActivationDone(userWalletId) }
|
||||
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tapped toggle refused WHEN notifications enabled in settings THEN toggle applied on resume`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
coEvery {
|
||||
updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true)
|
||||
} returns Either.Right(Unit)
|
||||
val model = model(osEnabled = false, firstActivationDone = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act: tap -> refused -> notifications enabled in the OS settings -> back to the screen.
|
||||
(model.uiState.value as PushNotificationSettingsUM.Content)
|
||||
.toggles.first { it.id == ToggleId.OffersUpdates }
|
||||
.onCheckedChange(true)
|
||||
advanceUntilIdle()
|
||||
model.onPermissionResult(isGranted = false)
|
||||
advanceUntilIdle()
|
||||
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
|
||||
model.onResume()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) }
|
||||
val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
|
||||
.toggles.first { it.id == ToggleId.OffersUpdates }
|
||||
assertThat(offers.isOn).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN first activation not done AND toggle refused WHEN notifications enabled THEN all three enabled`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
val model = model(osEnabled = false, firstActivationDone = false, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
(model.uiState.value as PushNotificationSettingsUM.Content)
|
||||
.toggles.first { it.id == ToggleId.OffersUpdates }
|
||||
.onCheckedChange(true)
|
||||
advanceUntilIdle()
|
||||
model.onPermissionResult(isGranted = false)
|
||||
advanceUntilIdle()
|
||||
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
|
||||
model.onResume()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert: the refusal did not consume the first-activation rule, so it runs on the late grant.
|
||||
coVerify(exactly = 1) { setNotificationsEnabled(userWalletId, isEnabled = true) }
|
||||
coVerify(exactly = 1) { setAllPreferences(userWalletId, true, true, true) }
|
||||
coVerify(exactly = 1) { markFirstActivationDone(userWalletId) }
|
||||
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tapped toggle refused WHEN returning with notifications still disabled THEN nothing is written`() =
|
||||
runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
val model = model(osEnabled = false, firstActivationDone = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
|
||||
// Act
|
||||
(model.uiState.value as PushNotificationSettingsUM.Content)
|
||||
.toggles.first { it.id == ToggleId.OffersUpdates }
|
||||
.onCheckedChange(true)
|
||||
advanceUntilIdle()
|
||||
model.onPermissionResult(isGranted = false)
|
||||
advanceUntilIdle()
|
||||
model.onResume()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { markFirstActivationDone(any()) }
|
||||
val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
|
||||
.toggles.first { it.id == ToggleId.OffersUpdates }
|
||||
assertThat(offers.isOn).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dialog declined WHEN notifications enabled afterwards THEN tapped toggle is not applied`() = runTest {
|
||||
// Arrange
|
||||
val flow = MutableSharedFlow<WalletPushNotificationPreferences>(replay = 1)
|
||||
flow.tryEmit(allFalse())
|
||||
val model = model(osEnabled = false, firstActivationDone = true, preferencesFlow = flow)
|
||||
advanceUntilIdle()
|
||||
val dialog = slot<DialogMessage>()
|
||||
|
||||
// Act
|
||||
(model.uiState.value as PushNotificationSettingsUM.Content)
|
||||
.toggles.first { it.id == ToggleId.OffersUpdates }
|
||||
.onCheckedChange(true)
|
||||
advanceUntilIdle()
|
||||
model.onPermissionResult(isGranted = false)
|
||||
advanceUntilIdle()
|
||||
verify { messageSender.send(capture(dialog)) }
|
||||
dialog.captured.secondAction?.onClick?.invoke()
|
||||
every { systemNotificationsStateProvider.areNotificationsEnabled() } returns true
|
||||
model.onResume()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setAllPreferences(any(), any(), any(), any()) }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue