Updated on 2026-08-14
This commit is contained in:
parent
8af068b74b
commit
fc319f2b70
17 changed files with 518 additions and 68 deletions
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.ObserveWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.PreloadWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
|
|
@ -46,4 +48,20 @@ internal object PushNotificationPreferencesDomainModule {
|
|||
): SetAllWalletPushNotificationPreferencesUseCase {
|
||||
return SetAllWalletPushNotificationPreferencesUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIsPushNotificationFirstActivationDoneUseCase(
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
): IsPushNotificationFirstActivationDoneUseCase {
|
||||
return IsPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesMarkPushNotificationFirstActivationDoneUseCase(
|
||||
repository: WalletPushNotificationPreferencesRepository,
|
||||
): MarkPushNotificationFirstActivationDoneUseCase {
|
||||
return MarkPushNotificationFirstActivationDoneUseCase(repository = repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -163,6 +163,10 @@ object PreferencesKeys {
|
|||
)
|
||||
}
|
||||
|
||||
val PUSH_NOTIFICATION_FIRST_ACTIVATION_DONE_WALLET_IDS_KEY by lazy {
|
||||
stringSetPreferencesKey(name = "pushNotificationFirstActivationDoneWalletIds")
|
||||
}
|
||||
|
||||
val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy {
|
||||
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ dependencies {
|
|||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testImplementation(deps.test.mockk)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import com.tangem.datasource.api.common.response.getOrThrow
|
|||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferencesBody
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory
|
||||
import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences
|
||||
|
|
@ -28,11 +31,23 @@ import kotlinx.coroutines.withContext
|
|||
internal class DefaultWalletPushNotificationPreferencesRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val cache: RuntimeSharedStore<Map<String, WalletPushNotificationPreferences>>,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : WalletPushNotificationPreferencesRepository {
|
||||
|
||||
private val walletMutexes = ConcurrentHashMap<String, Mutex>()
|
||||
|
||||
override suspend fun isFirstActivationDone(userWalletId: UserWalletId): Boolean =
|
||||
appPreferencesStore.getSyncOrNull(PreferencesKeys.PUSH_NOTIFICATION_FIRST_ACTIVATION_DONE_WALLET_IDS_KEY)
|
||||
?.contains(userWalletId.stringValue) == true
|
||||
|
||||
override suspend fun markFirstActivationDone(userWalletId: UserWalletId) {
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val key = PreferencesKeys.PUSH_NOTIFICATION_FIRST_ACTIVATION_DONE_WALLET_IDS_KEY
|
||||
preferences[key] = preferences.getOrDefault(key, emptySet()) + userWalletId.stringValue
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun preload(userWalletId: UserWalletId) {
|
||||
if (isCached(userWalletId)) return
|
||||
mutexFor(userWalletId).withLock {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.data.pushnotificationpreferences.di
|
|||
import com.tangem.data.pushnotificationpreferences.DefaultWalletPushNotificationPreferencesRepository
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -19,10 +20,12 @@ internal object PushNotificationPreferencesModule {
|
|||
@Provides
|
||||
fun providesWalletPushNotificationPreferencesRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletPushNotificationPreferencesRepository = DefaultWalletPushNotificationPreferencesRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
cache = RuntimeSharedStore(),
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,17 +1,21 @@
|
|||
package com.tangem.data.pushnotificationpreferences
|
||||
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import app.cash.turbine.test
|
||||
import arrow.core.Either
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferencesBody
|
||||
import com.tangem.datasource.api.tangemTech.models.PushNotificationPreferencesResponse
|
||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCategory
|
||||
import com.tangem.domain.pushnotificationpreferences.models.PushNotificationPreference
|
||||
import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
|
|
@ -30,9 +34,17 @@ class DefaultWalletPushNotificationPreferencesRepositoryTest {
|
|||
private val userWalletId = UserWalletId(stringValue = "0011223344556677")
|
||||
private val otherWalletId = UserWalletId(stringValue = "ffeeddccbbaa9988")
|
||||
|
||||
// Real in-memory store so the persisted first-activation flag (a Set<String> merge) is genuinely exercised.
|
||||
private val appPreferencesStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = MockStateDataStore(default = emptyPreferences()),
|
||||
)
|
||||
|
||||
private val repository = DefaultWalletPushNotificationPreferencesRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
cache = RuntimeSharedStore(),
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
|
|
@ -202,6 +214,28 @@ class DefaultWalletPushNotificationPreferencesRepositoryTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallet never activated WHEN isFirstActivationDone THEN false`() = runTest {
|
||||
assertThat(repository.isFirstActivationDone(userWalletId)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN wallet marked WHEN isFirstActivationDone THEN true and persisted`() = runTest {
|
||||
repository.markFirstActivationDone(userWalletId)
|
||||
|
||||
assertThat(repository.isFirstActivationDone(userWalletId)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN one wallet marked WHEN another wallet marked THEN both stay done`() = runTest {
|
||||
// Guards the additive-merge backbone: a regression to a single-id overwrite would drop the first wallet.
|
||||
repository.markFirstActivationDone(userWalletId)
|
||||
repository.markFirstActivationDone(otherWalletId)
|
||||
|
||||
assertThat(repository.isFirstActivationDone(userWalletId)).isTrue()
|
||||
assertThat(repository.isFirstActivationDone(otherWalletId)).isTrue()
|
||||
}
|
||||
|
||||
private fun stubGet(id: UserWalletId, transaction: Boolean, offers: Boolean, price: Boolean) {
|
||||
coEvery { tangemTechApi.getPushNotificationPreferences(id.stringValue) } returns
|
||||
ApiResponse.Success(PushNotificationPreferencesResponse(transaction, offers, price))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.pushnotificationpreferences
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
|
||||
|
||||
class IsPushNotificationFirstActivationDoneUseCase(
|
||||
private val repository: WalletPushNotificationPreferencesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId): Boolean = repository.isFirstActivationDone(userWalletId)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.pushnotificationpreferences
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.pushnotificationpreferences.repository.WalletPushNotificationPreferencesRepository
|
||||
|
||||
class MarkPushNotificationFirstActivationDoneUseCase(
|
||||
private val repository: WalletPushNotificationPreferencesRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userWalletId: UserWalletId) = repository.markFirstActivationDone(userWalletId)
|
||||
}
|
||||
|
|
@ -6,12 +6,18 @@ import com.tangem.domain.pushnotificationpreferences.models.PushNotificationCate
|
|||
import com.tangem.domain.pushnotificationpreferences.models.WalletPushNotificationPreferences
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/** Per-wallet push notification preferences. In-memory cache, not persisted. */
|
||||
/** Per-wallet push notification preferences. Values are cached in-memory; the first-activation flag is persisted. */
|
||||
interface WalletPushNotificationPreferencesRepository {
|
||||
|
||||
/** Warms up the cache for [userWalletId]. No-op if already cached. */
|
||||
suspend fun preload(userWalletId: UserWalletId)
|
||||
|
||||
/** Whether the first push-permission activation has already run for [userWalletId]. Persisted. */
|
||||
suspend fun isFirstActivationDone(userWalletId: UserWalletId): Boolean
|
||||
|
||||
/** Marks the first push-permission activation as done for [userWalletId]. */
|
||||
suspend fun markFirstActivationDone(userWalletId: UserWalletId)
|
||||
|
||||
fun observePreferences(userWalletId: UserWalletId): Flow<WalletPushNotificationPreferences>
|
||||
|
||||
/** Updates a single [category]; full-replace PUT under the hood. On failure cache is untouched. */
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
.onLeft { revertOptimistic(spec, newValue) }
|
||||
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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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, preferencesFlow = flow)
|
||||
val model = model(osEnabled = false, firstActivationDone = false, 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()
|
||||
// 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, 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()
|
||||
|
||||
(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()
|
||||
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -45,8 +45,8 @@ dependencies {
|
|||
implementation(projects.domain.notifications)
|
||||
implementation(projects.domain.pushNotificationPreferences)
|
||||
implementation(projects.domain.common)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets)
|
||||
|
||||
/** Feature modules */
|
||||
implementation(projects.features.pushNotifications.api)
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ 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.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
|
|
@ -22,7 +24,6 @@ import com.tangem.features.pushnotifications.impl.domain.GetPushNotificationsDou
|
|||
import com.tangem.features.pushnotifications.impl.domain.DoubleAskVariant
|
||||
import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -43,7 +44,9 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
|
||||
private val setAllWalletPushNotificationPreferences: SetAllWalletPushNotificationPreferencesUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository,
|
||||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
|
||||
private val isPushNotificationFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase,
|
||||
private val markPushNotificationFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase,
|
||||
private val getPushNotificationsDoubleAskVariantUseCase: GetPushNotificationsDoubleAskVariantUseCase,
|
||||
) : Model(), PushNotificationsClickIntents {
|
||||
|
||||
|
|
@ -145,6 +148,10 @@ internal class PushNotificationsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
if (isPushNotificationSettingsEnabled) {
|
||||
// Flag is fixed on DENY too, so a later activation is selective.
|
||||
markFirstActivationDoneForAllWallets()
|
||||
}
|
||||
params.modelCallbacks.onDenySystemPermission()
|
||||
if (!params.isBottomSheet) {
|
||||
params.nextRoute?.let { appRouter.push(it) }
|
||||
|
|
@ -152,21 +159,29 @@ 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.
|
||||
/** On the first grant, enable all three categories for every not-yet-activated wallet (guarded once per wallet). */
|
||||
private suspend fun applyFirstActivationRule() {
|
||||
userWalletsListRepository.userWalletsSync().forEach { wallet ->
|
||||
val result = setAllWalletPushNotificationPreferences(
|
||||
if (isPushNotificationFirstActivationDone(wallet.walletId)) return@forEach
|
||||
|
||||
// Tokens (address re-subscription) first, then preferences; mark only on success, undo tokens on failure.
|
||||
val tokensResult = setNotificationsEnabledUseCase(wallet.walletId, isEnabled = true)
|
||||
if (tokensResult is Either.Left) return@forEach
|
||||
|
||||
setAllWalletPushNotificationPreferences(
|
||||
userWalletId = wallet.walletId,
|
||||
transactionAlerts = true,
|
||||
offersUpdates = true,
|
||||
priceAlerts = true,
|
||||
)
|
||||
if (result is Either.Right) {
|
||||
runSuspendCatching { accountsCRUDRepository.syncTokens(wallet.walletId) }
|
||||
.onRight { markPushNotificationFirstActivationDone(wallet.walletId) }
|
||||
.onLeft { setNotificationsEnabledUseCase(wallet.walletId, isEnabled = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun markFirstActivationDoneForAllWallets() {
|
||||
userWalletsListRepository.userWalletsSync().forEach { wallet ->
|
||||
markPushNotificationFirstActivationDone(wallet.walletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,22 @@
|
|||
package com.tangem.features.pushnotifications.impl.model
|
||||
|
||||
import arrow.core.Either
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.model.MutableParamsContainer
|
||||
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.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.pushnotificationpreferences.IsPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.MarkPushNotificationFirstActivationDoneUseCase
|
||||
import com.tangem.domain.pushnotificationpreferences.SetAllWalletPushNotificationPreferencesUseCase
|
||||
import com.tangem.domain.settings.NeverRequestPermissionUseCase
|
||||
import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase
|
||||
import com.tangem.domain.wallets.usecase.SetNotificationsEnabledUseCase
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
|
|
@ -21,6 +26,8 @@ import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeat
|
|||
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
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
|
@ -43,7 +50,11 @@ internal class PushNotificationsModelTest {
|
|||
private val setAllWalletPushNotificationPreferences: SetAllWalletPushNotificationPreferencesUseCase =
|
||||
mockk(relaxed = true)
|
||||
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxed = true)
|
||||
private val accountsCRUDRepository: AccountsCRUDRepository = mockk(relaxed = true)
|
||||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase = mockk(relaxed = true)
|
||||
private val isPushNotificationFirstActivationDone: IsPushNotificationFirstActivationDoneUseCase =
|
||||
mockk(relaxed = true)
|
||||
private val markPushNotificationFirstActivationDone: MarkPushNotificationFirstActivationDoneUseCase =
|
||||
mockk(relaxed = true)
|
||||
private val getDoubleAskVariantUseCase: GetPushNotificationsDoubleAskVariantUseCase = mockk()
|
||||
private val modelCallbacks: PushNotificationsModelCallbacks = mockk(relaxed = true)
|
||||
|
||||
|
|
@ -162,6 +173,78 @@ internal class PushNotificationsModelTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN settings enabled WHEN onAllowPermission THEN fresh wallets get all-three and done wallets skipped`() =
|
||||
runTest {
|
||||
every { pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled } returns true
|
||||
val fresh = UserWalletId("aa")
|
||||
val done = UserWalletId("bb")
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(fresh), wallet(done))
|
||||
coEvery { isPushNotificationFirstActivationDone(fresh) } returns false
|
||||
coEvery { isPushNotificationFirstActivationDone(done) } returns true
|
||||
coEvery { setNotificationsEnabledUseCase(any(), any()) } returns Either.Right(Unit)
|
||||
coEvery {
|
||||
setAllWalletPushNotificationPreferences(any(), any(), any(), any())
|
||||
} returns Either.Right(Unit)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAllowPermission()
|
||||
advanceUntilIdle()
|
||||
|
||||
// Fresh wallet: tokens (re-subscription) before preferences, then flag marked.
|
||||
coVerifyOrder {
|
||||
setNotificationsEnabledUseCase(fresh, isEnabled = true)
|
||||
setAllWalletPushNotificationPreferences(fresh, true, true, true)
|
||||
}
|
||||
coVerify(exactly = 1) { markPushNotificationFirstActivationDone(fresh) }
|
||||
// Already-activated wallet: skipped entirely.
|
||||
coVerify(exactly = 0) { setAllWalletPushNotificationPreferences(done, any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setNotificationsEnabledUseCase(done, any()) }
|
||||
coVerify(exactly = 0) { markPushNotificationFirstActivationDone(done) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN settings enabled WHEN onDenyPermission THEN first-activation flag marked for all wallets`() = runTest {
|
||||
every { pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled } returns true
|
||||
val w1 = UserWalletId("aa")
|
||||
val w2 = UserWalletId("bb")
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(w1), wallet(w2))
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onDenyPermission()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { markPushNotificationFirstActivationDone(w1) }
|
||||
coVerify(exactly = 1) { markPushNotificationFirstActivationDone(w2) }
|
||||
// Deny only fixes the flag — it must not enable any category or re-subscribe tokens.
|
||||
coVerify(exactly = 0) { setAllWalletPushNotificationPreferences(any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN settings disabled WHEN onAllowPermission THEN no first-activation writes`() = runTest {
|
||||
every { pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled } returns false
|
||||
// Non-empty wallet list + not-done + successful stubs so that, if the toggle gate were removed,
|
||||
// applyFirstActivationRule WOULD iterate and write — making the exactly=0 assertions actually protect the gate.
|
||||
coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(wallet(UserWalletId("aa")))
|
||||
coEvery { isPushNotificationFirstActivationDone(any()) } returns false
|
||||
coEvery { setNotificationsEnabledUseCase(any(), any()) } returns Either.Right(Unit)
|
||||
coEvery { setAllWalletPushNotificationPreferences(any(), any(), any(), any()) } returns Either.Right(Unit)
|
||||
val model = createModel(testScope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
model.onAllowPermission()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { setAllWalletPushNotificationPreferences(any(), any(), any(), any()) }
|
||||
coVerify(exactly = 0) { setNotificationsEnabledUseCase(any(), any()) }
|
||||
coVerify(exactly = 0) { markPushNotificationFirstActivationDone(any()) }
|
||||
}
|
||||
|
||||
private fun wallet(id: UserWalletId): UserWallet = mockk { every { walletId } returns id }
|
||||
|
||||
private fun createModel(
|
||||
testScope: TestScope,
|
||||
isBottomSheet: Boolean = false,
|
||||
|
|
@ -186,7 +269,9 @@ internal class PushNotificationsModelTest {
|
|||
pushNotificationSettingsFeatureToggles = pushNotificationSettingsFeatureToggles,
|
||||
setAllWalletPushNotificationPreferences = setAllWalletPushNotificationPreferences,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
accountsCRUDRepository = accountsCRUDRepository,
|
||||
setNotificationsEnabledUseCase = setNotificationsEnabledUseCase,
|
||||
isPushNotificationFirstActivationDone = isPushNotificationFirstActivationDone,
|
||||
markPushNotificationFirstActivationDone = markPushNotificationFirstActivationDone,
|
||||
getPushNotificationsDoubleAskVariantUseCase = getDoubleAskVariantUseCase,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -894,6 +894,8 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun enableNotificationsIfNeeded() {
|
||||
// New first-activation owns auto-enable when the feature is on; skip the legacy path.
|
||||
if (pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled) return
|
||||
modelScope.launch {
|
||||
val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications()
|
||||
if (isUserAllowToEnableNotifications) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
|||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
|
|
@ -120,6 +121,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
private val notificationsRepository: NotificationsRepository,
|
||||
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
|
||||
private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase,
|
||||
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val reviewManager: ReviewManager,
|
||||
private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase,
|
||||
|
|
@ -373,6 +375,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
private suspend fun enableNotificationsIfNeeded() {
|
||||
// New first-activation owns auto-enable when the feature is on; skip the legacy path.
|
||||
if (pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled) return
|
||||
val alreadyEnabledWallets = notificationsRepository.getWalletAutomaticallyEnabledList().map {
|
||||
UserWalletId(it)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue