From 4369164ce96c81793119c17258a106e666419f2e Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Thu, 31 Oct 2024 17:33:39 +0000 Subject: [PATCH 1/8] Bring privacy pro onboarding dialog to all users --- .../app/browser/BrowserTabFragment.kt | 2 +- .../app/browser/BrowserTabViewModel.kt | 8 +++++--- .../main/java/com/duckduckgo/app/cta/ui/Cta.kt | 18 ++++++++++++++++++ .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 14 +++++++++----- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt index 73ba576b7c78..bb1bb5713703 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt @@ -3847,7 +3847,7 @@ class BrowserTabFragment : when (configuration) { is HomePanelCta -> showHomeCta(configuration) is DaxBubbleCta.DaxExperimentIntroSearchOptionsCta, is DaxBubbleCta.DaxExperimentIntroVisitSiteOptionsCta, - is DaxBubbleCta.DaxExperimentEndCta, + is DaxBubbleCta.DaxExperimentEndCta, is DaxBubbleCta.DaxExperimentPrivacyProCta, -> showDaxExperimentOnboardingBubbleCta(configuration as DaxBubbleCta) is DaxBubbleCta -> showDaxOnboardingBubbleCta(configuration) is OnboardingDaxDialogCta -> showOnboardingDialogCta(configuration) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index bf77ed639b95..4e1124a67579 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -2619,7 +2619,7 @@ class BrowserTabViewModel @Inject constructor( } private fun showOrHideKeyboard(cta: Cta?) { - val shouldHideKeyboard = cta is HomePanelCta || cta is DaxBubbleCta.DaxPrivacyProCta + val shouldHideKeyboard = cta is HomePanelCta || cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta command.value = if (shouldHideKeyboard) HideKeyboard else ShowKeyboard } @@ -2647,7 +2647,7 @@ class BrowserTabViewModel @Inject constructor( fun onUserClickCtaSecondaryButton(cta: Cta) { viewModelScope.launch { ctaViewModel.onUserDismissedCta(cta) - if (cta is DaxBubbleCta.DaxPrivacyProCta) { + if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) { val updatedCta = refreshCta() ctaViewState.value = currentCtaViewState().copy(cta = updatedCta) } @@ -3433,7 +3433,9 @@ class BrowserTabViewModel @Inject constructor( private fun onDaxBubbleCtaOkButtonClicked(cta: DaxBubbleCta): Command? { onUserDismissedCta(cta) return when (cta) { - is DaxBubbleCta.DaxPrivacyProCta -> LaunchPrivacyPro("https://duckduckgo.com/pro?origin=funnel_pro_android_onboarding".toUri()) + is DaxBubbleCta.DaxPrivacyProCta, is DaxBubbleCta.DaxExperimentPrivacyProCta -> LaunchPrivacyPro( + "https://duckduckgo.com/pro?origin=funnel_pro_android_onboarding".toUri(), + ) is DaxBubbleCta.DaxEndCta, is DaxBubbleCta.DaxExperimentEndCta -> { viewModelScope.launch { val updatedCta = refreshCta() diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index c4e3c235e605..2b2dd259d8ec 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -1030,6 +1030,24 @@ sealed class DaxBubbleCta( appInstallStore = appInstallStore, ) + data class DaxExperimentPrivacyProCta( + override val onboardingStore: OnboardingStore, + override val appInstallStore: AppInstallStore, + ) : DaxBubbleCta( + ctaId = CtaId.DAX_INTRO_PRIVACY_PRO, + title = R.string.onboardingPrivacyProDaxDialogTitle, + description = R.string.onboardingPrivacyProDaxDialogDescription, + placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, + primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, + secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, + shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN, + okPixel = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, + cancelPixel = AppPixelName.ONBOARDING_DAX_CTA_CANCEL_BUTTON, + ctaPixelParam = Pixel.PixelValues.DAX_PRIVACY_PRO, + onboardingStore = onboardingStore, + appInstallStore = appInstallStore, + ) + data class DaxDialogIntroOption( val optionText: String, @DrawableRes val iconRes: Int, diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 36febc9b8554..e250143a6735 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -87,8 +87,8 @@ class CtaViewModel @Inject constructor( } } - private val requiredDaxOnboardingCtas: Array by lazy { - if (extendedOnboardingFeatureToggles.privacyProCta().isEnabled()) { + private suspend fun requiredDaxOnboardingCtas(): Array { + return if (extendedOnboardingFeatureToggles.privacyProCta().isEnabled()) { // TODO NOELIA add subscriptions.isEligible() arrayOf( CtaId.DAX_INTRO, CtaId.DAX_DIALOG_SERP, @@ -238,8 +238,12 @@ class CtaViewModel @Inject constructor( } } - canShowPrivacyProCta() && extendedOnboardingFeatureToggles.privacyProCta().isEnabled() -> { - DaxBubbleCta.DaxPrivacyProCta(onboardingStore, appInstallStore) + canShowPrivacyProCta() && extendedOnboardingFeatureToggles.privacyProCta().isEnabled() -> { // TODO NOELIA add subscriptions.isEligible() + if (highlightsOnboardingExperimentManager.isHighlightsEnabled()) { + DaxBubbleCta.DaxExperimentPrivacyProCta(onboardingStore, appInstallStore) + } else { + DaxBubbleCta.DaxPrivacyProCta(onboardingStore, appInstallStore) + } } canShowWidgetCta() -> { @@ -427,7 +431,7 @@ class CtaViewModel @Inject constructor( private suspend fun allOnboardingCtasShown(): Boolean { return withContext(dispatchers.io()) { - requiredDaxOnboardingCtas.all { + requiredDaxOnboardingCtas().all { dismissedCtaDao.exists(it) } } From 66ba75a25b4e685493ebb1f832fbfa2161060a50 Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Thu, 31 Oct 2024 17:40:48 +0000 Subject: [PATCH 2/8] Set privacyPro dialog under a feature flag --- .../main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt | 8 +++++--- .../ExtendedOnboardingFeatureToggles.kt | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index e250143a6735..a2270e9bf3d5 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -88,7 +88,8 @@ class CtaViewModel @Inject constructor( } private suspend fun requiredDaxOnboardingCtas(): Array { - return if (extendedOnboardingFeatureToggles.privacyProCta().isEnabled()) { // TODO NOELIA add subscriptions.isEligible() + val shouldShowPrivacyProCta = extendedOnboardingFeatureToggles.privacyProCta().isEnabled() && subscriptions.isEligible() + return if (shouldShowPrivacyProCta) { arrayOf( CtaId.DAX_INTRO, CtaId.DAX_DIALOG_SERP, @@ -238,7 +239,7 @@ class CtaViewModel @Inject constructor( } } - canShowPrivacyProCta() && extendedOnboardingFeatureToggles.privacyProCta().isEnabled() -> { // TODO NOELIA add subscriptions.isEligible() + canShowPrivacyProCta() -> { if (highlightsOnboardingExperimentManager.isHighlightsEnabled()) { DaxBubbleCta.DaxExperimentPrivacyProCta(onboardingStore, appInstallStore) } else { @@ -287,7 +288,8 @@ class CtaViewModel @Inject constructor( } private suspend fun canShowPrivacyProCta(): Boolean { - return daxOnboardingActive() && !hideTips() && !daxDialogPrivacyProShown() + return daxOnboardingActive() && !hideTips() && !daxDialogPrivacyProShown() && subscriptions.isEligible() && + extendedOnboardingFeatureToggles.privacyProCta().isEnabled() } @WorkerThread diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt index f52ad60f546b..e748f26f530f 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt @@ -31,11 +31,9 @@ interface ExtendedOnboardingFeatureToggles { fun self(): Toggle @Toggle.DefaultValue(false) - @Experiment fun noBrowserCtas(): Toggle @Toggle.DefaultValue(false) - @Experiment fun privacyProCta(): Toggle @Toggle.DefaultValue(false) From b9387426d6af01098b4e79498cc085db727ff7d4 Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Wed, 6 Nov 2024 23:36:26 +0000 Subject: [PATCH 3/8] Added experiment sub feature and cohorts --- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 15 +++--- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 4 +- .../app/onboarding/store/OnboardingStore.kt | 2 + .../onboarding/store/OnboardingStoreImpl.kt | 46 ++++++++++++++++++- .../ExtendedOnboardingFeatureToggles.kt | 11 +++++ app/src/main/res/values/donottranslate.xml | 14 ++++++ 6 files changed, 83 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 2b2dd259d8ec..ce877691d326 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -37,6 +37,9 @@ import com.duckduckgo.app.cta.ui.DaxCta.Companion.MAX_DAYS_ALLOWED import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.onboarding.store.OnboardingStore +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.DESCRIPTION +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.PRIMARY_BUTTON +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.TITLE import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel @@ -970,10 +973,10 @@ sealed class DaxBubbleCta( override val appInstallStore: AppInstallStore, ) : DaxBubbleCta( ctaId = CtaId.DAX_INTRO_PRIVACY_PRO, - title = R.string.onboardingPrivacyProDaxDialogTitle, - description = R.string.onboardingPrivacyProDaxDialogDescription, + title = onboardingStore.getPrivacyProContent(TITLE), + description = onboardingStore.getPrivacyProContent(DESCRIPTION), placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, - primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, + primaryCta = onboardingStore.getPrivacyProContent(PRIMARY_BUTTON), secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN, okPixel = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, @@ -1035,10 +1038,10 @@ sealed class DaxBubbleCta( override val appInstallStore: AppInstallStore, ) : DaxBubbleCta( ctaId = CtaId.DAX_INTRO_PRIVACY_PRO, - title = R.string.onboardingPrivacyProDaxDialogTitle, - description = R.string.onboardingPrivacyProDaxDialogDescription, + title = onboardingStore.getPrivacyProContent(TITLE), + description = onboardingStore.getPrivacyProContent(DESCRIPTION), placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, - primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, + primaryCta = onboardingStore.getPrivacyProContent(PRIMARY_BUTTON), secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN, okPixel = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index a2270e9bf3d5..4c525dc69ad3 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -288,8 +288,8 @@ class CtaViewModel @Inject constructor( } private suspend fun canShowPrivacyProCta(): Boolean { - return daxOnboardingActive() && !hideTips() && !daxDialogPrivacyProShown() && subscriptions.isEligible() && - extendedOnboardingFeatureToggles.privacyProCta().isEnabled() + return daxOnboardingActive() && !hideTips() && !daxDialogPrivacyProShown() && + extendedOnboardingFeatureToggles.privacyProOnboardingCopyNov24().isEnabled() } @WorkerThread diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt index 0da349b25523..ad3120858c11 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt @@ -17,10 +17,12 @@ package com.duckduckgo.app.onboarding.store import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType interface OnboardingStore { var onboardingDialogJourney: String? fun getSearchOptions(): List fun getSitesOptions(): List fun getExperimentSearchOptions(): List + fun getPrivacyProContent(content: ContentType): Int } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt index d18fcdadbb65..facf1bcdaa21 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt @@ -21,11 +21,19 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.duckduckgo.app.browser.R import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.DESCRIPTION +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.PRIMARY_BUTTON +import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.TITLE +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Cohorts import com.duckduckgo.mobile.android.R.drawable import java.util.Locale import javax.inject.Inject -class OnboardingStoreImpl @Inject constructor(private val context: Context) : OnboardingStore { +class OnboardingStoreImpl @Inject constructor( + private val context: Context, + private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles, +) : OnboardingStore { private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } @@ -205,6 +213,42 @@ class OnboardingStoreImpl @Inject constructor(private val context: Context) : On ) } + override fun getPrivacyProContent(content: ContentType): Int { + val cohort = extendedOnboardingFeatureToggles.privacyProOnboardingCopyNov24().getCohort()?.name + return when (content) { + TITLE -> { + when (cohort) { + Cohorts.PROTECTION.name -> R.string.onboardingPrivacyProProtectionDaxDialogTitle + Cohorts.PIR.name -> R.string.onboardingPrivacyProPirDaxDialogTitle + Cohorts.VPN.name -> R.string.onboardingPrivacyProVpnDaxDialogTitle + else -> R.string.onboardingPrivacyProDaxDialogTitle + } + } + DESCRIPTION -> { + when (cohort) { + Cohorts.PROTECTION.name -> R.string.onboardingPrivacyProProtectionDaxDialogDescription + Cohorts.PIR.name -> R.string.onboardingPrivacyProPirDaxDialogDescription + Cohorts.VPN.name -> R.string.onboardingPrivacyProVpnDaxDialogDescription + else -> R.string.onboardingPrivacyProDaxDialogDescription + } + } + PRIMARY_BUTTON -> { + when (cohort) { + Cohorts.PROTECTION.name -> R.string.onboardingPrivacyProProtectionDaxDialogOkButton + Cohorts.PIR.name -> R.string.onboardingPrivacyProPirDaxDialogOkButton + Cohorts.VPN.name -> R.string.onboardingPrivacyProVpnDaxDialogOkButton + else -> R.string.onboardingPrivacyProDaxDialogOkButton + } + } + } + } + + enum class ContentType { + TITLE, + DESCRIPTION, + PRIMARY_BUTTON, + } + companion object { const val FILENAME = "com.duckduckgo.app.onboarding.settings" const val ONBOARDING_JOURNEY = "onboardingJourney" diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt index e748f26f530f..b36d666ef878 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt @@ -20,6 +20,7 @@ import com.duckduckgo.anvil.annotations.ContributesRemoteFeature import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.Experiment +import com.duckduckgo.feature.toggles.api.Toggle.State.CohortName @ContributesRemoteFeature( scope = AppScope::class, @@ -39,4 +40,14 @@ interface ExtendedOnboardingFeatureToggles { @Toggle.DefaultValue(false) @Experiment fun highlights(): Toggle + + @Toggle.DefaultValue(false) + fun privacyProOnboardingCopyNov24(): Toggle + + enum class Cohorts(override val cohortName: String) : CohortName { + CONTROL("control"), + PROTECTION("protection"), + PIR("pir"), + VPN("vpn"), + } } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 727181f5eb9f..30bbea1681e3 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -63,4 +63,18 @@ The government may be blocking access to duckduckgo.com on this network provider, which could affect this app\'s functionality. Other providers may not be affected. Okay + + Protection… + VPN!

Activate it with a paid Privacy Pro subscription.]]>
+ Get More + Skip + PIR… + VPN!

Activate it with a paid Privacy Pro subscription.]]>
+ PIR More + Skip + VPN… + VPN!

Activate it with a paid Privacy Pro subscription.]]>
+ VPN More + Skip + From 8991cc3d0561ba20493de84ea4c6ede94322dd62 Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Fri, 8 Nov 2024 19:34:01 +0100 Subject: [PATCH 4/8] Implement experiment with new test framework --- .../app/browser/BrowserTabViewModel.kt | 5 +- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 49 +++++++++++---- .../onboarding/store/OnboardingStoreImpl.kt | 34 +++++++---- .../ExtendedOnboardingFeatureToggles.kt | 60 ++++++++++++++++++- 4 files changed, 122 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 4e1124a67579..35cc20bcc485 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -2632,7 +2632,9 @@ class BrowserTabViewModel @Inject constructor( } fun onUserClickCtaOkButton(cta: Cta) { - ctaViewModel.onUserClickCtaOkButton(cta) + viewModelScope.launch { + ctaViewModel.onUserClickCtaOkButton(cta) + } val onboardingCommand = when (cta) { is HomePanelCta.AddWidgetAuto, is HomePanelCta.AddWidgetInstructions -> LaunchAddWidget is OnboardingDaxDialogCta -> onOnboardingCtaOkButtonClicked(cta) @@ -2647,6 +2649,7 @@ class BrowserTabViewModel @Inject constructor( fun onUserClickCtaSecondaryButton(cta: Cta) { viewModelScope.launch { ctaViewModel.onUserDismissedCta(cta) + ctaViewModel.onUserClickCtaSkipButton(cta) if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) { val updatedCta = refreshCta() ctaViewState.value = currentCtaViewState().copy(cta = updatedCta) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 4c525dc69ad3..39ea1e5f53e5 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -31,7 +31,10 @@ import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingPixelsPlugin import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.testPrivacyProOnboardingPrimaryButtonMetricPixel +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.testPrivacyProOnboardingShownMetricPixel import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.app.settings.db.SettingsDataStore @@ -71,6 +74,7 @@ class CtaViewModel @Inject constructor( private val subscriptions: Subscriptions, private val duckPlayer: DuckPlayer, private val highlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager, + private val extendedOnboardingPixelsPlugin: ExtendedOnboardingPixelsPlugin, ) { @ExperimentalCoroutinesApi @VisibleForTesting @@ -87,8 +91,8 @@ class CtaViewModel @Inject constructor( } } - private suspend fun requiredDaxOnboardingCtas(): Array { - val shouldShowPrivacyProCta = extendedOnboardingFeatureToggles.privacyProCta().isEnabled() && subscriptions.isEligible() + private fun requiredDaxOnboardingCtas(): Array { + val shouldShowPrivacyProCta = extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled() return if (shouldShowPrivacyProCta) { arrayOf( CtaId.DAX_INTRO, @@ -116,7 +120,7 @@ class CtaViewModel @Inject constructor( } } - fun onCtaShown(cta: Cta) { + suspend fun onCtaShown(cta: Cta) { cta.shownPixel?.let { val canSendPixel = when (cta) { is DaxCta -> cta.canSendShownPixel() @@ -129,6 +133,13 @@ class CtaViewModel @Inject constructor( if (cta is OnboardingDaxDialogCta && cta.markAsReadOnShow) { dismissedCtaDao.insert(DismissedCta(cta.ctaId)) } + withContext(dispatchers.io()) { + if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) { + extendedOnboardingPixelsPlugin.testPrivacyProOnboardingShownMetricPixel()?.getPixelDefinitions()?.forEach { + pixel.fire(it.pixelName, it.params) + } + } + } } suspend fun registerDaxBubbleCtaDismissed(cta: Cta) { @@ -159,10 +170,27 @@ class CtaViewModel @Inject constructor( } } - fun onUserClickCtaOkButton(cta: Cta) { + suspend fun onUserClickCtaOkButton(cta: Cta) { cta.okPixel?.let { pixel.fire(it, cta.pixelOkParameters()) } + withContext(dispatchers.io()) { + if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) { + extendedOnboardingPixelsPlugin.testPrivacyProOnboardingPrimaryButtonMetricPixel()?.getPixelDefinitions()?.forEach { + pixel.fire(it.pixelName, it.params) + } + } + } + } + + suspend fun onUserClickCtaSkipButton(cta: Cta) { + withContext(dispatchers.io()) { + if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) { + extendedOnboardingPixelsPlugin.testPrivacyProOnboardingPrimaryButtonMetricPixel()?.getPixelDefinitions()?.forEach { + pixel.fire(it.pixelName, it.params) + } + } + } } suspend fun refreshCta( @@ -288,8 +316,11 @@ class CtaViewModel @Inject constructor( } private suspend fun canShowPrivacyProCta(): Boolean { + Timber.e( + "NOELIA canShowPrivacyProCta - ${daxOnboardingActive()} - ${!hideTips()} - ${!daxDialogPrivacyProShown()} - ${extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled()}", + ) return daxOnboardingActive() && !hideTips() && !daxDialogPrivacyProShown() && - extendedOnboardingFeatureToggles.privacyProOnboardingCopyNov24().isEnabled() + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled() } @WorkerThread @@ -431,12 +462,8 @@ class CtaViewModel @Inject constructor( private suspend fun pulseAnimationDisabled(): Boolean = !daxOnboardingActive() || pulseFireButtonShown() || daxDialogFireEducationShown() || hideTips() - private suspend fun allOnboardingCtasShown(): Boolean { - return withContext(dispatchers.io()) { - requiredDaxOnboardingCtas().all { - dismissedCtaDao.exists(it) - } - } + private fun allOnboardingCtasShown(): Boolean { + return requiredDaxOnboardingCtas().all { dismissedCtaDao.exists(it) } } private fun forceStopFireButtonPulseAnimationFlow() = tabRepository.flowTabs.distinctUntilChanged() diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt index facf1bcdaa21..7a469d720fc1 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt @@ -214,29 +214,37 @@ class OnboardingStoreImpl @Inject constructor( } override fun getPrivacyProContent(content: ContentType): Int { - val cohort = extendedOnboardingFeatureToggles.privacyProOnboardingCopyNov24().getCohort()?.name return when (content) { TITLE -> { - when (cohort) { - Cohorts.PROTECTION.name -> R.string.onboardingPrivacyProProtectionDaxDialogTitle - Cohorts.PIR.name -> R.string.onboardingPrivacyProPirDaxDialogTitle - Cohorts.VPN.name -> R.string.onboardingPrivacyProVpnDaxDialogTitle + when { + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> + R.string.onboardingPrivacyProProtectionDaxDialogTitle + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> + R.string.onboardingPrivacyProPirDaxDialogTitle + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> + R.string.onboardingPrivacyProVpnDaxDialogTitle else -> R.string.onboardingPrivacyProDaxDialogTitle } } DESCRIPTION -> { - when (cohort) { - Cohorts.PROTECTION.name -> R.string.onboardingPrivacyProProtectionDaxDialogDescription - Cohorts.PIR.name -> R.string.onboardingPrivacyProPirDaxDialogDescription - Cohorts.VPN.name -> R.string.onboardingPrivacyProVpnDaxDialogDescription + when { + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> + R.string.onboardingPrivacyProProtectionDaxDialogDescription + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> + R.string.onboardingPrivacyProPirDaxDialogDescription + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> + R.string.onboardingPrivacyProVpnDaxDialogDescription else -> R.string.onboardingPrivacyProDaxDialogDescription } } PRIMARY_BUTTON -> { - when (cohort) { - Cohorts.PROTECTION.name -> R.string.onboardingPrivacyProProtectionDaxDialogOkButton - Cohorts.PIR.name -> R.string.onboardingPrivacyProPirDaxDialogOkButton - Cohorts.VPN.name -> R.string.onboardingPrivacyProVpnDaxDialogOkButton + when { + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> + R.string.onboardingPrivacyProProtectionDaxDialogOkButton + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> + R.string.onboardingPrivacyProPirDaxDialogOkButton + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> + R.string.onboardingPrivacyProVpnDaxDialogOkButton else -> R.string.onboardingPrivacyProDaxDialogOkButton } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt index b36d666ef878..8ce9dbf08b23 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt @@ -17,10 +17,17 @@ package com.duckduckgo.app.onboarding.ui.page.extendedonboarding import com.duckduckgo.anvil.annotations.ContributesRemoteFeature +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Companion.EXPERIMENT_PREFIX import com.duckduckgo.di.scopes.AppScope +import com.duckduckgo.feature.toggles.api.ConversionWindow +import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory +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.Experiment import com.duckduckgo.feature.toggles.api.Toggle.State.CohortName +import com.squareup.anvil.annotations.ContributesMultibinding +import javax.inject.Inject @ContributesRemoteFeature( scope = AppScope::class, @@ -42,7 +49,7 @@ interface ExtendedOnboardingFeatureToggles { fun highlights(): Toggle @Toggle.DefaultValue(false) - fun privacyProOnboardingCopyNov24(): Toggle + fun testPrivacyProOnboardingCopyNov24(): Toggle enum class Cohorts(override val cohortName: String) : CohortName { CONTROL("control"), @@ -50,4 +57,55 @@ interface ExtendedOnboardingFeatureToggles { PIR("pir"), VPN("vpn"), } + + companion object { + const val EXPERIMENT_PREFIX = "test" + } +} + +internal suspend fun ExtendedOnboardingPixelsPlugin.testPrivacyProOnboardingShownMetricPixel(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == "dialogShown" } +} + +internal suspend fun ExtendedOnboardingPixelsPlugin.testPrivacyProOnboardingPrimaryButtonMetricPixel(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == "primaryButtonSelected" } +} + +internal suspend fun ExtendedOnboardingPixelsPlugin.testPrivacyProOnboardingSecondaryButtonMetricPixel(): MetricsPixel? { + return this.getMetrics().firstOrNull { it.metric == "secondaryButtonSelected" } +} + +suspend fun FeatureTogglesInventory.onboardingExperiments(): Toggle? { + return this.getAllTogglesForParent("extendedOnboarding").firstOrNull { + it.featureName().name.startsWith(EXPERIMENT_PREFIX) && it.isEnabled() + } +} + +@ContributesMultibinding(AppScope::class) +class ExtendedOnboardingPixelsPlugin @Inject constructor(private val inventory: FeatureTogglesInventory) : MetricsPixelPlugin { + + override suspend fun getMetrics(): List { + val activeToggle = inventory.onboardingExperiments() ?: return emptyList() + + return listOf( + MetricsPixel( + metric = "dialogShown", + value = "1", + toggle = activeToggle, + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = Int.MAX_VALUE)), + ), + MetricsPixel( + metric = "primaryButtonSelected", + value = "1", + toggle = activeToggle, + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = Int.MAX_VALUE)), + ), + MetricsPixel( + metric = "secondaryButtonSelected", + value = "1", + toggle = activeToggle, + conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = Int.MAX_VALUE)), + ), + ) + } } From d7ff8e2fc483708640a58efaf37bfe0cfebe0d25 Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Sat, 9 Nov 2024 01:27:20 +0100 Subject: [PATCH 5/8] fix tests --- .../app/browser/BrowserTabViewModelTest.kt | 31 +++++++++++++- .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 41 ++++++++++++++++--- .../app/fakes/FakeFeatureTogglesInventory.kt | 27 ++++++++++++ .../app/browser/BrowserTabViewModel.kt | 9 ++-- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 17 ++++++-- .../app/cta/ui/OnboardingDaxDialogTests.kt | 3 ++ 6 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 2bd56dd64447..fa963e8abc9f 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -130,6 +130,7 @@ import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta +import com.duckduckgo.app.fakes.FakeFeatureTogglesInventory import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepositoryImpl @@ -148,6 +149,7 @@ import com.duckduckgo.app.onboarding.store.AppStage.ESTABLISHED import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingPixelsPlugin import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_BANNER_SHOWN @@ -209,8 +211,13 @@ import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Disabled import com.duckduckgo.duckplayer.api.PrivatePlayerMode.Enabled import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory +import com.duckduckgo.feature.toggles.api.FakeToggleStore +import com.duckduckgo.feature.toggles.api.FeatureToggle +import com.duckduckgo.feature.toggles.api.FeatureToggles +import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.State +import com.duckduckgo.feature.toggles.impl.RealFeatureTogglesInventory import com.duckduckgo.history.api.HistoryEntry.VisitedPage import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels @@ -490,6 +497,9 @@ class BrowserTabViewModelTest { private var fakeAndroidConfigBrowserFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java) private val mockAutocompleteTabsFeature: AutocompleteTabsFeature = mock() private val fakeCustomHeadersPlugin = FakeCustomHeadersProvider(emptyMap()) + private lateinit var extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles + private lateinit var extendedOnboardingPixelsPlugin: ExtendedOnboardingPixelsPlugin + private lateinit var inventory: FeatureTogglesInventory @Before fun before() = runTest { @@ -517,6 +527,24 @@ class BrowserTabViewModelTest { lazyFaviconManager, ) + extendedOnboardingFeatureToggles = FeatureToggles.Builder( + FakeToggleStore(), + featureName = "extendedOnboarding", + ).build().create(ExtendedOnboardingFeatureToggles::class.java) + + inventory = RealFeatureTogglesInventory( + setOf( + FakeFeatureTogglesInventory( + features = listOf( + extendedOnboardingFeatureToggles.highlights(), + extendedOnboardingFeatureToggles.highlights(), + ), + ), + ), + coroutineRule.testDispatcherProvider, + ) + extendedOnboardingPixelsPlugin = ExtendedOnboardingPixelsPlugin(inventory) + whenever(mockHighlightsOnboardingExperimentManager.isHighlightsEnabled()).thenReturn(false) whenever(mockDuckPlayer.observeUserPreferences()).thenReturn(flowOf(UserPreferences(false, Disabled))) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow()) @@ -557,6 +585,7 @@ class BrowserTabViewModelTest { subscriptions = mock(), duckPlayer = mockDuckPlayer, highlightsOnboardingExperimentManager = mockHighlightsOnboardingExperimentManager, + extendedOnboardingPixelsPlugin = extendedOnboardingPixelsPlugin, ) val siteFactory = SiteFactoryImpl( @@ -2567,7 +2596,7 @@ class BrowserTabViewModelTest { val cta = DaxBubbleCta.DaxPrivacyProCta(mockOnboardingStore, mockAppInstallStore) setCta(cta) testee.onUserClickCtaOkButton(cta) - assertCommandIssued() + assertCommandIssued() } @Test diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 05f60543c4fc..87e233f7ed08 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -27,6 +27,7 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta +import com.duckduckgo.app.fakes.FakeFeatureTogglesInventory import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site @@ -34,6 +35,7 @@ import com.duckduckgo.app.onboarding.store.AppStage import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingPixelsPlugin import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager import com.duckduckgo.app.pixels.AppPixelName.* import com.duckduckgo.app.privacy.db.UserAllowListRepository @@ -57,7 +59,11 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.DISABLED import com.duckduckgo.duckplayer.api.DuckPlayer.DuckPlayerState.ENABLED import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk +import com.duckduckgo.feature.toggles.api.FakeToggleStore +import com.duckduckgo.feature.toggles.api.FeatureToggles +import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory import com.duckduckgo.feature.toggles.api.Toggle +import com.duckduckgo.feature.toggles.impl.RealFeatureTogglesInventory import com.duckduckgo.subscriptions.api.Subscriptions import java.util.concurrent.TimeUnit import kotlinx.coroutines.FlowPreview @@ -124,6 +130,10 @@ class CtaViewModelTest { CtaId.DAX_END, ) + private lateinit var extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles + private lateinit var extendedOnboardingPixelsPlugin: ExtendedOnboardingPixelsPlugin + private lateinit var inventory: FeatureTogglesInventory + private lateinit var testee: CtaViewModel val context: Context = InstrumentationRegistry.getInstrumentation().targetContext @@ -132,6 +142,24 @@ class CtaViewModelTest { @Before fun before() = runTest { + extendedOnboardingFeatureToggles = FeatureToggles.Builder( + FakeToggleStore(), + featureName = "extendedOnboarding", + ).build().create(ExtendedOnboardingFeatureToggles::class.java) + + inventory = RealFeatureTogglesInventory( + setOf( + FakeFeatureTogglesInventory( + features = listOf( + extendedOnboardingFeatureToggles.highlights(), + extendedOnboardingFeatureToggles.highlights(), + ), + ), + ), + coroutineRule.testDispatcherProvider, + ) + extendedOnboardingPixelsPlugin = ExtendedOnboardingPixelsPlugin(inventory) + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) .allowMainThreadQueries() .build() @@ -165,6 +193,7 @@ class CtaViewModelTest { subscriptions = mockSubscriptions, duckPlayer = mockDuckPlayer, highlightsOnboardingExperimentManager = mockHighlightsOnboardingExperimentManager, + extendedOnboardingPixelsPlugin = extendedOnboardingPixelsPlugin, ) } @@ -174,26 +203,26 @@ class CtaViewModelTest { } @Test - fun whenCtaShownAndCtaIsDaxAndCanNotSendPixelThenPixelIsNotFired() { + fun whenCtaShownAndCtaIsDaxAndCanNotSendPixelThenPixelIsNotFired() = runTest { testee.onCtaShown(DaxBubbleCta.DaxIntroSearchOptionsCta(mockOnboardingStore, mockAppInstallStore)) verify(mockPixel, never()).fire(eq(SURVEY_CTA_SHOWN), any(), any(), eq(Count)) } @Test - fun whenCtaShownAndCtaIsDaxAndCanSendPixelThenPixelIsFired() { + fun whenCtaShownAndCtaIsDaxAndCanSendPixelThenPixelIsFired() = runTest { whenever(mockOnboardingStore.onboardingDialogJourney).thenReturn("s:0") testee.onCtaShown(DaxBubbleCta.DaxEndCta(mockOnboardingStore, mockAppInstallStore)) verify(mockPixel, never()).fire(eq(SURVEY_CTA_SHOWN), any(), any(), eq(Count)) } @Test - fun whenCtaShownAndCtaIsNotDaxThenPixelIsFired() { + fun whenCtaShownAndCtaIsNotDaxThenPixelIsFired() = runTest { testee.onCtaShown(HomePanelCta.AddWidgetAuto) verify(mockPixel).fire(eq(WIDGET_CTA_SHOWN), any(), any(), eq(Count)) } @Test - fun whenCtaLaunchedPixelIsFired() { + fun whenCtaLaunchedPixelIsFired() = runTest { testee.onUserClickCtaOkButton(HomePanelCta.AddWidgetAuto) verify(mockPixel).fire(eq(WIDGET_CTA_LAUNCHED), any(), any(), eq(Count)) } @@ -669,14 +698,14 @@ class CtaViewModelTest { } @Test - fun whenCtaShownIfCtaIsNotMarkedAsReadOnShowThenCtaNotInsertedInDatabase() { + fun whenCtaShownIfCtaIsNotMarkedAsReadOnShowThenCtaNotInsertedInDatabase() = runTest { testee.onCtaShown(OnboardingDaxDialogCta.DaxSerpCta(mockOnboardingStore, mockAppInstallStore)) verify(mockDismissedCtaDao, never()).insert(DismissedCta(CtaId.DAX_DIALOG_SERP)) } @Test - fun whenCtaShownIfCtaIsMarkedAsReadOnShowThenCtaInsertedInDatabase() { + fun whenCtaShownIfCtaIsMarkedAsReadOnShowThenCtaInsertedInDatabase() = runTest { testee.onCtaShown(OnboardingDaxDialogCta.DaxEndCta(mockOnboardingStore, mockAppInstallStore, mockSettingsDataStore)) verify(mockDismissedCtaDao).insert(DismissedCta(CtaId.DAX_END)) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt b/app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt new file mode 100644 index 000000000000..a989ff9f11d5 --- /dev/null +++ b/app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 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.fakes + +import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory +import com.duckduckgo.feature.toggles.api.Toggle + +class FakeFeatureTogglesInventory(private val features: List) : FeatureTogglesInventory { + + override suspend fun getAll(): List { + return features + } +} diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 35cc20bcc485..f1cca0962278 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -3436,9 +3436,12 @@ class BrowserTabViewModel @Inject constructor( private fun onDaxBubbleCtaOkButtonClicked(cta: DaxBubbleCta): Command? { onUserDismissedCta(cta) return when (cta) { - is DaxBubbleCta.DaxPrivacyProCta, is DaxBubbleCta.DaxExperimentPrivacyProCta -> LaunchPrivacyPro( - "https://duckduckgo.com/pro?origin=funnel_pro_android_onboarding".toUri(), - ) + is DaxBubbleCta.DaxPrivacyProCta, is DaxBubbleCta.DaxExperimentPrivacyProCta -> { + val cohortOrigin = ctaViewModel.getCohortOrigin() + LaunchPrivacyPro( + "https://duckduckgo.com/pro?origin=funnel_pro_android_onboarding$cohortOrigin".toUri(), + ) + } is DaxBubbleCta.DaxEndCta, is DaxBubbleCta.DaxExperimentEndCta -> { viewModelScope.launch { val updatedCta = refreshCta() diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index 39ea1e5f53e5..dbc3c1b176a0 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -31,9 +31,11 @@ import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities import com.duckduckgo.app.onboarding.store.* import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Cohorts import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingPixelsPlugin import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.testPrivacyProOnboardingPrimaryButtonMetricPixel +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.testPrivacyProOnboardingSecondaryButtonMetricPixel import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.testPrivacyProOnboardingShownMetricPixel import com.duckduckgo.app.pixels.AppPixelName.ONBOARDING_SKIP_MAJOR_NETWORK_UNIQUE import com.duckduckgo.app.privacy.db.UserAllowListRepository @@ -186,7 +188,7 @@ class CtaViewModel @Inject constructor( suspend fun onUserClickCtaSkipButton(cta: Cta) { withContext(dispatchers.io()) { if (cta is DaxBubbleCta.DaxPrivacyProCta || cta is DaxBubbleCta.DaxExperimentPrivacyProCta) { - extendedOnboardingPixelsPlugin.testPrivacyProOnboardingPrimaryButtonMetricPixel()?.getPixelDefinitions()?.forEach { + extendedOnboardingPixelsPlugin.testPrivacyProOnboardingSecondaryButtonMetricPixel()?.getPixelDefinitions()?.forEach { pixel.fire(it.pixelName, it.params) } } @@ -316,9 +318,6 @@ class CtaViewModel @Inject constructor( } private suspend fun canShowPrivacyProCta(): Boolean { - Timber.e( - "NOELIA canShowPrivacyProCta - ${daxOnboardingActive()} - ${!hideTips()} - ${!daxDialogPrivacyProShown()} - ${extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled()}", - ) return daxOnboardingActive() && !hideTips() && !daxDialogPrivacyProShown() && extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled() } @@ -508,6 +507,16 @@ class CtaViewModel @Inject constructor( fun isSuggestedSiteOption(query: String): Boolean = onboardingStore.getSitesOptions().map { it.link }.contains(query) + fun getCohortOrigin(): String { + return when { + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> "_${Cohorts.PROTECTION.cohortName}" + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> "_${Cohorts.PIR.cohortName}" + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> "_${Cohorts.VPN.cohortName}" + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.CONTROL) -> "_${Cohorts.CONTROL.cohortName}" + else -> "" + } + } + companion object { private const val MAX_TABS_OPEN_FIRE_EDUCATION = 2 } diff --git a/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt b/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt index 9d40ba9a1d68..2ed39e0c1ae1 100644 --- a/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt +++ b/app/src/test/java/com/duckduckgo/app/cta/ui/OnboardingDaxDialogTests.kt @@ -28,6 +28,7 @@ import com.duckduckgo.app.onboarding.store.AppStage.DAX_ONBOARDING import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.UserStageStore import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles +import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingPixelsPlugin import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.HighlightsOnboardingExperimentManager import com.duckduckgo.app.privacy.db.UserAllowListRepository import com.duckduckgo.app.settings.db.SettingsDataStore @@ -74,6 +75,7 @@ class OnboardingDaxDialogTests { private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles = mock() private val mockDuckPlayer: DuckPlayer = mock() private val mockHighlightsOnboardingExperimentManager: HighlightsOnboardingExperimentManager = mock() + private val mockExtendedOnboardingPixelsPlugin: ExtendedOnboardingPixelsPlugin = mock() val mockEnabledToggle: Toggle = org.mockito.kotlin.mock { on { it.isEnabled() } doReturn true } val mockDisabledToggle: Toggle = org.mockito.kotlin.mock { on { it.isEnabled() } doReturn false } @@ -93,6 +95,7 @@ class OnboardingDaxDialogTests { subscriptions = mock(), mockDuckPlayer, mockHighlightsOnboardingExperimentManager, + mockExtendedOnboardingPixelsPlugin, ) } From b863ad305fd0c9829e8595cfd32c741b7eb6abac Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Sat, 9 Nov 2024 01:40:47 +0100 Subject: [PATCH 6/8] Update final copy --- .../main/java/com/duckduckgo/app/cta/ui/Cta.kt | 5 ++--- .../onboarding/store/OnboardingStoreImpl.kt | 13 ------------- app/src/main/res/values/donottranslate.xml | 18 ++++++------------ 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index ce877691d326..1faf0dc96d2e 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -38,7 +38,6 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.onboarding.store.OnboardingStore import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.DESCRIPTION -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.PRIMARY_BUTTON import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.TITLE import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.settings.db.SettingsDataStore @@ -976,7 +975,7 @@ sealed class DaxBubbleCta( title = onboardingStore.getPrivacyProContent(TITLE), description = onboardingStore.getPrivacyProContent(DESCRIPTION), placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, - primaryCta = onboardingStore.getPrivacyProContent(PRIMARY_BUTTON), + primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN, okPixel = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, @@ -1041,7 +1040,7 @@ sealed class DaxBubbleCta( title = onboardingStore.getPrivacyProContent(TITLE), description = onboardingStore.getPrivacyProContent(DESCRIPTION), placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, - primaryCta = onboardingStore.getPrivacyProContent(PRIMARY_BUTTON), + primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, shownPixel = AppPixelName.ONBOARDING_DAX_CTA_SHOWN, okPixel = AppPixelName.ONBOARDING_DAX_CTA_OK_BUTTON, diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt index 7a469d720fc1..a21610902c6e 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt @@ -22,7 +22,6 @@ import androidx.core.content.edit import com.duckduckgo.app.browser.R import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.DESCRIPTION -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.PRIMARY_BUTTON import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.TITLE import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Cohorts @@ -237,24 +236,12 @@ class OnboardingStoreImpl @Inject constructor( else -> R.string.onboardingPrivacyProDaxDialogDescription } } - PRIMARY_BUTTON -> { - when { - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> - R.string.onboardingPrivacyProProtectionDaxDialogOkButton - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> - R.string.onboardingPrivacyProPirDaxDialogOkButton - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> - R.string.onboardingPrivacyProVpnDaxDialogOkButton - else -> R.string.onboardingPrivacyProDaxDialogOkButton - } - } } } enum class ContentType { TITLE, DESCRIPTION, - PRIMARY_BUTTON, } companion object { diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 30bbea1681e3..73e059c19f31 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -64,17 +64,11 @@ Okay - Protection… - VPN!

Activate it with a paid Privacy Pro subscription.]]>
- Get More - Skip - PIR… - VPN!

Activate it with a paid Privacy Pro subscription.]]>
- PIR More - Skip - VPN… - VPN!

Activate it with a paid Privacy Pro subscription.]]>
- VPN More - Skip + Oh, just one more step… + VPN!

Activate it with a paid Privacy Pro subscription.]]>
+ Ready for a deal…? + VPN.

Activate it with a paid Privacy Pro subscription.]]>
+ Get even more protection… + VPN!

Activate it with a paid Privacy Pro subscription.]]>
From 242abdaf153fcef415af6245f2b8edd71b8a922e Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Fri, 15 Nov 2024 15:27:52 +0000 Subject: [PATCH 7/8] Improved feature toggle cohort assignation --- .../java/com/duckduckgo/app/cta/ui/Cta.kt | 14 +-- .../com/duckduckgo/app/cta/ui/CtaViewModel.kt | 85 ++++++++++++++++++- .../app/onboarding/store/OnboardingStore.kt | 2 - .../onboarding/store/OnboardingStoreImpl.kt | 37 -------- .../ExtendedOnboardingFeatureToggles.kt | 22 +---- 5 files changed, 93 insertions(+), 67 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt index 1faf0dc96d2e..cd4022c4f5f4 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/Cta.kt @@ -37,8 +37,6 @@ import com.duckduckgo.app.cta.ui.DaxCta.Companion.MAX_DAYS_ALLOWED import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.install.daysInstalled import com.duckduckgo.app.onboarding.store.OnboardingStore -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.DESCRIPTION -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.TITLE import com.duckduckgo.app.pixels.AppPixelName import com.duckduckgo.app.settings.db.SettingsDataStore import com.duckduckgo.app.statistics.pixels.Pixel @@ -970,10 +968,12 @@ sealed class DaxBubbleCta( data class DaxPrivacyProCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, + val titleRes: Int, + val descriptionRes: Int, ) : DaxBubbleCta( ctaId = CtaId.DAX_INTRO_PRIVACY_PRO, - title = onboardingStore.getPrivacyProContent(TITLE), - description = onboardingStore.getPrivacyProContent(DESCRIPTION), + title = titleRes, + description = descriptionRes, placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, @@ -1035,10 +1035,12 @@ sealed class DaxBubbleCta( data class DaxExperimentPrivacyProCta( override val onboardingStore: OnboardingStore, override val appInstallStore: AppInstallStore, + val titleRes: Int, + val descriptionRes: Int, ) : DaxBubbleCta( ctaId = CtaId.DAX_INTRO_PRIVACY_PRO, - title = onboardingStore.getPrivacyProContent(TITLE), - description = onboardingStore.getPrivacyProContent(DESCRIPTION), + title = titleRes, + description = descriptionRes, placeholder = com.duckduckgo.mobile.android.R.drawable.ic_privacy_pro_128, primaryCta = R.string.onboardingPrivacyProDaxDialogOkButton, secondaryCta = R.string.onboardingPrivacyProDaxDialogCancelButton, diff --git a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt index dbc3c1b176a0..8c72f5ce4b71 100644 --- a/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/cta/ui/CtaViewModel.kt @@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread import androidx.core.net.toUri import com.duckduckgo.app.browser.DuckDuckGoUrlDetector +import com.duckduckgo.app.browser.R import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta @@ -29,7 +30,10 @@ import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site import com.duckduckgo.app.global.model.domain import com.duckduckgo.app.global.model.orderedTrackerBlockedEntities -import com.duckduckgo.app.onboarding.store.* +import com.duckduckgo.app.onboarding.store.AppStage +import com.duckduckgo.app.onboarding.store.OnboardingStore +import com.duckduckgo.app.onboarding.store.UserStageStore +import com.duckduckgo.app.onboarding.store.daxOnboardingActive import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Cohorts import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingPixelsPlugin @@ -55,7 +59,14 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import timber.log.Timber @@ -245,6 +256,7 @@ class CtaViewModel @Inject constructor( userStageStore.stageCompleted(AppStage.DAX_ONBOARDING) null } + canShowDaxIntroCta() && !extendedOnboardingFeatureToggles.noBrowserCtas().isEnabled() -> { if (highlightsOnboardingExperimentManager.isHighlightsEnabled()) { DaxBubbleCta.DaxExperimentIntroSearchOptionsCta(onboardingStore, appInstallStore) @@ -271,9 +283,73 @@ class CtaViewModel @Inject constructor( canShowPrivacyProCta() -> { if (highlightsOnboardingExperimentManager.isHighlightsEnabled()) { - DaxBubbleCta.DaxExperimentPrivacyProCta(onboardingStore, appInstallStore) + when { + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> + DaxBubbleCta.DaxExperimentPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProProtectionDaxDialogTitle, + R.string.onboardingPrivacyProProtectionDaxDialogDescription, + ) + + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> + DaxBubbleCta.DaxExperimentPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProPirDaxDialogTitle, + R.string.onboardingPrivacyProPirDaxDialogDescription, + ) + + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> + DaxBubbleCta.DaxExperimentPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProVpnDaxDialogTitle, + R.string.onboardingPrivacyProVpnDaxDialogDescription, + ) + + else -> + DaxBubbleCta.DaxExperimentPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProDaxDialogTitle, + R.string.onboardingPrivacyProDaxDialogDescription, + ) + } } else { - DaxBubbleCta.DaxPrivacyProCta(onboardingStore, appInstallStore) + when { + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> + DaxBubbleCta.DaxPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProProtectionDaxDialogTitle, + R.string.onboardingPrivacyProProtectionDaxDialogDescription, + ) + + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> + DaxBubbleCta.DaxPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProPirDaxDialogTitle, + R.string.onboardingPrivacyProPirDaxDialogDescription, + ) + + extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> + DaxBubbleCta.DaxPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProVpnDaxDialogTitle, + R.string.onboardingPrivacyProVpnDaxDialogDescription, + ) + + else -> + DaxBubbleCta.DaxPrivacyProCta( + onboardingStore, + appInstallStore, + R.string.onboardingPrivacyProDaxDialogTitle, + R.string.onboardingPrivacyProDaxDialogDescription, + ) + } } } @@ -313,6 +389,7 @@ class CtaViewModel @Inject constructor( userStageStore.stageCompleted(AppStage.DAX_ONBOARDING) false } + else -> true } } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt index ad3120858c11..0da349b25523 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStore.kt @@ -17,12 +17,10 @@ package com.duckduckgo.app.onboarding.store import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType interface OnboardingStore { var onboardingDialogJourney: String? fun getSearchOptions(): List fun getSitesOptions(): List fun getExperimentSearchOptions(): List - fun getPrivacyProContent(content: ContentType): Int } diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt index a21610902c6e..87a4d1227c7d 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/store/OnboardingStoreImpl.kt @@ -21,17 +21,12 @@ import android.content.SharedPreferences import androidx.core.content.edit import com.duckduckgo.app.browser.R import com.duckduckgo.app.cta.ui.DaxBubbleCta.DaxDialogIntroOption -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.DESCRIPTION -import com.duckduckgo.app.onboarding.store.OnboardingStoreImpl.ContentType.TITLE -import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles -import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Cohorts import com.duckduckgo.mobile.android.R.drawable import java.util.Locale import javax.inject.Inject class OnboardingStoreImpl @Inject constructor( private val context: Context, - private val extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles, ) : OnboardingStore { private val preferences: SharedPreferences by lazy { context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) } @@ -212,38 +207,6 @@ class OnboardingStoreImpl @Inject constructor( ) } - override fun getPrivacyProContent(content: ContentType): Int { - return when (content) { - TITLE -> { - when { - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> - R.string.onboardingPrivacyProProtectionDaxDialogTitle - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> - R.string.onboardingPrivacyProPirDaxDialogTitle - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> - R.string.onboardingPrivacyProVpnDaxDialogTitle - else -> R.string.onboardingPrivacyProDaxDialogTitle - } - } - DESCRIPTION -> { - when { - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PROTECTION) -> - R.string.onboardingPrivacyProProtectionDaxDialogDescription - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.PIR) -> - R.string.onboardingPrivacyProPirDaxDialogDescription - extendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24().isEnabled(Cohorts.VPN) -> - R.string.onboardingPrivacyProVpnDaxDialogDescription - else -> R.string.onboardingPrivacyProDaxDialogDescription - } - } - } - } - - enum class ContentType { - TITLE, - DESCRIPTION, - } - companion object { const val FILENAME = "com.duckduckgo.app.onboarding.settings" const val ONBOARDING_JOURNEY = "onboardingJourney" diff --git a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt index 8ce9dbf08b23..16ea6e08fd8b 100644 --- a/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt +++ b/app/src/main/java/com/duckduckgo/app/onboarding/ui/page/extendedonboarding/ExtendedOnboardingFeatureToggles.kt @@ -17,10 +17,8 @@ package com.duckduckgo.app.onboarding.ui.page.extendedonboarding import com.duckduckgo.anvil.annotations.ContributesRemoteFeature -import com.duckduckgo.app.onboarding.ui.page.extendedonboarding.ExtendedOnboardingFeatureToggles.Companion.EXPERIMENT_PREFIX import com.duckduckgo.di.scopes.AppScope import com.duckduckgo.feature.toggles.api.ConversionWindow -import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory import com.duckduckgo.feature.toggles.api.MetricsPixel import com.duckduckgo.feature.toggles.api.MetricsPixelPlugin import com.duckduckgo.feature.toggles.api.Toggle @@ -57,10 +55,6 @@ interface ExtendedOnboardingFeatureToggles { PIR("pir"), VPN("vpn"), } - - companion object { - const val EXPERIMENT_PREFIX = "test" - } } internal suspend fun ExtendedOnboardingPixelsPlugin.testPrivacyProOnboardingShownMetricPixel(): MetricsPixel? { @@ -75,35 +69,27 @@ internal suspend fun ExtendedOnboardingPixelsPlugin.testPrivacyProOnboardingSeco return this.getMetrics().firstOrNull { it.metric == "secondaryButtonSelected" } } -suspend fun FeatureTogglesInventory.onboardingExperiments(): Toggle? { - return this.getAllTogglesForParent("extendedOnboarding").firstOrNull { - it.featureName().name.startsWith(EXPERIMENT_PREFIX) && it.isEnabled() - } -} - @ContributesMultibinding(AppScope::class) -class ExtendedOnboardingPixelsPlugin @Inject constructor(private val inventory: FeatureTogglesInventory) : MetricsPixelPlugin { +class ExtendedOnboardingPixelsPlugin @Inject constructor(private val toggle: ExtendedOnboardingFeatureToggles) : MetricsPixelPlugin { override suspend fun getMetrics(): List { - val activeToggle = inventory.onboardingExperiments() ?: return emptyList() - return listOf( MetricsPixel( metric = "dialogShown", value = "1", - toggle = activeToggle, + toggle = toggle.testPrivacyProOnboardingCopyNov24(), conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = Int.MAX_VALUE)), ), MetricsPixel( metric = "primaryButtonSelected", value = "1", - toggle = activeToggle, + toggle = toggle.testPrivacyProOnboardingCopyNov24(), conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = Int.MAX_VALUE)), ), MetricsPixel( metric = "secondaryButtonSelected", value = "1", - toggle = activeToggle, + toggle = toggle.testPrivacyProOnboardingCopyNov24(), conversionWindow = listOf(ConversionWindow(lowerWindow = 0, upperWindow = Int.MAX_VALUE)), ), ) From 39c1a5e11b226e89a45402e0bb08422c6372161c Mon Sep 17 00:00:00 2001 From: Noelia Alcala Date: Fri, 15 Nov 2024 16:09:56 +0000 Subject: [PATCH 8/8] tests --- .../app/browser/BrowserTabViewModelTest.kt | 41 +++++++------------ .../duckduckgo/app/cta/ui/CtaViewModelTest.kt | 31 +++----------- .../app/fakes/FakeFeatureTogglesInventory.kt | 27 ------------ 3 files changed, 20 insertions(+), 79 deletions(-) delete mode 100644 app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index fa963e8abc9f..b946f736e26b 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -130,7 +130,6 @@ import com.duckduckgo.app.cta.ui.CtaViewModel import com.duckduckgo.app.cta.ui.DaxBubbleCta import com.duckduckgo.app.cta.ui.HomePanelCta import com.duckduckgo.app.cta.ui.OnboardingDaxDialogCta -import com.duckduckgo.app.fakes.FakeFeatureTogglesInventory import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteDao import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteEntity import com.duckduckgo.app.fire.fireproofwebsite.data.FireproofWebsiteRepositoryImpl @@ -214,10 +213,8 @@ import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory import com.duckduckgo.feature.toggles.api.FakeToggleStore import com.duckduckgo.feature.toggles.api.FeatureToggle import com.duckduckgo.feature.toggles.api.FeatureToggles -import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory import com.duckduckgo.feature.toggles.api.Toggle import com.duckduckgo.feature.toggles.api.Toggle.State -import com.duckduckgo.feature.toggles.impl.RealFeatureTogglesInventory import com.duckduckgo.history.api.HistoryEntry.VisitedPage import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.newtabpage.impl.pixels.NewTabPixels @@ -470,7 +467,10 @@ class BrowserTabViewModelTest { private val mockEnabledToggle: Toggle = mock { on { it.isEnabled() } doReturn true } - private val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false } + private val mockDisabledToggle: Toggle = mock { + on { it.isEnabled() } doReturn false + on { it.isEnabled(any()) } doReturn false + } private val mockPrivacyProtectionsPopupManager: PrivacyProtectionsPopupManager = mock() @@ -497,9 +497,9 @@ class BrowserTabViewModelTest { private var fakeAndroidConfigBrowserFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java) private val mockAutocompleteTabsFeature: AutocompleteTabsFeature = mock() private val fakeCustomHeadersPlugin = FakeCustomHeadersProvider(emptyMap()) - private lateinit var extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles - private lateinit var extendedOnboardingPixelsPlugin: ExtendedOnboardingPixelsPlugin - private lateinit var inventory: FeatureTogglesInventory + private val extendedOnboardingFeatureToggles = FeatureToggles.Builder(FakeToggleStore(), featureName = "extendedOnboarding").build() + .create(ExtendedOnboardingFeatureToggles::class.java) + private val extendedOnboardingPixelsPlugin = ExtendedOnboardingPixelsPlugin(extendedOnboardingFeatureToggles) @Before fun before() = runTest { @@ -527,24 +527,7 @@ class BrowserTabViewModelTest { lazyFaviconManager, ) - extendedOnboardingFeatureToggles = FeatureToggles.Builder( - FakeToggleStore(), - featureName = "extendedOnboarding", - ).build().create(ExtendedOnboardingFeatureToggles::class.java) - - inventory = RealFeatureTogglesInventory( - setOf( - FakeFeatureTogglesInventory( - features = listOf( - extendedOnboardingFeatureToggles.highlights(), - extendedOnboardingFeatureToggles.highlights(), - ), - ), - ), - coroutineRule.testDispatcherProvider, - ) - extendedOnboardingPixelsPlugin = ExtendedOnboardingPixelsPlugin(inventory) - + whenever(mockExtendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24()).thenReturn(mockDisabledToggle) whenever(mockHighlightsOnboardingExperimentManager.isHighlightsEnabled()).thenReturn(false) whenever(mockDuckPlayer.observeUserPreferences()).thenReturn(flowOf(UserPreferences(false, Disabled))) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow()) @@ -2593,7 +2576,12 @@ class BrowserTabViewModelTest { @Test fun whenUserClickedLearnMoreExperimentBubbleCtaButtonThenLaunchPrivacyPro() { - val cta = DaxBubbleCta.DaxPrivacyProCta(mockOnboardingStore, mockAppInstallStore) + val cta = DaxBubbleCta.DaxPrivacyProCta( + mockOnboardingStore, + mockAppInstallStore, + R.string.onboardingPrivacyProDaxDialogTitle, + R.string.onboardingPrivacyProDaxDialogDescription, + ) setCta(cta) testee.onUserClickCtaOkButton(cta) assertCommandIssued() @@ -6031,6 +6019,7 @@ class BrowserTabViewModelTest { override suspend fun onToggleOff(origin: PrivacyToggleOrigin) { toggleOff++ } + override suspend fun onToggleOn(origin: PrivacyToggleOrigin) { toggleOn++ } diff --git a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt index 87e233f7ed08..9df10be9219d 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/cta/ui/CtaViewModelTest.kt @@ -27,7 +27,6 @@ import com.duckduckgo.app.browser.R import com.duckduckgo.app.cta.db.DismissedCtaDao import com.duckduckgo.app.cta.model.CtaId import com.duckduckgo.app.cta.model.DismissedCta -import com.duckduckgo.app.fakes.FakeFeatureTogglesInventory import com.duckduckgo.app.global.db.AppDatabase import com.duckduckgo.app.global.install.AppInstallStore import com.duckduckgo.app.global.model.Site @@ -61,9 +60,7 @@ import com.duckduckgo.duckplayer.api.DuckPlayer.UserPreferences import com.duckduckgo.duckplayer.api.PrivatePlayerMode.AlwaysAsk import com.duckduckgo.feature.toggles.api.FakeToggleStore import com.duckduckgo.feature.toggles.api.FeatureToggles -import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory import com.duckduckgo.feature.toggles.api.Toggle -import com.duckduckgo.feature.toggles.impl.RealFeatureTogglesInventory import com.duckduckgo.subscriptions.api.Subscriptions import java.util.concurrent.TimeUnit import kotlinx.coroutines.FlowPreview @@ -130,9 +127,9 @@ class CtaViewModelTest { CtaId.DAX_END, ) - private lateinit var extendedOnboardingFeatureToggles: ExtendedOnboardingFeatureToggles - private lateinit var extendedOnboardingPixelsPlugin: ExtendedOnboardingPixelsPlugin - private lateinit var inventory: FeatureTogglesInventory + private val extendedOnboardingFeatureToggles = FeatureToggles.Builder(FakeToggleStore(), featureName = "extendedOnboarding").build() + .create(ExtendedOnboardingFeatureToggles::class.java) + private val extendedOnboardingPixelsPlugin = ExtendedOnboardingPixelsPlugin(extendedOnboardingFeatureToggles) private lateinit var testee: CtaViewModel @@ -142,31 +139,13 @@ class CtaViewModelTest { @Before fun before() = runTest { - extendedOnboardingFeatureToggles = FeatureToggles.Builder( - FakeToggleStore(), - featureName = "extendedOnboarding", - ).build().create(ExtendedOnboardingFeatureToggles::class.java) - - inventory = RealFeatureTogglesInventory( - setOf( - FakeFeatureTogglesInventory( - features = listOf( - extendedOnboardingFeatureToggles.highlights(), - extendedOnboardingFeatureToggles.highlights(), - ), - ), - ), - coroutineRule.testDispatcherProvider, - ) - extendedOnboardingPixelsPlugin = ExtendedOnboardingPixelsPlugin(inventory) - db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) .allowMainThreadQueries() .build() val mockDisabledToggle: Toggle = mock { on { it.isEnabled() } doReturn false } whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockDisabledToggle) - whenever(mockExtendedOnboardingFeatureToggles.privacyProCta()).thenReturn(mockDisabledToggle) + whenever(mockExtendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24()).thenReturn(mockDisabledToggle) whenever(mockAppInstallStore.installTimestamp).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) whenever(mockUserAllowListRepository.isDomainInUserAllowList(any())).thenReturn(false) whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(db.dismissedCtaDao().dismissedCtas()) @@ -746,7 +725,7 @@ class CtaViewModelTest { fun givenPrivacyProCtaExperimentWhenRefreshCtaOnHomeTabThenReturnPrivacyProCta() = runTest { givenDaxOnboardingActive() whenever(mockExtendedOnboardingFeatureToggles.noBrowserCtas()).thenReturn(mockEnabledToggle) - whenever(mockExtendedOnboardingFeatureToggles.privacyProCta()).thenReturn(mockEnabledToggle) + whenever(mockExtendedOnboardingFeatureToggles.testPrivacyProOnboardingCopyNov24()).thenReturn(mockEnabledToggle) whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_INTRO_VISIT_SITE)).thenReturn(true) whenever(mockDismissedCtaDao.exists(CtaId.DAX_END)).thenReturn(true) diff --git a/app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt b/app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt deleted file mode 100644 index a989ff9f11d5..000000000000 --- a/app/src/androidTest/java/com/duckduckgo/app/fakes/FakeFeatureTogglesInventory.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2024 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.fakes - -import com.duckduckgo.feature.toggles.api.FeatureTogglesInventory -import com.duckduckgo.feature.toggles.api.Toggle - -class FakeFeatureTogglesInventory(private val features: List) : FeatureTogglesInventory { - - override suspend fun getAll(): List { - return features - } -}