From fe01324d86cf35b3acb8d38f4dadcd8c16f1c51e Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Fri, 3 Oct 2025 15:38:01 +0100 Subject: [PATCH 1/2] Add Notification Prompt Experiment Manager and integrate with onboarding flow. --- .../duckduckgo/app/launch/LaunchViewModel.kt | 13 +- .../NotificationPromptExperimentManager.kt | 150 ++++++++++++++++++ .../NotificationPromptExperimentToggles.kt | 130 +++++++++++++++ .../app/onboarding/ui/page/WelcomePage.kt | 12 +- .../src/main/res/raw/privacy_config.json | 18 +++ 5 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt create mode 100644 app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentToggles.kt diff --git a/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt index 703aa714d37d..a9d74f88977d 100644 --- a/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt @@ -19,6 +19,7 @@ package com.duckduckgo.app.launch import androidx.lifecycle.ViewModel import com.duckduckgo.anvil.annotations.ContributesViewModel import com.duckduckgo.app.global.install.AppInstallStore +import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentManager import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.store.isNewUser import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager @@ -43,6 +44,7 @@ class LaunchViewModel @Inject constructor( private val daxPrompts: DaxPrompts, private val appInstallStore: AppInstallStore, private val onboardingDesignExperimentManager: OnboardingDesignExperimentManager, + private val notificationPromptExperimentManager: NotificationPromptExperimentManager, ) : ViewModel() { @@ -56,13 +58,20 @@ class LaunchViewModel @Inject constructor( } suspend fun determineViewToShow() { - if (onboardingDesignExperimentManager.isWaitForLocalPrivacyConfigEnabled()) { + val waitForLocalPrivacyOnboardingExperiment = onboardingDesignExperimentManager.isWaitForLocalPrivacyConfigEnabled() + val waitForLocalPrivacyNotificationExperiment = notificationPromptExperimentManager.isWaitForLocalPrivacyConfigEnabled() + if (waitForLocalPrivacyOnboardingExperiment || waitForLocalPrivacyNotificationExperiment) { withTimeoutOrNull(MAX_REFERRER_WAIT_TIME_MS) { val referrerJob = async { waitForReferrerData() } val configJob = async { - onboardingDesignExperimentManager.waitForPrivacyConfig() + if (waitForLocalPrivacyOnboardingExperiment) { + onboardingDesignExperimentManager.waitForPrivacyConfig() + } + if (waitForLocalPrivacyNotificationExperiment) { + notificationPromptExperimentManager.waitForPrivacyConfig() + } } awaitAll(referrerJob, configJob) } diff --git a/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt b/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt new file mode 100644 index 000000000000..370fef9f881e --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.notificationpromptexperiment + +import android.os.Build +import com.duckduckgo.app.di.AppCoroutineScope +import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentToggles.Cohorts.CONTROL +import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentToggles.Cohorts.VARIANT_NO_NOTIFICATION_PROMPT +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.appbuildconfig.api.AppBuildConfig +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.MetricsPixel +import com.duckduckgo.privacy.config.api.PrivacyConfigCallbackPlugin +import com.squareup.anvil.annotations.ContributesBinding +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +interface NotificationPromptExperimentManager { + + suspend fun enroll() + fun isControl(): Boolean + fun isExperimentalNoNotificationPrompt(): Boolean + suspend fun waitForPrivacyConfig(): Boolean + suspend fun isWaitForLocalPrivacyConfigEnabled(): Boolean + suspend fun fireDdgSetAsDefault() + suspend fun fireNotifyMeClickedLater() + suspend fun fireNotificationsEnabledLater() +} + +@ContributesMultibinding( + scope = AppScope::class, + boundType = PrivacyConfigCallbackPlugin::class, +) +@ContributesBinding( + scope = AppScope::class, + boundType = NotificationPromptExperimentManager::class, +) +@SingleInstanceIn(AppScope::class) +class NotificationPromptExperimentManagerImpl @Inject constructor( + private val dispatcherProvider: DispatcherProvider, + private val notificationPromptExperimentToggles: NotificationPromptExperimentToggles, + private val notificationPromptExperimentPixelsPlugin: NotificationPromptExperimentPixelsPlugin, + private val pixel: Pixel, + private val appBuildConfig: AppBuildConfig, + @AppCoroutineScope private val coroutineScope: CoroutineScope, +) : NotificationPromptExperimentManager, PrivacyConfigCallbackPlugin { + + private var isExperimentEnabled: Boolean? = null + private var notificationPromptExperimentCohort: NotificationPromptExperimentToggles.Cohorts? = null + + private var privacyPersisted: Boolean = false + + override suspend fun enroll() { + if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU && !appBuildConfig.isAppReinstall()) { + notificationPromptExperimentToggles.notificationPromptExperimentOct25().enroll() + } + } + + override fun isControl(): Boolean = + isExperimentEnabled == true && + notificationPromptExperimentCohort == CONTROL + + override fun isExperimentalNoNotificationPrompt(): Boolean = + isExperimentEnabled == true && + notificationPromptExperimentCohort == VARIANT_NO_NOTIFICATION_PROMPT + + override suspend fun waitForPrivacyConfig(): Boolean { + while (!privacyPersisted) { + delay(10) + } + return true + } + + override suspend fun isWaitForLocalPrivacyConfigEnabled(): Boolean = notificationPromptExperimentToggles.waitForLocalPrivacyConfig().isEnabled() + + override suspend fun fireDdgSetAsDefault() { + withContext(dispatcherProvider.io()) { + notificationPromptExperimentPixelsPlugin.getDdgSetAsDefaultMetric()?.fire() + } + } + + override suspend fun fireNotifyMeClickedLater() { + withContext(dispatcherProvider.io()) { + notificationPromptExperimentPixelsPlugin.getNotifyMeClickedLaterMetric()?.fire() + } + } + + override suspend fun fireNotificationsEnabledLater() { + withContext(dispatcherProvider.io()) { + notificationPromptExperimentPixelsPlugin.getNotificationsEnabledLaterMetric()?.fire() + } + } + + override fun onPrivacyConfigPersisted() { + privacyPersisted = true + coroutineScope.launch { + setCachedProperties() + } + } + + override fun onPrivacyConfigDownloaded() { + coroutineScope.launch { + setCachedProperties() + } + } + + private suspend fun setCachedProperties() { + withContext(dispatcherProvider.io()) { + enroll() + notificationPromptExperimentCohort = getEnrolledAndEnabledExperimentCohort() + isExperimentEnabled = + notificationPromptExperimentToggles.self().isEnabled() && notificationPromptExperimentToggles.notificationPromptExperimentOct25() + .isEnabled() + } + } + + private suspend fun getEnrolledAndEnabledExperimentCohort(): NotificationPromptExperimentToggles.Cohorts? { + val cohort = notificationPromptExperimentToggles.notificationPromptExperimentOct25().getCohort() + + return when (cohort?.name) { + CONTROL.cohortName -> CONTROL + VARIANT_NO_NOTIFICATION_PROMPT.cohortName -> VARIANT_NO_NOTIFICATION_PROMPT + else -> null + } + } + + private fun MetricsPixel.fire() = getPixelDefinitions().forEach { + pixel.fire(it.pixelName, it.params) + } +} diff --git a/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentToggles.kt b/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentToggles.kt new file mode 100644 index 000000000000..b4ff61826d80 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentToggles.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 DuckDuckGo + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.duckduckgo.app.notificationpromptexperiment + +import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentToggles.Companion.BASE_EXPERIMENT_NAME +import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.ConversionWindow +import com.duckduckgo.feature.toggles.api.MetricsPixel +import com.duckduckgo.feature.toggles.api.MetricsPixelPlugin +import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.api.Toggle.DefaultFeatureValue +import com.duckduckgo.feature.toggles.api.Toggle.State.CohortName +import com.squareup.anvil.annotations.ContributesMultibinding +import dagger.SingleInstanceIn +import javax.inject.Inject + +@ContributesRemoteFeature( + scope = AppScope::class, + featureName = BASE_EXPERIMENT_NAME, +) +interface NotificationPromptExperimentToggles { + + /** + * Toggle to enable/disable the "self" notification prompt experiment. + * Default value: false (disabled). + */ + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun self(): Toggle + + /** + * Toggle to enable/disable the "sub-feature" notification prompt experiment. + * Default value: false (disabled). + */ + @Toggle.DefaultValue(DefaultFeatureValue.FALSE) + fun notificationPromptExperimentOct25(): Toggle + + /** + * Toggle to enable/disable the "sub-feature" waitForLocalPrivacyConfig. + * Default value: true (enabled). + */ + @Toggle.DefaultValue(DefaultFeatureValue.TRUE) + fun waitForLocalPrivacyConfig(): Toggle + + enum class Cohorts(override val cohortName: String) : CohortName { + // Current experience, where the Notification Prompt is shown at app start after fresh install. + CONTROL("control"), + + // New experience, where the Notification Prompt is never shown at app start after fresh install. + VARIANT_NO_NOTIFICATION_PROMPT("experimentalNoNotificationPrompt"), + } + + companion object { + internal const val BASE_EXPERIMENT_NAME = "notificationPromptExperiment" + } +} + +@ContributesMultibinding(AppScope::class) +@SingleInstanceIn(AppScope::class) +class NotificationPromptExperimentPixelsPlugin @Inject constructor( + private val toggles: NotificationPromptExperimentToggles, +) : MetricsPixelPlugin { + + override suspend fun getMetrics(): List { + return listOf( + MetricsPixel( + metric = METRIC_NOTIFICATION_PROMPT_DDG_SET_AS_DEFAULT, + value = "1", + toggle = toggles.notificationPromptExperimentOct25(), + conversionWindow = listOf( + ConversionWindow(lowerWindow = 0, upperWindow = 0), + ), + ), + MetricsPixel( + metric = METRIC_NOTIFICATION_PROMPT_NOTIFY_ME_CLICKED_LATER, + value = "1", + toggle = toggles.notificationPromptExperimentOct25(), + conversionWindow = listOf( + ConversionWindow(lowerWindow = 0, upperWindow = 0), + ConversionWindow(lowerWindow = 1, upperWindow = 1), + ConversionWindow(lowerWindow = 2, upperWindow = 7), + ConversionWindow(lowerWindow = 8, upperWindow = 14), + ), + ), + MetricsPixel( + metric = METRIC_NOTIFICATION_PROMPT_NOTIFICATIONS_ENABLED_LATER, + value = "1", + toggle = toggles.notificationPromptExperimentOct25(), + conversionWindow = listOf( + ConversionWindow(lowerWindow = 0, upperWindow = 0), + ConversionWindow(lowerWindow = 1, upperWindow = 1), + ConversionWindow(lowerWindow = 2, upperWindow = 7), + ConversionWindow(lowerWindow = 8, upperWindow = 14), + ), + ), + ) + } + + suspend fun getDdgSetAsDefaultMetric(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_NOTIFICATION_PROMPT_DDG_SET_AS_DEFAULT } + } + + suspend fun getNotifyMeClickedLaterMetric(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_NOTIFICATION_PROMPT_NOTIFY_ME_CLICKED_LATER } + } + + suspend fun getNotificationsEnabledLaterMetric(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == METRIC_NOTIFICATION_PROMPT_NOTIFICATIONS_ENABLED_LATER } + } + + companion object { + internal const val METRIC_NOTIFICATION_PROMPT_DDG_SET_AS_DEFAULT = "ddgSetAsDefault" + internal const val METRIC_NOTIFICATION_PROMPT_NOTIFY_ME_CLICKED_LATER = "notifyMeClickedLater" + internal const val METRIC_NOTIFICATION_PROMPT_NOTIFICATIONS_ENABLED_LATER = "notificationsEnabledLater" + } +} diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt index 48c9ff98e853..e74850eab115 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/WelcomePage.kt @@ -37,6 +37,7 @@ import androidx.transition.TransitionManager import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.app.browser.R import com.duckduckgo.app.browser.databinding.ContentOnboardingWelcomePageBinding +import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentManager import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.ADDRESS_BAR_POSITION import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.COMPARISON_CHART import com.duckduckgo.app.onboarding.ui.page.PreOnboardingDialogType.INITIAL @@ -80,6 +81,9 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p @Inject lateinit var onboardingDesignExperimentManager: OnboardingDesignExperimentManager + @Inject + lateinit var notificationPromptExperimentManager: NotificationPromptExperimentManager + private val binding: ContentOnboardingWelcomePageBinding by viewBinding() private val viewModel by lazy { ViewModelProvider(this, viewModelFactory)[WelcomePageViewModel::class.java] @@ -184,8 +188,12 @@ class WelcomePage : OnboardingPageFragment(R.layout.content_onboarding_welcome_p @SuppressLint("InlinedApi") private fun requestNotificationsPermissions() { if (appBuildConfig.sdkInt >= android.os.Build.VERSION_CODES.TIRAMISU) { - viewModel.notificationRuntimePermissionRequested() - requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + if (notificationPromptExperimentManager.isControl()) { + viewModel.notificationRuntimePermissionRequested() + requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + scheduleWelcomeAnimation() + } } else { scheduleWelcomeAnimation() } diff --git a/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json b/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json index 7d1ee7fbaac0..59619d230421 100644 --- a/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json +++ b/privacy-config/privacy-config-impl/src/main/res/raw/privacy_config.json @@ -2,6 +2,24 @@ "readme": "https://github.com/duckduckgo/privacy-configuration", "version": 1652275711516, "features": { + "notificationPromptExperiment": { + "state": "enabled", + "features": { + "notificationPromptExperimentOct25": { + "state": "enabled", + "cohorts": [ + { + "name": "control", + "weight": 1 + }, + { + "name": "experimentalNoNotificationPrompt", + "weight": 1 + } + ] + } + } + }, "onboardingDesignExperiment": { "state": "enabled", "features": { From edbbcd7fa174329e90a8f7404a22e5e6da784b9e Mon Sep 17 00:00:00 2001 From: Ana Capatina Date: Wed, 22 Oct 2025 18:15:27 +0100 Subject: [PATCH 2/2] Fixed lint and tests. --- .../NotificationPromptExperimentManager.kt | 2 ++ .../duckduckgo/app/launch/LaunchViewModelTest.kt | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt b/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt index 370fef9f881e..1fa8a8e90b33 100644 --- a/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt +++ b/app/src/main/java/com/duckduckgo/app/notificationpromptexperiment/NotificationPromptExperimentManager.kt @@ -16,6 +16,7 @@ package com.duckduckgo.app.notificationpromptexperiment +import android.annotation.SuppressLint import android.os.Build import com.duckduckgo.app.di.AppCoroutineScope import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentToggles.Cohorts.CONTROL @@ -70,6 +71,7 @@ class NotificationPromptExperimentManagerImpl @Inject constructor( private var privacyPersisted: Boolean = false + @SuppressLint("DenyListedApi") override suspend fun enroll() { if (appBuildConfig.sdkInt >= Build.VERSION_CODES.TIRAMISU && !appBuildConfig.isAppReinstall()) { notificationPromptExperimentToggles.notificationPromptExperimentOct25().enroll() diff --git a/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt index fac91e79cbb1..0ff68024d0e7 100644 --- a/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/launch/LaunchViewModelTest.kt @@ -22,6 +22,7 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.launch.LaunchViewModel.Command.DaxPromptBrowserComparison import com.duckduckgo.app.launch.LaunchViewModel.Command.Home import com.duckduckgo.app.launch.LaunchViewModel.Command.Onboarding +import com.duckduckgo.app.notificationpromptexperiment.NotificationPromptExperimentManager import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboardingdesignexperiment.OnboardingDesignExperimentManager @@ -55,12 +56,14 @@ class LaunchViewModelTest { private val mockDaxPrompts: DaxPrompts = mock() private val mockAppInstallStore: AppInstallStore = mock() private val mockOnboardingExperiment: OnboardingDesignExperimentManager = mock() + private val mockNotificationPromptExperiment: NotificationPromptExperimentManager = mock() private lateinit var testee: LaunchViewModel @Before fun before() = runTest { whenever(mockOnboardingExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(false) + whenever(mockNotificationPromptExperiment.isWaitForLocalPrivacyConfigEnabled()).thenReturn(false) } @After @@ -76,6 +79,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) @@ -94,6 +98,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) @@ -112,6 +117,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) @@ -130,6 +136,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) @@ -146,6 +153,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) @@ -162,6 +170,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) @@ -178,6 +187,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.SHOW_BROWSER_COMPARISON_PROMPT) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) @@ -195,6 +205,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) @@ -215,6 +226,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.DAX_ONBOARDING) @@ -236,6 +248,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW) @@ -260,6 +273,7 @@ class LaunchViewModelTest { mockDaxPrompts, mockAppInstallStore, mockOnboardingExperiment, + mockNotificationPromptExperiment, ) whenever(mockDaxPrompts.evaluate()).thenReturn(ActionType.NONE) whenever(userStageStore.getUserAppStage()).thenReturn(AppStage.NEW)