Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 22:43:52 +03:00
commit d6f9f59866
1729 changed files with 67614 additions and 9361 deletions

View file

@ -21,6 +21,7 @@ dependencies {
implementation(deps.compose.foundation)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
implementation(deps.lifecycle.compose)
/** Other dependencies */
implementation(deps.arrow.core)
@ -34,6 +35,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.utils)
implementation(projects.core.abTests)
/** Common modules */
implementation(projects.common.routing)
@ -41,6 +43,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)
@ -49,4 +55,10 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Test */
testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(deps.test.coroutine)
}

View file

@ -2,8 +2,10 @@ package com.tangem.features.pushnotifications.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
@ -12,6 +14,8 @@ import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsUM
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsDoubleAskSheetState
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -26,12 +30,21 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val activity = LocalContext.current.findActivity()
val isDoubleAskSheetShown by model.isDoubleAskSheetShown.collectAsStateWithLifecycle()
BackHandler(onBack = { activity.finish() })
NavigationBar3ButtonsScrim()
PushNotificationsScreen(
isPushNotificationSettingsEnabled = model.isPushNotificationSettingsEnabled,
state = PushNotificationsUM(
isPushNotificationSettingsEnabled = model.isPushNotificationSettingsEnabled,
doubleAskSheet = PushNotificationsDoubleAskSheetState(
isShown = isDoubleAskSheetShown,
onEnableClick = model::onDoubleAskEnableClick,
onSkipClick = model::onDoubleAskSkipClick,
onDismiss = model::onDoubleAskDismiss,
),
),
onAllowClick = model::onAllowClick,
onLaterClick = model::onLaterClick,
onAllowPermission = model::onAllowPermission,

View file

@ -0,0 +1,16 @@
package com.tangem.features.pushnotifications.impl
import com.tangem.core.configtoggle.FeatureToggles
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import javax.inject.Inject
internal class DefaultPushNotificationsFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) : PushNotificationsFeatureToggles {
override val isOnboardingPushDoubleAskAbEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
FeatureToggles.AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED,
)
}

View file

@ -1,10 +1,12 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsBottomSheetComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsComponent
import com.tangem.features.pushnotifications.impl.DefaultPushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.impl.model.PushNotificationsModel
import dagger.Binds
import dagger.Module
@ -12,6 +14,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
@ -29,4 +32,8 @@ internal interface PushNotificationsModule {
@IntoMap
@ClassKey(PushNotificationsModel::class)
fun bindModel(model: PushNotificationsModel): Model
@Binds
@Singleton
fun bindFeatureToggles(impl: DefaultPushNotificationsFeatureToggles): PushNotificationsFeatureToggles
}

View file

@ -0,0 +1,12 @@
package com.tangem.features.pushnotifications.impl.domain
enum class DoubleAskVariant(val key: String) {
Off(key = "control"),
On(key = "treatment"),
;
companion object {
fun fromKey(value: String): DoubleAskVariant =
entries.firstOrNull { it.key.equals(value, ignoreCase = true) } ?: Off
}
}

View file

@ -0,0 +1,23 @@
package com.tangem.features.pushnotifications.impl.domain
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import javax.inject.Inject
class GetPushNotificationsDoubleAskVariantUseCase @Inject constructor(
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
private val abTestsManager: ABTestsManager,
) {
suspend operator fun invoke(): DoubleAskVariant {
if (!pushNotificationsFeatureToggles.isOnboardingPushDoubleAskAbEnabled) {
return DoubleAskVariant.Off
}
val variant = abTestsManager.getValue(AMPLITUDE_ID, DoubleAskVariant.Off.key)
return DoubleAskVariant.fromKey(variant)
}
private companion object {
const val AMPLITUDE_ID = "twi_1403_onboarding_push_notification_double_ask"
}
}

View file

@ -8,4 +8,10 @@ internal interface PushNotificationsClickIntents {
fun onAllowPermission()
fun onDenyPermission()
fun onDoubleAskEnableClick()
fun onDoubleAskSkipClick()
fun onDoubleAskDismiss()
}

View file

@ -5,17 +5,27 @@ 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
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.impl.domain.GetPushNotificationsDoubleAskVariantUseCase
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
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -31,6 +41,10 @@ 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,
private val getPushNotificationsDoubleAskVariantUseCase: GetPushNotificationsDoubleAskVariantUseCase,
) : Model(), PushNotificationsClickIntents {
val params: PushNotificationsParams = paramsContainer.require()
@ -43,6 +57,11 @@ internal class PushNotificationsModel @Inject constructor(
AppRoute.PushNotification.Source.Onboarding -> AnalyticsParam.ScreensSources.Onboarding
}
private val _isDoubleAskSheetShown = MutableStateFlow(false)
val isDoubleAskSheetShown: StateFlow<Boolean> = _isDoubleAskSheetShown.asStateFlow()
private var resolvedVariant: String = DoubleAskVariant.Off.key
init {
analyticHandler.send(PushNotificationAnalyticEvents.NotificationsScreenOpened(source))
}
@ -56,16 +75,50 @@ internal class PushNotificationsModel @Inject constructor(
override fun onLaterClick() {
analyticHandler.send(PushNotificationAnalyticEvents.ButtonLater(source))
modelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
if (params.isBottomSheet) {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false)
} else {
params.nextRoute?.let { appRouter.push(it) }
}
params.modelCallbacks.onDenySystemPermission()
if (isOnWalletScreen()) {
modelScope.launch { proceedAfterLater() }
return
}
modelScope.launch {
val variant = getPushNotificationsDoubleAskVariantUseCase()
resolvedVariant = variant.key
if (variant == DoubleAskVariant.On) {
analyticHandler.send(PushNotificationAnalyticEvents.WarningScreenShown(source, resolvedVariant))
_isDoubleAskSheetShown.value = true
} else {
proceedAfterLater()
}
}
}
override fun onDoubleAskEnableClick() {
analyticHandler.send(PushNotificationAnalyticEvents.WarningScreenEnableTapped(source, resolvedVariant))
modelScope.launch {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true)
}
}
override fun onDoubleAskSkipClick() {
analyticHandler.send(PushNotificationAnalyticEvents.WarningScreenSkipTapped(source, resolvedVariant))
modelScope.launch { proceedAfterLater() }
}
override fun onDoubleAskDismiss() {
_isDoubleAskSheetShown.value = false
}
private fun isOnWalletScreen(): Boolean =
params.isBottomSheet && params.source == AppRoute.PushNotification.Source.Main
private suspend fun proceedAfterLater() {
neverRequestPermissionUseCase(PUSH_PERMISSION)
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
if (params.isBottomSheet) {
notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false)
} else {
params.nextRoute?.let { appRouter.push(it) }
}
params.modelCallbacks.onDenySystemPermission()
}
override fun onAllowPermission() {
@ -75,6 +128,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 +151,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) }
}
}
}
}

View file

@ -1,20 +1,49 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.icon
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
import com.tangem.core.ui.components.bottomsheets.message.messageBottomSheetUM
import com.tangem.core.ui.components.bottomsheets.message.onClick
import com.tangem.core.ui.components.bottomsheets.message.primaryButton
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
import com.tangem.core.ui.components.showcase.Showcase
import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.requestPermission
import com.tangem.feature.pushnotifications.impl.R
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import kotlinx.collections.immutable.persistentListOf
/**
* Holds the treatment-variant "Double Ask" bottom sheet state and callbacks for the onboarding soft-ask.
*/
@Immutable
internal data class PushNotificationsDoubleAskSheetState(
val isShown: Boolean,
val onEnableClick: () -> Unit,
val onSkipClick: () -> Unit,
val onDismiss: () -> Unit,
)
@Immutable
internal data class PushNotificationsUM(
val isPushNotificationSettingsEnabled: Boolean,
val doubleAskSheet: PushNotificationsDoubleAskSheetState,
)
@Composable
internal fun PushNotificationsScreen(
isPushNotificationSettingsEnabled: Boolean,
state: PushNotificationsUM,
onAllowClick: () -> Unit,
onLaterClick: () -> Unit,
onAllowPermission: () -> Unit,
@ -26,12 +55,12 @@ internal fun PushNotificationsScreen(
permission = PUSH_PERMISSION,
)
val argumentTwoTitleRes = if (isPushNotificationSettingsEnabled) {
val argumentTwoTitleRes = if (state.isPushNotificationSettingsEnabled) {
R.string.user_push_notification_agreement_argument_two_title_v2
} else {
R.string.user_push_notification_agreement_argument_two_title
}
val argumentTwoSubtitleRes = if (isPushNotificationSettingsEnabled) {
val argumentTwoSubtitleRes = if (state.isPushNotificationSettingsEnabled) {
R.string.user_push_notification_agreement_argument_two_subtitle_v2
} else {
R.string.user_push_notification_agreement_argument_two_subtitle
@ -65,4 +94,84 @@ internal fun PushNotificationsScreen(
),
modifier = Modifier.systemBarsPadding(),
)
if (state.doubleAskSheet.isShown) {
PushNotificationsDoubleAskBottomSheet(
onEnableClick = {
state.doubleAskSheet.onEnableClick()
requestPushPermission()
},
onSkipClick = state.doubleAskSheet.onSkipClick,
onDismiss = state.doubleAskSheet.onDismiss,
)
}
}
@Composable
private fun PushNotificationsDoubleAskBottomSheet(
onEnableClick: () -> Unit,
onSkipClick: () -> Unit,
onDismiss: () -> Unit,
) {
MessageBottomSheet(
state = messageBottomSheetUM {
infoBlock {
icon(com.tangem.core.ui.R.drawable.ic_attention_default_24) {
type = MessageBottomSheetUM.Icon.Type.Attention
backgroundType = MessageBottomSheetUM.Icon.BackgroundType.Attention
}
title = resourceReference(R.string.push_notification_warning_sheet_title)
body = resourceReference(R.string.push_notification_warning_sheet_description)
}
primaryButton {
text = resourceReference(R.string.push_notification_warning_sheet_button_enable)
onClick { onEnableClick() }
}
secondaryButton {
text = resourceReference(R.string.common_skip)
onClick { onSkipClick() }
}
},
onDismissRequest = onDismiss,
)
}
private fun previewState(isDoubleAskShown: Boolean) = PushNotificationsUM(
isPushNotificationSettingsEnabled = true,
doubleAskSheet = PushNotificationsDoubleAskSheetState(
isShown = isDoubleAskShown,
onEnableClick = {},
onSkipClick = {},
onDismiss = {},
),
)
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_PushNotificationsScreen() {
TangemThemePreview {
PushNotificationsScreen(
state = previewState(isDoubleAskShown = false),
onAllowClick = {},
onLaterClick = {},
onAllowPermission = {},
onDenyPermission = {},
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_PushNotificationsScreen_DoubleAsk() {
TangemThemePreview {
PushNotificationsScreen(
state = previewState(isDoubleAskShown = true),
onAllowClick = {},
onLaterClick = {},
onAllowPermission = {},
onDenyPermission = {},
)
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.features.pushnotifications.impl.domain
import com.google.common.truth.Truth.assertThat
import com.tangem.core.abtests.manager.ABTestsManager
import com.tangem.features.pushnotifications.PushNotificationsFeatureToggles
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
internal class GetPushNotificationsDoubleAskVariantUseCaseTest {
private val featureToggles: PushNotificationsFeatureToggles = mockk()
private val abTestsManager: ABTestsManager = mockk()
private val useCase = GetPushNotificationsDoubleAskVariantUseCase(
pushNotificationsFeatureToggles = featureToggles,
abTestsManager = abTestsManager,
)
@Test
fun `GIVEN toggle disabled WHEN invoke THEN returns Off and AB not queried`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns false
val result = useCase()
assertThat(result).isEqualTo(DoubleAskVariant.Off)
coVerify(exactly = 0) { abTestsManager.getValue(any(), any()) }
}
@Test
fun `GIVEN toggle enabled AND AB returns treatment WHEN invoke THEN returns On`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns true
coEvery { abTestsManager.getValue(KEY, "control") } returns "treatment"
val result = useCase()
assertThat(result).isEqualTo(DoubleAskVariant.On)
coVerify(exactly = 1) { abTestsManager.getValue(KEY, "control") }
}
@Test
fun `GIVEN toggle enabled AND AB returns control WHEN invoke THEN returns Off`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns true
coEvery { abTestsManager.getValue(KEY, "control") } returns "control"
assertThat(useCase()).isEqualTo(DoubleAskVariant.Off)
}
@Test
fun `GIVEN toggle enabled AND AB returns unknown WHEN invoke THEN returns Off`() = runTest {
every { featureToggles.isOnboardingPushDoubleAskAbEnabled } returns true
coEvery { abTestsManager.getValue(KEY, "control") } returns "unexpected_value"
assertThat(useCase()).isEqualTo(DoubleAskVariant.Off)
}
private companion object {
const val KEY = "twi_1403_onboarding_push_notification_double_ask"
}
}

View file

@ -0,0 +1,204 @@
package com.tangem.features.pushnotifications.impl.model
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.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.PushNotificationsModelCallbacks
import com.tangem.features.pushnotifications.api.PushNotificationsParams
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
import com.tangem.features.pushnotifications.impl.domain.GetPushNotificationsDoubleAskVariantUseCase
import com.tangem.features.pushnotifications.impl.domain.DoubleAskVariant
import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
internal class PushNotificationsModelTest {
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase = mockk(relaxed = true)
private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase = mockk(relaxed = true)
private val appRouter: AppRouter = mockk(relaxed = true)
private val analyticHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val notificationsRepository: NotificationsRepository = mockk(relaxed = true)
private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles = mockk(relaxed = true)
private val setAllWalletPushNotificationPreferences: SetAllWalletPushNotificationPreferencesUseCase =
mockk(relaxed = true)
private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxed = true)
private val accountsCRUDRepository: AccountsCRUDRepository = mockk(relaxed = true)
private val getDoubleAskVariantUseCase: GetPushNotificationsDoubleAskVariantUseCase = mockk()
private val modelCallbacks: PushNotificationsModelCallbacks = mockk(relaxed = true)
@BeforeEach
fun setUp() {
coEvery { getDoubleAskVariantUseCase() } returns DoubleAskVariant.Off
}
@Test
fun `GIVEN onboarding treatment WHEN onLaterClick THEN double ask shown and not proceeded`() = runTest {
coEvery { getDoubleAskVariantUseCase() } returns DoubleAskVariant.On
val model = createModel(testScope = this)
advanceUntilIdle()
model.onLaterClick()
advanceUntilIdle()
assertThat(model.isDoubleAskSheetShown.value).isTrue()
verify {
analyticHandler.send(
match<PushNotificationAnalyticEvents.WarningScreenShown> {
it.variant == DoubleAskVariant.On.key
},
)
}
coVerify(exactly = 0) { neverRequestPermissionUseCase(any()) }
verify(exactly = 0) { modelCallbacks.onDenySystemPermission() }
}
@Test
fun `GIVEN onboarding control WHEN onLaterClick THEN proceeds without double ask`() = runTest {
coEvery { getDoubleAskVariantUseCase() } returns DoubleAskVariant.Off
val model = createModel(testScope = this)
advanceUntilIdle()
model.onLaterClick()
advanceUntilIdle()
assertThat(model.isDoubleAskSheetShown.value).isFalse()
coVerify { neverRequestPermissionUseCase(any()) }
coVerify { neverToInitiallyAskPermissionUseCase(any()) }
verify { modelCallbacks.onDenySystemPermission() }
verify(exactly = 0) {
analyticHandler.send(match<PushNotificationAnalyticEvents.WarningScreenShown> { true })
}
}
@Test
fun `GIVEN main bottom sheet WHEN onLaterClick THEN double ask not shown and variant not resolved`() = runTest {
val model = createModel(
testScope = this,
isBottomSheet = true,
source = AppRoute.PushNotification.Source.Main,
)
advanceUntilIdle()
model.onLaterClick()
advanceUntilIdle()
assertThat(model.isDoubleAskSheetShown.value).isFalse()
coVerify(exactly = 0) { getDoubleAskVariantUseCase() }
verify { modelCallbacks.onDenySystemPermission() }
}
@Test
fun `GIVEN double ask shown WHEN onDoubleAskEnableClick THEN enable tapped sent and not proceeded`() = runTest {
coEvery { getDoubleAskVariantUseCase() } returns DoubleAskVariant.On
val model = createModel(testScope = this)
advanceUntilIdle()
model.onLaterClick()
advanceUntilIdle()
model.onDoubleAskEnableClick()
advanceUntilIdle()
verify {
analyticHandler.send(match<PushNotificationAnalyticEvents.WarningScreenEnableTapped> { true })
}
coVerify { notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true) }
verify(exactly = 0) { modelCallbacks.onDenySystemPermission() }
}
@Test
fun `GIVEN double ask shown WHEN onDoubleAskSkipClick THEN event sent and proceeded`() = runTest {
coEvery { getDoubleAskVariantUseCase() } returns DoubleAskVariant.On
val model = createModel(testScope = this)
advanceUntilIdle()
model.onLaterClick()
advanceUntilIdle()
model.onDoubleAskSkipClick()
advanceUntilIdle()
verify {
analyticHandler.send(match<PushNotificationAnalyticEvents.WarningScreenSkipTapped> { true })
}
coVerify { neverRequestPermissionUseCase(any()) }
verify { modelCallbacks.onDenySystemPermission() }
}
@Test
fun `GIVEN double ask shown WHEN onDoubleAskDismiss THEN sheet hidden and not proceeded`() = runTest {
coEvery { getDoubleAskVariantUseCase() } returns DoubleAskVariant.On
val model = createModel(testScope = this)
advanceUntilIdle()
model.onLaterClick()
advanceUntilIdle()
model.onDoubleAskDismiss()
advanceUntilIdle()
assertThat(model.isDoubleAskSheetShown.value).isFalse()
verify(exactly = 0) { modelCallbacks.onDenySystemPermission() }
verify(exactly = 0) {
analyticHandler.send(match<PushNotificationAnalyticEvents.WarningScreenSkipTapped> { true })
}
}
private fun createModel(
testScope: TestScope,
isBottomSheet: Boolean = false,
source: AppRoute.PushNotification.Source = AppRoute.PushNotification.Source.Onboarding,
paramsContainer: ParamsContainer = MutableParamsContainer(
value = PushNotificationsParams(
isBottomSheet = isBottomSheet,
nextRoute = null,
modelCallbacks = modelCallbacks,
source = source,
),
),
): PushNotificationsModel {
return PushNotificationsModel(
paramsContainer = paramsContainer,
dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
neverRequestPermissionUseCase = neverRequestPermissionUseCase,
neverToInitiallyAskPermissionUseCase = neverToInitiallyAskPermissionUseCase,
appRouter = appRouter,
analyticHandler = analyticHandler,
notificationsRepository = notificationsRepository,
pushNotificationSettingsFeatureToggles = pushNotificationSettingsFeatureToggles,
setAllWalletPushNotificationPreferences = setAllWalletPushNotificationPreferences,
userWalletsListRepository = userWalletsListRepository,
accountsCRUDRepository = accountsCRUDRepository,
getPushNotificationsDoubleAskVariantUseCase = getDoubleAskVariantUseCase,
)
}
private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
val testDispatcher = StandardTestDispatcher(testScheduler)
return TestingCoroutineDispatcherProvider(
main = testDispatcher,
mainImmediate = testDispatcher,
io = testDispatcher,
default = testDispatcher,
single = testDispatcher,
)
}
}