Updated on 2026-08-14

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

View file

@ -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)
}
}
}

View file

@ -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,
)
}