Updated on 2026-08-14
This commit is contained in:
commit
d6f9f59866
1729 changed files with 67614 additions and 9361 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,4 +8,10 @@ internal interface PushNotificationsClickIntents {
|
|||
fun onAllowPermission()
|
||||
|
||||
fun onDenyPermission()
|
||||
|
||||
fun onDoubleAskEnableClick()
|
||||
|
||||
fun onDoubleAskSkipClick()
|
||||
|
||||
fun onDoubleAskDismiss()
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue