Updated on 2026-08-14
This commit is contained in:
parent
fb08f7c4e1
commit
4860b1fd8b
10 changed files with 425 additions and 63 deletions
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue