From 1cf8a16fae3a805ce14aad0e5de73b29717e951f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jun 2026 12:38:54 +0300 Subject: [PATCH] Updated on 2026-08-14 --- ...PushNotificationPreferencesDomainModule.kt | 9 + ...etPushNotificationPreferencesRepository.kt | 37 +- ...alletPushNotificationPreferencesUseCase.kt | 22 ++ .../WalletPushNotificationPreferences.kt | 16 +- ...etPushNotificationPreferencesRepository.kt | 10 + .../api/build.gradle.kts | 13 + .../PushNotificationSettingsComponent.kt | 12 + .../impl/build.gradle.kts | 40 +- .../di/PushNotificationSettingsModelModule.kt | 20 + .../entity/AllowPushNotificationsBannerUM.kt | 8 + ...etworksAvailableForNotificationBSConfig.kt | 6 + .../impl/entity/PushNotificationSettingsUM.kt | 22 ++ .../impl/entity/ToggleUM.kt | 20 + .../model/PushNotificationSettingsModel.kt | 346 ++++++++++++++++++ .../PushNotificationSettingsModelTest.kt | 295 +++++++++++++++ .../PushNotificationAnalyticEvents.kt | 35 ++ .../push-notifications/impl/build.gradle.kts | 4 + .../impl/model/PushNotificationsModel.kt | 29 ++ 18 files changed, 922 insertions(+), 22 deletions(-) create mode 100644 domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/SetAllWalletPushNotificationPreferencesUseCase.kt create mode 100644 features/push-notification-settings/api/src/main/kotlin/com/tangem/features/pushnotificationsettings/component/PushNotificationSettingsComponent.kt create mode 100644 features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsModelModule.kt create mode 100644 features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/AllowPushNotificationsBannerUM.kt create mode 100644 features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/NetworksAvailableForNotificationBSConfig.kt create mode 100644 features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt create mode 100644 features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/ToggleUM.kt create mode 100644 features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt create mode 100644 features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt index 548ea7d9df..3ab23fe831 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/PushNotificationPreferencesDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase +import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase import com.tangem.domain.pushnotificationpreferences.UpdateWalletPushNotificationPreferenceUseCase import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository import dagger.Module @@ -37,4 +38,12 @@ internal object PushNotificationPreferencesDomainModule { ): UpdateWalletPushNotificationPreferenceUseCase { return UpdateWalletPushNotificationPreferenceUseCase(repository = repository) } + + @Provides + @Singleton + fun providesSetAllWalletPushNotificationPreferencesUseCase( + repository: WalletPushNotificationPreferencesRepository, + ): SetAllWalletPushNotificationPreferencesUseCase { + return SetAllWalletPushNotificationPreferencesUseCase(repository = repository) + } } \ No newline at end of file diff --git a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt index a7997de172..c5b207e92b 100644 --- a/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt +++ b/data/push-notification-preferences/src/main/kotlin/com/tangem/data/pushnotificationpreferences/DefaultWalletPushNotificationPreferencesRepository.kt @@ -65,7 +65,26 @@ internal class DefaultWalletPushNotificationPreferencesRepository( isEnabled: Boolean, ): Either = Either.catch { val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId) - val updated = applyCategory(current, category, isEnabled) + val updated = current.withCategory(category, isEnabled) + putAndCommit(userWalletId, updated) + } + + override suspend fun setAllPreferences( + userWalletId: UserWalletId, + transactionAlerts: Boolean, + offersUpdates: Boolean, + priceAlerts: Boolean, + ): Either = Either.catch { + val current = cache.getSyncOrNull()?.get(userWalletId.stringValue) ?: loadDefaults(userWalletId) + val updated = current.copy( + transactionAlerts = current.transactionAlerts.copy(isEnabled = transactionAlerts), + offersUpdates = current.offersUpdates.copy(isEnabled = offersUpdates), + priceAlerts = current.priceAlerts.copy(isEnabled = priceAlerts), + ) + putAndCommit(userWalletId, updated) + } + + private suspend fun putAndCommit(userWalletId: UserWalletId, updated: WalletPushNotificationPreferences) { withContext(dispatchers.io) { // TODO: uncomment when api is ready // tangemTechApi.updatePushNotificationPreferences( @@ -80,22 +99,6 @@ internal class DefaultWalletPushNotificationPreferencesRepository( cache.update(default = emptyMap()) { it + (userWalletId.stringValue to updated) } } - private fun applyCategory( - current: WalletPushNotificationPreferences, - category: PushNotificationCategory, - isEnabled: Boolean, - ): WalletPushNotificationPreferences = when (category) { - PushNotificationCategory.TransactionAlerts -> current.copy( - transactionAlerts = current.transactionAlerts.copy(isEnabled = isEnabled), - ) - PushNotificationCategory.OffersUpdates -> current.copy( - offersUpdates = current.offersUpdates.copy(isEnabled = isEnabled), - ) - PushNotificationCategory.PriceAlerts -> current.copy( - priceAlerts = current.priceAlerts.copy(isEnabled = isEnabled), - ) - } - // TODO remove when api is ready, use api methods to load real settings private suspend fun loadDefaults(userWalletId: UserWalletId): WalletPushNotificationPreferences { val areTransactionAlertsEnabled = appPreferencesStore diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/SetAllWalletPushNotificationPreferencesUseCase.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/SetAllWalletPushNotificationPreferencesUseCase.kt new file mode 100644 index 0000000000..272fe46981 --- /dev/null +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/SetAllWalletPushNotificationPreferencesUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.pushnotificationpreferences + +import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository + +class SetAllWalletPushNotificationPreferencesUseCase( + private val repository: WalletPushNotificationPreferencesRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + transactionAlerts: Boolean, + offersUpdates: Boolean, + priceAlerts: Boolean, + ): Either = repository.setAllPreferences( + userWalletId = userWalletId, + transactionAlerts = transactionAlerts, + offersUpdates = offersUpdates, + priceAlerts = priceAlerts, + ) +} \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt index bbb773d931..6ba2914097 100644 --- a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/models/WalletPushNotificationPreferences.kt @@ -4,4 +4,18 @@ data class WalletPushNotificationPreferences( val transactionAlerts: PushNotificationPreference, val offersUpdates: PushNotificationPreference, val priceAlerts: PushNotificationPreference, -) \ No newline at end of file +) { + + fun withCategory(category: PushNotificationCategory, isEnabled: Boolean): WalletPushNotificationPreferences = + when (category) { + PushNotificationCategory.TransactionAlerts -> copy( + transactionAlerts = transactionAlerts.copy(isEnabled = isEnabled), + ) + PushNotificationCategory.OffersUpdates -> copy( + offersUpdates = offersUpdates.copy(isEnabled = isEnabled), + ) + PushNotificationCategory.PriceAlerts -> copy( + priceAlerts = priceAlerts.copy(isEnabled = isEnabled), + ) + } +} \ No newline at end of file diff --git a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt index cde8d5a050..f238674bbf 100644 --- a/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt +++ b/domain/push-notification-preferences/src/main/kotlin/com/tangem/domain/pushnotificationpreferences/repository/WalletPushNotificationPreferencesRepository.kt @@ -20,4 +20,14 @@ interface WalletPushNotificationPreferencesRepository { category: PushNotificationCategory, isEnabled: Boolean, ): Either + + /** + * Set all categories at once + */ + suspend fun setAllPreferences( + userWalletId: UserWalletId, + transactionAlerts: Boolean, + offersUpdates: Boolean, + priceAlerts: Boolean, + ): Either } \ No newline at end of file diff --git a/features/push-notification-settings/api/build.gradle.kts b/features/push-notification-settings/api/build.gradle.kts index b81ea7349e..3514a26763 100644 --- a/features/push-notification-settings/api/build.gradle.kts +++ b/features/push-notification-settings/api/build.gradle.kts @@ -6,4 +6,17 @@ plugins { android { namespace = "com.tangem.features.pushnotificationsettings.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/push-notification-settings/api/src/main/kotlin/com/tangem/features/pushnotificationsettings/component/PushNotificationSettingsComponent.kt b/features/push-notification-settings/api/src/main/kotlin/com/tangem/features/pushnotificationsettings/component/PushNotificationSettingsComponent.kt new file mode 100644 index 0000000000..2673fcc7e8 --- /dev/null +++ b/features/push-notification-settings/api/src/main/kotlin/com/tangem/features/pushnotificationsettings/component/PushNotificationSettingsComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.pushnotificationsettings.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface PushNotificationSettingsComponent : ComposableContentComponent { + + data class Params(val userWalletId: UserWalletId) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/push-notification-settings/impl/build.gradle.kts b/features/push-notification-settings/impl/build.gradle.kts index c593bf8504..bed7c654b6 100644 --- a/features/push-notification-settings/impl/build.gradle.kts +++ b/features/push-notification-settings/impl/build.gradle.kts @@ -11,16 +11,48 @@ android { } dependencies { - /** Api */ + + /* Project - API */ implementation(projects.features.pushNotificationSettings.api) + implementation(projects.features.pushNotifications.api) + implementation(projects.features.walletSettings.api) - /** Core modules */ + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) implementation(projects.core.configToggles) + implementation(projects.core.navigation) + implementation(projects.core.analytics) + implementation(projects.core.utils) - /** Compose */ + /* Project - Domain */ + implementation(projects.domain.models) + implementation(projects.domain.account) + implementation(projects.domain.pushNotificationPreferences) + + /* AndroidX */ + implementation(deps.lifecycle.compose) + + /* Compose */ + implementation(deps.compose.ui) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) implementation(deps.compose.runtime) + implementation(deps.compose.shimmer) + implementation(deps.decompose.ext.compose) - /** DI */ + /* DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /* Other */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + + /* Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.turbine) } \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsModelModule.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsModelModule.kt new file mode 100644 index 0000000000..4e4ba4e3e9 --- /dev/null +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.pushnotificationsettings.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.pushnotificationsettings.impl.model.PushNotificationSettingsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface PushNotificationSettingsModelModule { + + @Binds + @IntoMap + @ClassKey(PushNotificationSettingsModel::class) + fun bindPushNotificationSettingsModel(model: PushNotificationSettingsModel): Model +} \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/AllowPushNotificationsBannerUM.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/AllowPushNotificationsBannerUM.kt new file mode 100644 index 0000000000..3c25d1fa29 --- /dev/null +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/AllowPushNotificationsBannerUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.pushnotificationsettings.impl.entity + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class AllowPushNotificationsBannerUM( + val onOpenSettingsClick: () -> Unit, +) \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/NetworksAvailableForNotificationBSConfig.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/NetworksAvailableForNotificationBSConfig.kt new file mode 100644 index 0000000000..da1835c7cd --- /dev/null +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/NetworksAvailableForNotificationBSConfig.kt @@ -0,0 +1,6 @@ +package com.tangem.features.pushnotificationsettings.impl.entity + +import kotlinx.serialization.Serializable + +@Serializable +internal object NetworksAvailableForNotificationBSConfig \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt new file mode 100644 index 0000000000..f31df5f7cf --- /dev/null +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.pushnotificationsettings.impl.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal sealed interface PushNotificationSettingsUM { + + data object Loading : PushNotificationSettingsUM + + data class Content( + val banner: AllowPushNotificationsBannerUM?, + val toggles: PersistentList, + val requestPermissionEvent: StateEvent, + val onMoreInfoClick: () -> Unit, + ) : PushNotificationSettingsUM + + data class Error( + val onRetryClick: () -> Unit, + ) : PushNotificationSettingsUM +} \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/ToggleUM.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/ToggleUM.kt new file mode 100644 index 0000000000..5cf9df3b05 --- /dev/null +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/ToggleUM.kt @@ -0,0 +1,20 @@ +package com.tangem.features.pushnotificationsettings.impl.entity + +import androidx.annotation.StringRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal data class ToggleUM( + val id: ToggleId, + @StringRes val titleRes: Int, + val subtitle: TextReference, + val isOn: Boolean, + val onCheckedChange: (Boolean) -> Unit, +) + +internal enum class ToggleId { + TransactionAlerts, + OffersUpdates, + PriceAlerts, +} \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt new file mode 100644 index 0000000000..f27b886fbb --- /dev/null +++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt @@ -0,0 +1,346 @@ +package com.tangem.features.pushnotificationsettings.impl.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.notifications.SystemNotificationsStateProvider +import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +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.ObserveWalletPushNotificationPreferencesUseCase +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.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents +import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent +import com.tangem.features.pushnotificationsettings.impl.R +import com.tangem.features.pushnotificationsettings.impl.entity.AllowPushNotificationsBannerUM +import com.tangem.features.pushnotificationsettings.impl.entity.NetworksAvailableForNotificationBSConfig +import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM +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.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList", "LargeClass") +@ModelScoped +internal class PushNotificationSettingsModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val messageSender: UiMessageSender, + private val analyticsEventHandler: AnalyticsEventHandler, + private val observePreferences: ObserveWalletPushNotificationPreferencesUseCase, + private val updatePreference: UpdateWalletPushNotificationPreferenceUseCase, + private val systemNotificationsStateProvider: SystemNotificationsStateProvider, + private val settingsManager: SettingsManager, + private val accountsCRUDRepository: AccountsCRUDRepository, +) : Model() { + + private val params: PushNotificationSettingsComponent.Params = paramsContainer.require() + private val userWalletId: UserWalletId get() = params.userWalletId + + private val loadState = MutableStateFlow(LoadState.Loading) + private val osNotificationsEnabled = MutableStateFlow(systemNotificationsStateProvider.areNotificationsEnabled()) + private val pendingRequest = MutableStateFlow>(consumedEvent()) + + private var pendingPermissionToggle: ToggleSpec? = null + private val preferencesJobHolder = JobHolder() + + private val cachedPrefs: WalletPushNotificationPreferences? + get() = (loadState.value as? LoadState.Content)?.prefs + + val uiState: StateFlow = combine( + loadState, + osNotificationsEnabled, + pendingRequest, + ) { load, osEnabled, request -> + when (load) { + is LoadState.Failed -> PushNotificationSettingsUM.Error(onRetryClick = ::onRetry) + is LoadState.Loading -> PushNotificationSettingsUM.Loading + is LoadState.Content -> buildContent(prefs = load.prefs, osEnabled = osEnabled, request = request) + } + }.stateIn( + scope = modelScope, + // Eagerly: tests read uiState.value synchronously after advanceUntilIdle() with no live collector. + started = SharingStarted.Eagerly, + initialValue = PushNotificationSettingsUM.Loading, + ) + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + private val ToggleId.analyticsValue: String + get() = when (this) { + ToggleId.TransactionAlerts -> "transaction_alerts" + ToggleId.OffersUpdates -> "offers_updates" + ToggleId.PriceAlerts -> "price_alerts" + } + + init { + analyticsEventHandler.send( + PushNotificationAnalyticEvents.NotificationSettingsScreenOpened( + isSystemPermissionEnabled = osNotificationsEnabled.value, + ), + ) + subscribeOnPreferences() + } + + fun onResume() { + osNotificationsEnabled.value = systemNotificationsStateProvider.areNotificationsEnabled() + } + + fun onPermissionResult(isGranted: Boolean) { + pendingRequest.value = consumedEvent() + val tapped = pendingPermissionToggle + pendingPermissionToggle = null + modelScope.launch { + osNotificationsEnabled.value = systemNotificationsStateProvider.areNotificationsEnabled() + analyticsEventHandler.send(PushNotificationAnalyticEvents.PermissionStatus(isAllowed = isGranted)) + if (isGranted && tapped != null) { + applyOptimisticToggle(tapped, newValue = true) + } else if (!isGranted) { + showEnableNotificationsDialog() + } + } + } + + private fun subscribeOnPreferences() { + observePreferences(userWalletId) + .catch { + // 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) } + .launchIn(modelScope) + .saveIn(preferencesJobHolder) + } + + private fun buildContent( + prefs: WalletPushNotificationPreferences, + osEnabled: Boolean, + request: StateEvent, + ): PushNotificationSettingsUM.Content { + return PushNotificationSettingsUM.Content( + banner = buildBanner(prefs = prefs, osEnabled = osEnabled), + toggles = buildToggles(prefs), + requestPermissionEvent = request, + onMoreInfoClick = ::onMoreInfoClick, + ) + } + + private fun onMoreInfoClick() { + bottomSheetNavigation.activate(NetworksAvailableForNotificationBSConfig) + } + + private fun buildToggles(prefs: WalletPushNotificationPreferences): PersistentList { + return TOGGLE_ORDER + .asSequence() + .map { id -> id.spec(prefs) } + .filter { it.preference.isVisible } + .map { spec -> + ToggleUM( + id = spec.id, + titleRes = spec.titleRes, + subtitle = spec.subtitle, + isOn = spec.preference.isEnabled, + onCheckedChange = { newValue -> onToggleTapped(spec, newValue) }, + ) + } + .toList() + .toPersistentList() + } + + private fun buildBanner( + prefs: WalletPushNotificationPreferences, + osEnabled: Boolean, + ): AllowPushNotificationsBannerUM? { + val isAnyOn = prefs.transactionAlerts.isEnabled || + prefs.offersUpdates.isEnabled || + prefs.priceAlerts.isEnabled + if (osEnabled || !isAnyOn) return null + return AllowPushNotificationsBannerUM(onOpenSettingsClick = ::onBannerCtaClick) + } + + private fun requestPermission(tapped: ToggleSpec? = null) { + pendingPermissionToggle = tapped + pendingRequest.value = triggeredEvent(data = Unit, onConsume = ::onPermissionEventConsumed) + } + + private fun onBannerCtaClick() { + analyticsEventHandler.send(PushNotificationAnalyticEvents.BannerOpenSettingsTapped()) + // The banner only shows when OS notifications are disabled, which also covers the case + // where POST_NOTIFICATIONS is already granted but notifications are off at the system level. + // A permission request would be a no-op there, so send the user to the OS settings instead. + settingsManager.openAppNotificationSettings() + } + + private fun onRetry() { + loadState.value = LoadState.Loading + subscribeOnPreferences() + } + + private fun onToggleTapped(spec: ToggleSpec, newValue: Boolean) { + analyticsEventHandler.send( + PushNotificationAnalyticEvents.ToggleClicked(toggleType = spec.id.analyticsValue, isEnabled = newValue), + ) + + if (newValue && !osNotificationsEnabled.value) { + requestPermission(tapped = spec) + return + } + + applyOptimisticToggle(spec, newValue) + } + + private fun onPermissionEventConsumed() { + pendingRequest.value = consumedEvent() + } + + private fun applyOptimisticToggle(spec: ToggleSpec, newValue: Boolean) { + val current = cachedPrefs ?: return + loadState.value = LoadState.Content(current.withCategory(spec.category, newValue)) + + // Writes are intentionally not serialized here: serializing repository writes is the data + // layer's responsibility, not the model's. A failed write reverts only its own category. + modelScope.launch { writeToggle(spec, newValue) } + } + + 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) } + } + + private fun revertOptimistic(spec: ToggleSpec, newValue: Boolean) { + // Revert only the failed category on top of the current state, so a concurrent toggle's + // optimistic value isn't clobbered by a stale full-snapshot replacement. + loadState.update { state -> + if (state is LoadState.Content) { + LoadState.Content(state.prefs.withCategory(spec.category, !newValue)) + } else { + state + } + } + analyticsEventHandler.send( + PushNotificationAnalyticEvents.NotificationSettingsErrorShown( + toggleType = spec.id.analyticsValue, + errorType = ERROR_TYPE_WRITE_FAILED, + ), + ) + messageSender.send( + DialogMessage( + title = resourceReference(R.string.common_something_went_wrong), + message = resourceReference(R.string.common_try_again_later), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_ok), + onClick = {}, + ), + ), + ) + } + + private fun showEnableNotificationsDialog() { + messageSender.send( + DialogMessage( + title = resourceReference(R.string.push_notifications_permission_alert_title), + message = resourceReference(R.string.push_notifications_permission_alert_description), + firstAction = EventMessageAction( + title = resourceReference(R.string.push_notifications_permission_alert_positive_button), + onClick = { settingsManager.openAppNotificationSettings() }, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.push_notifications_permission_alert_negative_button), + onClick = {}, + ), + ), + ) + } + + private fun ToggleId.spec(prefs: WalletPushNotificationPreferences): ToggleSpec = when (this) { + ToggleId.TransactionAlerts -> ToggleSpec( + id = this, + titleRes = R.string.push_notification_settings_transaction_alerts_title, + subtitle = resourceReference(R.string.push_notification_settings_transaction_alerts_subtitle), + preference = prefs.transactionAlerts, + category = PushNotificationCategory.TransactionAlerts, + ) + ToggleId.OffersUpdates -> ToggleSpec( + id = this, + titleRes = R.string.push_notification_settings_offers_updates_title, + subtitle = resourceReference(R.string.push_notification_settings_offers_updates_subtitle), + preference = prefs.offersUpdates, + category = PushNotificationCategory.OffersUpdates, + ) + ToggleId.PriceAlerts -> ToggleSpec( + id = this, + titleRes = R.string.push_notification_settings_price_alerts_title, + subtitle = resourceReference(R.string.push_notification_settings_price_alerts_subtitle), + preference = prefs.priceAlerts, + category = PushNotificationCategory.PriceAlerts, + ) + } + + private data class ToggleSpec( + val id: ToggleId, + val titleRes: Int, + val subtitle: TextReference, + val preference: PushNotificationPreference, + val category: PushNotificationCategory, + ) + + private sealed interface LoadState { + data object Loading : LoadState + data object Failed : LoadState + data class Content(val prefs: WalletPushNotificationPreferences) : LoadState + } + + private companion object { + const val ERROR_TYPE_WRITE_FAILED = "Write Failed" + val TOGGLE_ORDER = listOf( + ToggleId.TransactionAlerts, + ToggleId.OffersUpdates, + ToggleId.PriceAlerts, + ) + } +} \ No newline at end of file diff --git a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt new file mode 100644 index 0000000000..152d433421 --- /dev/null +++ b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt @@ -0,0 +1,295 @@ +package com.tangem.features.pushnotificationsettings.impl.model + +import app.cash.turbine.test +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +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.navigation.settings.SettingsManager +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase +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.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.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@Suppress("LongParameterList") +class PushNotificationSettingsModelTest { + + private val userWalletId = UserWalletId("0011223344556677") + + private val observePreferences: ObserveWalletPushNotificationPreferencesUseCase = mockk() + private val updatePreference: UpdateWalletPushNotificationPreferenceUseCase = mockk() + 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, + preferencesFlow: MutableSharedFlow = MutableSharedFlow(replay = 1), + ): PushNotificationSettingsModel { + every { systemNotificationsStateProvider.areNotificationsEnabled() } returns osEnabled + every { observePreferences(userWalletId) } returns preferencesFlow + return PushNotificationSettingsModel( + paramsContainer = MutableParamsContainer(PushNotificationSettingsComponent.Params(userWalletId)), + dispatchers = TestingCoroutineDispatcherProvider(), + messageSender = messageSender, + analyticsEventHandler = analyticsEventHandler, + observePreferences = observePreferences, + updatePreference = updatePreference, + systemNotificationsStateProvider = systemNotificationsStateProvider, + settingsManager = settingsManager, + accountsCRUDRepository = accountsCRUDRepository, + ) + } + + @Test + fun `GIVEN cache populated WHEN model created THEN ui state becomes Content`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + val model = model(preferencesFlow = flow) + advanceUntilIdle() + + model.uiState.test { + assertThat(awaitItem()).isInstanceOf(PushNotificationSettingsUM.Content::class.java) + } + } + + @Test + 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 + + val model = PushNotificationSettingsModel( + paramsContainer = MutableParamsContainer(PushNotificationSettingsComponent.Params(userWalletId)), + dispatchers = TestingCoroutineDispatcherProvider(), + messageSender = messageSender, + analyticsEventHandler = analyticsEventHandler, + observePreferences = observePreferences, + updatePreference = updatePreference, + systemNotificationsStateProvider = systemNotificationsStateProvider, + settingsManager = settingsManager, + accountsCRUDRepository = accountsCRUDRepository, + ) + advanceUntilIdle() + + model.uiState.test { + assertThat(awaitItem()).isInstanceOf(PushNotificationSettingsUM.Error::class.java) + } + } + + @Test + fun `GIVEN OS enabled AND any toggle on WHEN built THEN banner is Hidden`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(anyOn()) + val model = model(osEnabled = true, preferencesFlow = flow) + advanceUntilIdle() + + val content = model.uiState.value as PushNotificationSettingsUM.Content + assertThat(content.banner).isNull() + } + + @Test + fun `GIVEN OS disabled AND any toggle on WHEN built THEN banner is Visible`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(anyOn()) + val model = model(osEnabled = false, preferencesFlow = flow) + advanceUntilIdle() + + val content = model.uiState.value as PushNotificationSettingsUM.Content + assertThat(content.banner).isNotNull() + } + + @Test + fun `GIVEN OS disabled AND no toggle on WHEN built THEN banner is Hidden`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + val model = model(osEnabled = false, preferencesFlow = flow) + advanceUntilIdle() + + val content = model.uiState.value as PushNotificationSettingsUM.Content + assertThat(content.banner).isNull() + } + + @Test + fun `GIVEN OS enabled WHEN toggle flipped on THEN repository is updated`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + coEvery { + updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) + } returns Either.Right(Unit) + val model = model(osEnabled = true, preferencesFlow = flow) + advanceUntilIdle() + + val content = model.uiState.value as PushNotificationSettingsUM.Content + val offers = content.toggles.first { it.id == ToggleId.OffersUpdates } + offers.onCheckedChange(true) + advanceUntilIdle() + + coVerify { + updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) + } + } + + @Test + fun `GIVEN repository write fails WHEN toggle flipped THEN message is sent and toggle is reverted`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + coEvery { + updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) + } returns Either.Left(RuntimeException("network")) + val model = model(osEnabled = true, preferencesFlow = flow) + advanceUntilIdle() + + val offers = (model.uiState.value as PushNotificationSettingsUM.Content) + .toggles.first { it.id == ToggleId.OffersUpdates } + offers.onCheckedChange(true) + advanceUntilIdle() + + coVerify(atLeast = 1) { messageSender.send(any()) } + val current = (model.uiState.value as PushNotificationSettingsUM.Content) + .toggles.first { it.id == ToggleId.OffersUpdates } + assertThat(current.isOn).isFalse() + } + + @Test + fun `GIVEN two toggles flipped WHEN one write fails THEN only the failed toggle reverts`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + coEvery { + updatePreference(userWalletId, PushNotificationCategory.TransactionAlerts, true) + } returns Either.Left(RuntimeException("network")) + coEvery { + updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) + } returns Either.Right(Unit) + val model = model(osEnabled = true, preferencesFlow = flow) + advanceUntilIdle() + + // Flip both toggles optimistically before either write resolves. + val initial = model.uiState.value as PushNotificationSettingsUM.Content + initial.toggles.first { it.id == ToggleId.TransactionAlerts }.onCheckedChange(true) + initial.toggles.first { it.id == ToggleId.OffersUpdates }.onCheckedChange(true) + advanceUntilIdle() + + // The failed TransactionAlerts write reverts only itself; OffersUpdates keeps its value. + val toggles = (model.uiState.value as PushNotificationSettingsUM.Content).toggles + assertThat(toggles.first { it.id == ToggleId.TransactionAlerts }.isOn).isFalse() + assertThat(toggles.first { it.id == ToggleId.OffersUpdates }.isOn).isTrue() + } + + @Test + fun `GIVEN OS disabled WHEN toggle ON THEN permission request is triggered`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + val model = model(osEnabled = false, preferencesFlow = flow) + advanceUntilIdle() + + val offers = (model.uiState.value as PushNotificationSettingsUM.Content) + .toggles.first { it.id == ToggleId.OffersUpdates } + offers.onCheckedChange(true) + advanceUntilIdle() + + val content = model.uiState.value as PushNotificationSettingsUM.Content + assertThat(content.requestPermissionEvent.javaClass.simpleName).isEqualTo("Triggered") + coVerify(exactly = 0) { updatePreference(any(), any(), any()) } + } + + @Test + fun `WHEN banner CTA tapped THEN OS notification settings are opened`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(anyOn()) + val model = model(osEnabled = false, preferencesFlow = flow) + advanceUntilIdle() + + val banner = requireNotNull( + (model.uiState.value as PushNotificationSettingsUM.Content).banner, + ) + banner.onOpenSettingsClick() + advanceUntilIdle() + + verify(exactly = 1) { settingsManager.openAppNotificationSettings() } + val refreshed = model.uiState.value as PushNotificationSettingsUM.Content + assertThat(refreshed.requestPermissionEvent.javaClass.simpleName).isEqualTo("Consumed") + } + + @Test + fun `WHEN Allow on a single tapped toggle THEN only that toggle is enabled`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + coEvery { + updatePreference(userWalletId, PushNotificationCategory.OffersUpdates, true) + } returns Either.Right(Unit) + + val model = model(osEnabled = false, preferencesFlow = flow) + advanceUntilIdle() + + val offers = (model.uiState.value as PushNotificationSettingsUM.Content) + .toggles.first { it.id == ToggleId.OffersUpdates } + offers.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()) + } + } + + @Test + fun `WHEN Deny THEN Enable Notifications dialog is shown and no PUT`() = runTest { + val flow = MutableSharedFlow(replay = 1) + flow.tryEmit(allFalse()) + val model = model(osEnabled = false, preferencesFlow = flow) + advanceUntilIdle() + + val offers = (model.uiState.value as PushNotificationSettingsUM.Content) + .toggles.first { it.id == ToggleId.OffersUpdates } + offers.onCheckedChange(true) + advanceUntilIdle() + model.onPermissionResult(isGranted = false) + advanceUntilIdle() + + coVerify(exactly = 1) { messageSender.send(any()) } + coVerify(exactly = 0) { updatePreference(any(), any(), any()) } + } + + private fun allFalse() = WalletPushNotificationPreferences( + transactionAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), + offersUpdates = PushNotificationPreference(isEnabled = false, isVisible = true), + priceAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), + ) + + private fun anyOn() = WalletPushNotificationPreferences( + transactionAlerts = PushNotificationPreference(isEnabled = true, isVisible = true), + offersUpdates = PushNotificationPreference(isEnabled = false, isVisible = true), + priceAlerts = PushNotificationPreference(isEnabled = false, isVisible = true), + ) +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt index 411fad6ab3..c82a9e2171 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -70,4 +70,39 @@ sealed class PushNotificationAnalyticEvents( AnalyticsParam.STATE to if (isEnabled) "On" else "Off", ), ) + + data class NotificationSettingsScreenOpened( + val isSystemPermissionEnabled: Boolean, + ) : PushNotificationAnalyticEvents( + event = "Notification Settings Screen Opened", + params = mapOf( + AnalyticsParam.STATE to isSystemPermissionEnabled.toString(), + ), + ) + + data class ToggleClicked( + val toggleType: String, + val isEnabled: Boolean, + ) : PushNotificationAnalyticEvents( + event = "Toggle Clicked", + params = mapOf( + "Toggle Type" to toggleType, + AnalyticsParam.STATE to if (isEnabled) "On" else "Off", + ), + ) + + class BannerOpenSettingsTapped : PushNotificationAnalyticEvents( + event = "Banner - Open Settings Tapped", + ) + + data class NotificationSettingsErrorShown( + val toggleType: String, + val errorType: String, + ) : PushNotificationAnalyticEvents( + event = "Notification Settings Error Shown", + params = mapOf( + "Toggle Type" to toggleType, + AnalyticsParam.ERROR_TYPE to errorType, + ), + ) } \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index 9fd7bd7369..c102276ebc 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -41,6 +41,10 @@ dependencies { /** Domain module */ implementation(projects.domain.settings) implementation(projects.domain.notifications) + implementation(projects.domain.pushNotificationPreferences) + implementation(projects.domain.common) + implementation(projects.domain.account) + implementation(projects.domain.models) /** Feature modules */ implementation(projects.features.pushNotifications.api) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index 99af1aa487..62084b0caf 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -5,10 +5,14 @@ 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.account.repository.AccountsCRUDRepository +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.features.pushnotifications.api.PushNotificationsParams @@ -16,6 +20,7 @@ import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnaly import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.launch import javax.inject.Inject @@ -31,6 +36,9 @@ 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 accountsCRUDRepository: AccountsCRUDRepository, ) : Model(), PushNotificationsClickIntents { val params: PushNotificationsParams = paramsContainer.require() @@ -75,6 +83,9 @@ internal class PushNotificationsModel @Inject constructor( modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + if (isPushNotificationSettingsEnabled) { + applyFirstActivationRule() + } params.modelCallbacks.onAllowSystemPermission() if (!params.isBottomSheet) { params.nextRoute?.let { appRouter.push(it) } @@ -95,4 +106,22 @@ internal class PushNotificationsModel @Inject constructor( } } } + + // TODO [REDACTED_JIRA] evaluate per-wallet "first-activation done" + // tracking (iOS keeps a [walletId] array in UserDefaults). Today the bulk-enable fires every + // time onAllowPermission is called under the feature toggle, but Soft Ask itself is gated by + // the existing `shouldShowPushPermission_*` flag so in practice it runs once per install. + private suspend fun applyFirstActivationRule() { + userWalletsListRepository.userWalletsSync().forEach { wallet -> + val result = setAllWalletPushNotificationPreferences( + userWalletId = wallet.walletId, + transactionAlerts = true, + offersUpdates = true, + priceAlerts = true, + ) + if (result is Either.Right) { + runSuspendCatching { accountsCRUDRepository.syncTokens(wallet.walletId) } + } + } + } } \ No newline at end of file